coze-studio/frontend/infra/idl/idl2ts-runtime/src/utils.ts

219 lines
6.9 KiB
TypeScript

/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import qs from 'qs';
import type { IMeta } from './types';
import { configCenter } from './config-center';
export interface ServiceConfig {
[key: string]: {
methods?: {
[key: string]: Omit<IdlConfig, 'clientFactory'>;
};
} & Omit<IdlConfig, 'clientFactory'>;
}
export interface IdlConfig {
// The client factory method requires a fetchClient function to be returned, which uses the meta total information to achieve flexible client configuration
clientFactory?: (
meta: IMeta,
) => (uri: string, init: RequestInit, opt: any) => any;
// URI prefix, if set in client, you can leave it unset here
uriPrefix?: string;
getParams?: (key: string) => string;
// Service level configuration
services?: ServiceConfig;
// During development, if the local verification fails, it can be called back here, usually by playing toast.
onVerifyReqError?: (message: string, ctx: any) => void;
}
export interface IOptions {
config?: IdlConfig;
// Passthrough request options
requestOptions?: Record<string, any>;
[key: string]: any;
}
export interface PathPrams<T> {
pathParams?: T;
}
export function getConfig(service: string, method: string): IdlConfig {
// Manually registered configuration takes precedence over global variables
let config: IdlConfig | undefined = configCenter.getConfig(service);
if (!config) {
config = {};
if (config.services && config.services[service]) {
const serviceConfig = config.services[service];
const { methods, ...rest } = serviceConfig;
Object.assign(config, rest);
if (methods && methods[method]) {
Object.assign(config, methods[method]);
}
}
delete config.services;
}
return config;
}
function getValue(origin: any, fields: string[]) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const res = {} as Record<string, any>;
fields.forEach(i => {
res[i] = origin[i];
});
return res;
}
// eslint-disable-next-line max-params
export function unifyUrl(
uri: string,
pathParams: string[],
option: IdlConfig & PathPrams<any>,
req: Record<string, any>,
): { apiUri: string; unmappedParams: string[] } {
let apiUri = uri;
pathParams = pathParams || [];
const unmappedParams = [] as string[];
const matches = apiUri.match(/:([^/]+)/g) || [];
if (matches.length === 0) {
return { apiUri, unmappedParams };
}
matches.forEach(item => {
const target = item.slice(1);
if (!pathParams.includes(target)) {
const param =
option.pathParams?.[target] ||
(option.getParams && option.getParams(target));
apiUri = apiUri.replace(item, param || '');
unmappedParams.push(target);
} else {
const param =
req[target] ||
option.pathParams?.[target] ||
option.pathParams?.[target] ||
(option.getParams && option.getParams(target));
apiUri = apiUri.replace(item, param);
}
});
return { apiUri, unmappedParams };
}
const ContentTypeMap = {
json: 'application/json',
urlencoded: 'application/x-www-form-urlencoded',
form: 'multipart/form-data',
};
// eslint-disable-next-line complexity
export function normalizeRequest(
req: Record<string, any>,
meta: IMeta,
option?: IOptions & PathPrams<any>,
) {
const config = {
...getConfig(meta.service, meta.method),
...(option?.config ?? {}),
};
const { apiUri } = unifyUrl(
meta.url,
meta.reqMapping.path || [],
{ ...config, pathParams: option?.pathParams ?? {} },
req,
);
const { uriPrefix = '', clientFactory } = config;
if (!clientFactory) {
// Todo here considers giving a default client to prevent some public packages from being used in some abnormal cases
throw new Error('Lack of clientFactory config');
}
let uri = uriPrefix + apiUri;
let headers: Record<string, string> = {};
headers['Content-Type'] =
meta.serializer && ContentTypeMap[meta.serializer]
? ContentTypeMap[meta.serializer]
: 'application/json';
if (option?.requestOptions?.headers) {
headers = { ...headers, ...option.requestOptions.headers };
// Merged headers, can be deleted
delete option.requestOptions.headers;
}
if (meta.reqMapping.query && meta.reqMapping.query.length > 0) {
// The default here is skipNulls, and the gateway backend needs to ignore null.
uri = `${uri}?${qs.stringify(getValue(req, meta.reqMapping.query), {
skipNulls: true,
arrayFormat: 'comma',
})}`;
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const requestOption = {
method: meta.method,
headers,
credentials: 'same-origin',
} as RequestInit;
if (meta.reqMapping.entire_body && meta.reqMapping.entire_body.length > 0) {
if (meta.reqMapping.entire_body.length === 1) {
// The default processing is json. If there are other scenarios that need to be supported, they need to be supported later.
requestOption.body = req[meta.reqMapping.entire_body[0]];
} else {
throw new Error('idl invalid entire_body should be only one filed');
}
} else if (meta.reqMapping.body && meta.reqMapping.body.length > 0) {
const body = getValue(req, meta.reqMapping.body);
requestOption.body = body as BodyInit;
if (meta.serializer === 'form') {
const formData = new FormData();
Object.keys(body).forEach(key => {
const formItemValue =
body[key] instanceof File
? new Blob([body[key]], { type: body[key].type })
: body[key];
formData.append(key, formItemValue);
});
requestOption.body = formData;
}
if (meta.serializer === 'urlencoded') {
requestOption.body = qs.stringify(body, {
skipNulls: true,
arrayFormat: 'comma',
});
}
}
if (meta.reqMapping.header && meta.reqMapping.header.length > 0) {
requestOption.headers = {
...headers,
...getValue(req, meta.reqMapping.header),
};
}
// In the old version of ferry, even if idl does not declare body, you need to add an empty body.
if (
!requestOption.body &&
['POST', 'PUT', 'PATCH'].includes(
(requestOption.method || '').toUpperCase(),
)
) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
requestOption.body = {} as BodyInit;
}
return { uri, requestOption, client: clientFactory(meta) };
}