@weclapp/sdk 1.8.0-dev.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/bin/cli.js +2 -0
- package/dist/cli.js +1397 -0
- package/package.json +70 -0
- package/tsconfig.lib.json +17 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { rmdir, stat, readFile, writeFile, rm, cp, mkdir } from 'fs/promises';
|
|
4
|
+
import { rollup } from 'rollup';
|
|
5
|
+
import terser from '@rollup/plugin-terser';
|
|
6
|
+
import ts from 'rollup-plugin-ts';
|
|
7
|
+
import indentString from 'indent-string';
|
|
8
|
+
import { snakeCase, pascalCase, camelCase } from 'change-case';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import { config } from 'dotenv';
|
|
13
|
+
import yargs from 'yargs';
|
|
14
|
+
import { hideBin } from 'yargs/helpers';
|
|
15
|
+
import prettyMs from 'pretty-ms';
|
|
16
|
+
|
|
17
|
+
var Target;
|
|
18
|
+
(function (Target) {
|
|
19
|
+
Target["BROWSER_PROMISES"] = "browser";
|
|
20
|
+
Target["BROWSER_RX"] = "browser.rx";
|
|
21
|
+
Target["NODE_PROMISES"] = "node";
|
|
22
|
+
Target["NODE_RX"] = "node.rx";
|
|
23
|
+
})(Target || (Target = {}));
|
|
24
|
+
const isNodeTarget = (target) => {
|
|
25
|
+
return target === Target.NODE_PROMISES || target === Target.NODE_RX;
|
|
26
|
+
};
|
|
27
|
+
const isRXTarget = (target) => {
|
|
28
|
+
return target === Target.BROWSER_RX || target === Target.NODE_RX;
|
|
29
|
+
};
|
|
30
|
+
const resolveResponseType = (target) => {
|
|
31
|
+
return isRXTarget(target) ? 'Observable' : 'Promise';
|
|
32
|
+
};
|
|
33
|
+
const resolveBinaryType = (target) => {
|
|
34
|
+
return isNodeTarget(target) ? 'Buffer' : 'Blob';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const currentDirname = () => {
|
|
38
|
+
// Go one level up as the CLI is inside a folder
|
|
39
|
+
return fileURLToPath(new URL('..', import.meta.url));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const tsconfig = resolve(currentDirname(), './tsconfig.lib.json');
|
|
43
|
+
const resolveGlobals = (...globals) => Object.fromEntries(globals.map(v => [v, '*']));
|
|
44
|
+
const generateOutput = (config) => ({
|
|
45
|
+
sourcemap: true,
|
|
46
|
+
banner: `/* weclapp sdk */`,
|
|
47
|
+
...config
|
|
48
|
+
});
|
|
49
|
+
const bundle = async (workingDirectory, target) => {
|
|
50
|
+
const dist = (...paths) => resolve(workingDirectory, 'dist', ...paths);
|
|
51
|
+
const src = (...paths) => resolve(workingDirectory, 'src', ...paths);
|
|
52
|
+
const generateNodeOutput = () => [
|
|
53
|
+
generateOutput({
|
|
54
|
+
file: dist('index.cjs'),
|
|
55
|
+
format: 'cjs',
|
|
56
|
+
globals: resolveGlobals('node-fetch', 'url')
|
|
57
|
+
}),
|
|
58
|
+
generateOutput({
|
|
59
|
+
file: dist('index.js'),
|
|
60
|
+
format: 'es',
|
|
61
|
+
globals: resolveGlobals('node-fetch', 'url')
|
|
62
|
+
})
|
|
63
|
+
];
|
|
64
|
+
// Remove build dir
|
|
65
|
+
await rmdir(dist()).catch(() => void 0);
|
|
66
|
+
const bundles = {
|
|
67
|
+
[Target.BROWSER_PROMISES]: () => ({
|
|
68
|
+
input: src('browser.ts'),
|
|
69
|
+
output: [
|
|
70
|
+
generateOutput({
|
|
71
|
+
file: dist('index.cjs'),
|
|
72
|
+
name: 'Weclapp',
|
|
73
|
+
format: 'umd'
|
|
74
|
+
}),
|
|
75
|
+
generateOutput({
|
|
76
|
+
file: dist('index.js'),
|
|
77
|
+
format: 'es'
|
|
78
|
+
})
|
|
79
|
+
],
|
|
80
|
+
plugins: [ts({ tsconfig }), terser()]
|
|
81
|
+
}),
|
|
82
|
+
[Target.BROWSER_RX]: () => ({
|
|
83
|
+
external: ['rxjs'],
|
|
84
|
+
input: src('browser.rx.ts'),
|
|
85
|
+
plugins: [ts({ tsconfig }), terser()],
|
|
86
|
+
output: [
|
|
87
|
+
generateOutput({
|
|
88
|
+
file: dist('index.cjs'),
|
|
89
|
+
name: 'Weclapp',
|
|
90
|
+
format: 'umd',
|
|
91
|
+
globals: resolveGlobals('rxjs')
|
|
92
|
+
}),
|
|
93
|
+
generateOutput({
|
|
94
|
+
file: dist('index.js'),
|
|
95
|
+
format: 'es',
|
|
96
|
+
globals: resolveGlobals('rxjs')
|
|
97
|
+
})
|
|
98
|
+
]
|
|
99
|
+
}),
|
|
100
|
+
[Target.NODE_PROMISES]: () => ({
|
|
101
|
+
input: src('node.ts'),
|
|
102
|
+
output: generateNodeOutput(),
|
|
103
|
+
external: ['node-fetch', 'url'],
|
|
104
|
+
plugins: [ts({ tsconfig })]
|
|
105
|
+
}),
|
|
106
|
+
[Target.NODE_RX]: () => ({
|
|
107
|
+
input: src('node.rx.ts'),
|
|
108
|
+
output: generateNodeOutput(),
|
|
109
|
+
external: ['node-fetch', 'url', 'rxjs'],
|
|
110
|
+
plugins: [ts({ tsconfig })]
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
const config = bundles[target]();
|
|
114
|
+
const bundle = await rollup(config);
|
|
115
|
+
if (Array.isArray(config.output)) {
|
|
116
|
+
await Promise.all(config.output.map(bundle.write));
|
|
117
|
+
}
|
|
118
|
+
else if (config.output) {
|
|
119
|
+
await bundle.write(config.output);
|
|
120
|
+
}
|
|
121
|
+
await bundle.close();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const generateString = (str) => `'${str}'`;
|
|
125
|
+
const generateStrings = (str) => str.map(generateString);
|
|
126
|
+
|
|
127
|
+
const generateImport = (opt) => {
|
|
128
|
+
const imports = [opt.default, opt.imports?.length ? `{${opt.imports.join(', ')}}` : ''];
|
|
129
|
+
return `import ${imports.filter(Boolean).join(', ')} from ${generateString(opt.src)};`;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Indents each line of the given string
|
|
134
|
+
* @param s String to indent
|
|
135
|
+
* @param level Indentation level
|
|
136
|
+
*/
|
|
137
|
+
const indent = (s, level = 1) => {
|
|
138
|
+
return indentString(s, 4 * level);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const generateStatements = (...statements) => statements
|
|
142
|
+
.map(v => v.trim())
|
|
143
|
+
.filter(v => v.length)
|
|
144
|
+
.join('\n\n');
|
|
145
|
+
const generateBlockStatements = (...statements) => `{\n${indent(generateStatements(...statements))}\n}`;
|
|
146
|
+
|
|
147
|
+
var root = "export type RequestPayloadMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH';\n\nexport interface RequestPayload {\n method?: RequestPayloadMethod;\n query?: Record<string, any>;\n body?: any;\n unwrap?: boolean;\n forceBlob?: boolean;\n}\n\nexport interface ServiceConfig {\n\n // Your API-Key, this is optional in the sense of if you omit this, and you're in a browser, the\n // cookie-authentication (include-credentials) will be used.\n key?: string;\n\n // Your domain, if omitted location.host will be used.\n host?: string;\n\n // If you want to use https, defaults to true.\n secure?: boolean;\n\n // Optional request/response interceptors.\n interceptors?: {\n\n // Takes the generated request, you can either return a new request,\n // a response (which will be taken as \"the\" response) or nothing.\n // The payload contains the raw input generated by the SDK.\n request?: (request: Request, payload: RequestPayload) => Request | Response | void | Promise<Request | Response | void>;\n\n // Takes the response. This can either be the one from the server or an\n // artificially-crafted one by the request interceptor.\n response?: (response: Response) => Response | void | Promise<Response | void>;\n };\n}\n\nlet globalConfig: ServiceConfig | undefined;\nexport const getGlobalConfig = (): ServiceConfig | undefined => globalConfig;\nexport const setGlobalConfig = (cfg?: ServiceConfig) => globalConfig = cfg;\n\nexport const raw = async (\n cfg: ServiceConfig | undefined = globalConfig,\n endpoint: string,\n payload: RequestPayload = {}\n): Promise<any> => {\n if (!cfg) {\n throw new Error(`ServiceConfig missing.`);\n }\n\n cfg = {\n ...globalConfig, ...cfg,\n interceptors: {...globalConfig?.interceptors, ...cfg?.interceptors}\n };\n\n const isBinaryData = payload.body instanceof resolveBinaryObject();\n const params = new URLSearchParams(Object.entries(payload.query ?? {}).filter(v => v[1] !== undefined));\n const protocol = (cfg.secure ?? true) ? 'https' : 'http';\n\n const interceptRequest = cfg.interceptors?.request ?? (v => v);\n const interceptResponse = cfg.interceptors?.response ?? (v => v);\n\n let host = cfg.host?.replace(/^https?:\\/\\//, '');\n if (!host && typeof location !== 'undefined') {\n host = location.host;\n }\n\n if (!host) {\n throw new Error('Please specify a domain');\n }\n\n const request = new Request(`${protocol}://${host}/webapp/api/v1${endpoint}?${params}`, {\n ...(payload.body && {\n body: isBinaryData\n ? payload.body\n : JSON.stringify(payload.body, (key, value) => value === undefined ? null : value)\n }),\n ...(!cfg.key && {credentials: 'same-origin'}),\n method: payload.method ?? 'get',\n headers: {\n 'Accept': 'application/json',\n ...(cfg.key && {'AuthenticationToken': cfg.key}),\n ...(!isBinaryData && {'Content-Type': 'application/json'})\n }\n });\n\n let res = await interceptRequest(request, payload) ?? request;\n if (!(res instanceof Response)) {\n res = await fetch(res);\n }\n\n res = await interceptResponse(res) ?? res;\n const data = (!payload.forceBlob || !res.ok) && res.headers?.get('content-type')?.includes('application/json') ?\n await res.json() : await res.blob();\n\n // Check if response was successful\n if (!res.ok) {\n return Promise.reject(data);\n }\n\n return payload.unwrap ? data.result : data;\n};\n\nconst _count = (\n cfg: ServiceConfig | undefined,\n endpoint: string,\n query?: CountQuery<any> & {params?: Record<any, any>}\n) => wrapResponse(() => raw(cfg, endpoint, {\n unwrap: true,\n query: {\n ...flattenFilter(query?.filter),\n ...flattenOrFilter(query?.or),\n ...query?.params\n }\n}));\n\nconst _some = (\n cfg: ServiceConfig | undefined,\n endpoint: string,\n query?: SomeQuery<any, any, any, any> & {params?: Record<any, any>}\n) => wrapResponse(() => raw(cfg, endpoint, {\n query: {\n serializeNulls: query?.serializeNulls,\n properties: query?.select ? flattenSelect(query.select).join(',') : undefined,\n includeReferencedEntities: query?.include ? Object.keys(query.include).join(',') : undefined,\n ...flattenOrFilter(query?.or),\n ...flattenFilter(query?.filter),\n ...flattenSort(query?.sort),\n ...query?.params,\n ...query?.pagination\n }\n }).then(data => ({entities: data.result, references: data.referencedEntities ?? {}}))\n);\n\nconst _remove = (\n cfg: ServiceConfig | undefined,\n endpoint: string\n) => wrapResponse(() => raw(cfg, endpoint, {\n method: 'DELETE'\n}).then(() => undefined));\n\nconst _create = (\n cfg: ServiceConfig | undefined,\n endpoint: string,\n data: any\n) => wrapResponse(() => raw(cfg, endpoint, {\n method: 'POST',\n body: data\n}));\n\nconst _update = (\n cfg: ServiceConfig | undefined,\n endpoint: string,\n data: any,\n {ignoreMissingProperties = true}: UpdateQuery = {}\n) => wrapResponse(() => raw(cfg, endpoint, {\n method: 'PUT',\n body: data,\n query: {ignoreMissingProperties}\n}));\n\nconst _unique = (\n cfg: ServiceConfig | undefined,\n endpoint: string,\n query?: UniqueQuery\n) => wrapResponse(() => raw(cfg, endpoint, {query}));\n\nconst _generic = (\n cfg: ServiceConfig | undefined,\n method: RequestPayloadMethod,\n endpoint: string,\n payload?: GenericQuery<any, any>,\n forceBlob?: boolean\n) => wrapResponse(() => raw(cfg, endpoint, {\n method,\n forceBlob,\n body: payload?.body,\n query: payload?.params\n}));\n";
|
|
148
|
+
|
|
149
|
+
var types$1 = "export type DeepPartial<T> = T extends object ? {\n [P in keyof T]?: DeepPartial<T[P]>;\n} : T;\n\n// Filter properties\nexport type EqualityOperators = 'EQ' | 'NE';\nexport type ComparisonOperators = 'LT' | 'GT' | 'LE' | 'GE' | 'LIKE' | 'ILIKE' | 'NOT_LIKE' | 'NOT_ILIKE';\nexport type ArrayOperators = 'IN' | 'NOT_IN';\nexport type Operator = EqualityOperators | ComparisonOperators | ArrayOperators;\n\nexport type MapOperators<T> = { [K in EqualityOperators]?: T | null; } &\n { [K in ComparisonOperators]?: T; } &\n { [K in ArrayOperators]?: T[]; };\n\nexport type QueryFilter<T> = {\n [P in keyof T]?:\n T[P] extends Array<infer U> | undefined ?\n U extends Record<any, any> ? QueryFilter<U> : MapOperators<U> :\n T[P] extends Record<any, any> | undefined ? QueryFilter<T[P]> : MapOperators<T[P]>;\n};\n\nexport type Sort<T> = {\n [K in keyof T]?: {\n [V in keyof T]?:\n V extends K ?\n T[V] extends Array<infer U> | undefined ?\n U extends object ?\n Sort<U> : never :\n T[V] extends object | undefined ?\n Sort<T[V]> : 'asc' | 'desc' : never;\n };\n}[keyof T];\n\n// Select properties\nexport type CustomAttributeFilter = {\n [K in number]: string | number | boolean |\n {id: string;} |\n {entityName: string; entityId: string;};\n}\n\nexport type QuerySelect<T> = {\n [P in keyof T]?:\n T[P] extends Array<infer U> | undefined ? (QuerySelect<U> | boolean) :\n T[P] extends Record<any, any> | undefined ? (QuerySelect<T[P]> | boolean) : boolean;\n}\n\nexport type Select<T, Q extends (QuerySelect<T> | undefined)> = Q extends QuerySelect<T> ? {\n\n // Filter out excluded properties beforehand\n [P in keyof T as Q[P] extends boolean ? P : Q[P] extends object ? P : never]:\n\n // Property\n Q[P] extends true ? T[P] :\n\n // Array\n T[P] extends Array<infer U> ? Select<U, Q[P] & QuerySelect<any>>[] :\n\n // Object\n T[P] extends Record<any, any> ? Select<T[P], Q[P] & QuerySelect<any>> : never\n} : undefined;\n\nexport type MapKeys<T, S extends Record<keyof T, string>> = {\n [K in keyof T as S[K]]: T[K];\n};\n\n// Endpoint configurations\nexport type CountQuery<F> = {\n filter?: QueryFilter<F>;\n or?: (QueryFilter<F> & CustomAttributeFilter)[];\n};\n\nexport type Pagination = {\n page: number;\n pageSize: number;\n};\n\nexport type SomeQuery<\n E, // Entity\n F, // Entity filter\n I extends (QuerySelect<any> | undefined), // Select for referenced entities\n S extends (QuerySelect<any> | undefined) // Select for entity properties\n> = {\n serializeNulls?: boolean;\n include?: I;\n filter?: QueryFilter<F> & CustomAttributeFilter;\n select?: S;\n or?: (QueryFilter<F> & CustomAttributeFilter)[];\n sort?: Sort<E>[];\n pagination?: Pagination;\n};\n\nexport type UniqueQuery = {\n serializeNulls?: boolean;\n}\n\nexport type SomeQueryReturn<\n E, // Entity\n R, // Map of referenced-entity names to the type\n M, // Map of referenced-entity-id names to their entity name\n I extends (QuerySelect<any> | undefined), // Select for referenced entities\n S extends (QuerySelect<any> | undefined) // Select for entity properties\n> = {\n entities: (S extends QuerySelect<E> ? Select<E, S> : E)[];\n references: I extends QuerySelect<R> ? Partial<MapKeys<Select<R, I>, M & Record<any, any>>> : {};\n};\n\nexport type GenericQuery<P, B> = {\n params?: P;\n body?: B;\n};\n\nexport type UpdateQuery = {\n ignoreMissingProperties?: boolean;\n}\n\n// Entity meta types\nexport type WEntityPropertyMeta = (\n { type: 'string'; format?: 'decimal' | 'html'; entity?: WEntity; service?: WService; } |\n { type: 'integer'; format: 'int32' | 'int64' | 'duration' | 'date' | 'timestamp'; } |\n { type: 'array'; format: 'reference'; entity: WEntity; service?: WService; } |\n { type: 'array'; format: 'reference'; enum: WEnum; } |\n { type: 'array'; format: 'string'; } |\n { type: 'number'; format: 'double'; } |\n { type: 'reference'; entity: WEntity; } |\n { type: 'reference'; enum: WEnum; } |\n { type: 'boolean'; } |\n { type: 'object'; }\n);\n\n// Utils\nconst equality: string[] = ['EQ', 'NE'];\nconst simple: string[] = [...equality, 'LT', 'GT', 'LE', 'GE', 'LIKE', 'NOT_LIKE', 'ILIKE', 'NOT_ILIKE'];\nconst array: string[] = ['IN', 'NOT_IN'];\nconst filterMap: Record<Operator, string> = {\n EQ: 'eq',\n NE: 'ne',\n LT: 'lt',\n GT: 'gt',\n LE: 'le',\n GE: 'ge',\n LIKE: 'like',\n NOT_LIKE: 'notlike',\n ILIKE: 'ilike',\n NOT_ILIKE: 'notilike',\n IN: 'in',\n NOT_IN: 'notin'\n};\n\nconst flattenCustomAttributes = (obj: CustomAttributeFilter = {}): [string, string][] => {\n const entries: [string, string][] = [];\n\n for (const [id, filter] of Object.entries(obj)) {\n const key = `customAttribute${id}`;\n\n if (typeof filter === 'object') {\n for (const [prop, value] of Object.entries(filter)) {\n entries.push([`${key}.${prop}-eq`, String(value)]);\n }\n } else if (filter !== undefined) {\n entries.push([`${key}-eq`, String(filter)]);\n }\n }\n\n return entries;\n};\n\nconst flatten = (obj: QueryFilter<any> = {}): [string, string][] => {\n const entries: [string, string][] = [];\n\n for (const [prop, propValue] of Object.entries(obj)) {\n for (const [filter, value] of Object.entries(propValue as object)) {\n if (value === undefined) continue;\n\n if (simple.includes(filter)) {\n if (value === null && equality.includes(filter)) {\n entries.push([`${prop}-${filter === 'EQ' ? 'null' : 'notnull'}`, '']);\n } else {\n entries.push([`${prop}-${filterMap[filter as Operator]}`, value]);\n }\n } else if (array.includes(filter)) {\n entries.push([\n `${prop}-${filterMap[filter as Operator]}`,\n `[${value.map((v: string | number) => typeof v === 'string' ? `\"${v}\"` : v)}]`\n ]);\n } else {\n entries.push(\n ...flatten(propValue as QueryFilter<any>)\n .map(v => [`${prop}.${v[0]}`, v[1]]) as [string, string][]\n );\n break;\n }\n }\n }\n\n return entries;\n};\n\nconst flattenFilter = (obj: QueryFilter<any> = {}): Record<string, string> => {\n const filter: [string, any][] = [], customAttributes: [string, any][] = [];\n\n Object.entries(obj).forEach(value => {\n (value[0].match(/^\\d+$/) ? customAttributes : filter).push(value);\n });\n\n return Object.fromEntries([\n ...flatten(Object.fromEntries(filter)),\n ...flattenCustomAttributes(Object.fromEntries(customAttributes) as CustomAttributeFilter)\n ]);\n};\n\nconst flattenOrFilter = (obj: QueryFilter<any>[] = []): Record<string, string> => {\n const entries: [string, any][] = [];\n\n for (let i = 0; i < obj.length; i++) {\n entries.push(\n ...flatten(obj[i])\n .map(v => [`or${i || ''}-${v[0]}`, v[1]]) as [string, string][]\n );\n }\n\n return Object.fromEntries(entries);\n};\n\nconst flattenSelect = (obj: Select<any, any> = {}): string[] => {\n const entries: string[] = [];\n\n for (const [prop, value] of Object.entries(obj)) {\n if (typeof value === 'object' && value) {\n entries.push(...flattenSelect(value).map(v => `${prop}.${v}`));\n } else if (value) {\n entries.push(prop);\n }\n }\n\n return entries;\n};\n\nexport const flattenSort = (obj: Sort<any>[] = []): {sort?: string} => {\n const flatten = (obj: Sort<any>, base = ''): string | undefined => {\n const [key, value] = Object.entries(obj ?? {})[0] ?? [];\n\n if (key && value) {\n const path = base + key;\n\n if (typeof value === 'object') {\n return flatten(value, path ? `${path}.` : '');\n } else if (['asc', 'desc'].includes(value)) {\n return `${value === 'desc' ? '-' : ''}${path}`;\n }\n }\n\n return undefined;\n };\n\n const sorts = obj.map(v => flatten(v)).filter(Boolean);\n return sorts.length ? {sort: sorts.join(',')} : {};\n};\n";
|
|
150
|
+
|
|
151
|
+
const resolveImports = (target) => {
|
|
152
|
+
const imports = [];
|
|
153
|
+
if (isRXTarget(target)) {
|
|
154
|
+
imports.push(generateImport({ src: 'rxjs', imports: ['defer', 'Observable'] }));
|
|
155
|
+
}
|
|
156
|
+
return imports.join('\n');
|
|
157
|
+
};
|
|
158
|
+
const resolveMappings = (target) => `const wrapResponse = ${isRXTarget(target) ? 'defer' : '(v: (...args: any[]) => any) => v()'};`;
|
|
159
|
+
const resolveBinaryClass = (target) => `const resolveBinaryObject = () => ${resolveBinaryType(target)};`;
|
|
160
|
+
const generateBase = (target) => {
|
|
161
|
+
return generateStatements(resolveImports(target), resolveMappings(target), resolveBinaryClass(target), types$1, root);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const transformKey = (s) => snakeCase(s).toUpperCase();
|
|
165
|
+
const generateEnum = (name, values) => {
|
|
166
|
+
const props = indent(values.map(v => `${transformKey(v)} = ${generateString(v)}`).join(',\n'));
|
|
167
|
+
return `export enum ${name} {\n${props}\n}`;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// We can't use the pascalCase utility as it converts "cDBReminderType" to "CDbReminderType" which is incorrect.
|
|
171
|
+
const loosePascalCase = (str) => str[0].toUpperCase() + str.slice(1);
|
|
172
|
+
|
|
173
|
+
const isObject = (v) => {
|
|
174
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
175
|
+
};
|
|
176
|
+
const isParameterObject = (v) => {
|
|
177
|
+
return isObject(v) && typeof v.name === 'string' && typeof v.in === 'string';
|
|
178
|
+
};
|
|
179
|
+
const isReferenceObject = (v) => {
|
|
180
|
+
return isObject(v) && typeof v.$ref === 'string';
|
|
181
|
+
};
|
|
182
|
+
const isObjectSchemaObject = (v) => {
|
|
183
|
+
return isObject(v) && v.type === 'object' && isObject(v.properties);
|
|
184
|
+
};
|
|
185
|
+
const isEnumSchemaObject = (v) => {
|
|
186
|
+
return isObject(v) && v.type === 'string' && Array.isArray(v.enum);
|
|
187
|
+
};
|
|
188
|
+
const isArraySchemaObject = (v) => {
|
|
189
|
+
return isObject(v) && v.type === 'array' && typeof v.items === 'object';
|
|
190
|
+
};
|
|
191
|
+
const isResponseObject = (v) => {
|
|
192
|
+
return isObject(v) && typeof v.description === 'string';
|
|
193
|
+
};
|
|
194
|
+
const isNonArraySchemaObject = (v) => {
|
|
195
|
+
return isObject(v) && ['string', 'undefined'].includes(typeof v.type);
|
|
196
|
+
};
|
|
197
|
+
const isRelatedEntitySchema = (v) => {
|
|
198
|
+
return isObject(v) && isNonArraySchemaObject(v) && 'x-weclapp' in v && isObject(v['x-weclapp']);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const generateEnums = (schemas) => {
|
|
202
|
+
const enums = new Map();
|
|
203
|
+
for (const [propName, schema] of schemas) {
|
|
204
|
+
if (isEnumSchemaObject(schema)) {
|
|
205
|
+
const name = loosePascalCase(propName);
|
|
206
|
+
if (!enums.has(name)) {
|
|
207
|
+
enums.set(name, {
|
|
208
|
+
properties: schema.enum,
|
|
209
|
+
source: generateEnum(name, schema.enum)
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return enums;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const concat = (strings, separator = ', ', maxLength = 80) => {
|
|
218
|
+
const joined = strings.join(separator);
|
|
219
|
+
if (joined.length > maxLength) {
|
|
220
|
+
const length = strings.length - 1;
|
|
221
|
+
return `\n${indent(strings
|
|
222
|
+
.map((value, index) => index === length ? value : `${(value + separator).trim()}\n`)
|
|
223
|
+
.join(''))}\n`;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
return joined;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/* eslint-disable no-use-before-define */
|
|
231
|
+
const createReferenceType = (value) => ({
|
|
232
|
+
type: 'reference',
|
|
233
|
+
toString: () => loosePascalCase(value)
|
|
234
|
+
});
|
|
235
|
+
const createRawType = (value) => ({
|
|
236
|
+
type: 'raw',
|
|
237
|
+
toString: () => value
|
|
238
|
+
});
|
|
239
|
+
const createArrayType = (value) => ({
|
|
240
|
+
type: 'array',
|
|
241
|
+
toString: () => `${value.toString()}[]`
|
|
242
|
+
});
|
|
243
|
+
const createTupleType = (value) => ({
|
|
244
|
+
type: 'tuple',
|
|
245
|
+
toString: () => concat([...new Set(value.map(v => typeof v === 'string' ? `'${v}'` : v.toString()))], ' | ')
|
|
246
|
+
});
|
|
247
|
+
const createObjectType = (value, required = []) => ({
|
|
248
|
+
type: 'object',
|
|
249
|
+
isFullyOptional: () => {
|
|
250
|
+
return !required.length && Object.values(value)
|
|
251
|
+
.filter(v => v?.type === 'object')
|
|
252
|
+
.every(v => v.isFullyOptional());
|
|
253
|
+
},
|
|
254
|
+
toString: (propagateOptionalProperties = false) => {
|
|
255
|
+
const properties = Object.entries(value)
|
|
256
|
+
.filter(v => v[1])
|
|
257
|
+
.map(v => {
|
|
258
|
+
const name = v[0];
|
|
259
|
+
const value = v[1];
|
|
260
|
+
const isRequired = required.includes(name) || (value.type === 'object' && !value.isFullyOptional() && propagateOptionalProperties);
|
|
261
|
+
return `${name + (isRequired ? '' : '?')}: ${value.toString()};`;
|
|
262
|
+
});
|
|
263
|
+
return properties.length ? `{\n${indent(properties.join('\n'))}\n}` : '{}';
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
const getRefName = (obj) => {
|
|
267
|
+
return obj.$ref.replace(/.*\//, '');
|
|
268
|
+
};
|
|
269
|
+
const convertToTypeScriptType = (schema, property) => {
|
|
270
|
+
if (isReferenceObject(schema)) {
|
|
271
|
+
return createReferenceType(getRefName(schema));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
switch (schema.type) {
|
|
275
|
+
case 'integer':
|
|
276
|
+
case 'number':
|
|
277
|
+
return createRawType('number');
|
|
278
|
+
case 'string':
|
|
279
|
+
if (schema.enum) {
|
|
280
|
+
return property ? createReferenceType(property) : createTupleType(schema.enum);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
return schema.format === 'binary' ? createRawType('binary') : createRawType('string');
|
|
284
|
+
}
|
|
285
|
+
case 'boolean':
|
|
286
|
+
return createRawType('boolean');
|
|
287
|
+
case 'object': {
|
|
288
|
+
const { properties = {}, required = [] } = schema;
|
|
289
|
+
return createObjectType(Object.fromEntries(Object.entries(properties)
|
|
290
|
+
.map(v => [v[0], convertToTypeScriptType(v[1])])), required);
|
|
291
|
+
}
|
|
292
|
+
case 'array':
|
|
293
|
+
return createArrayType(convertToTypeScriptType(schema.items, property));
|
|
294
|
+
default:
|
|
295
|
+
return createRawType('unknown');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const setEntityEnumProperty = (enums, prop, meta) => {
|
|
301
|
+
const referenceName = getRefName(prop);
|
|
302
|
+
const enumName = loosePascalCase(referenceName);
|
|
303
|
+
if (enums.has(enumName)) {
|
|
304
|
+
meta.enum = enumName;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
meta.entity = referenceName;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const extractPropertyMetaData = (enums, meta, prop) => {
|
|
311
|
+
const result = { service: meta.service, entity: meta.entity };
|
|
312
|
+
if (isReferenceObject(prop)) {
|
|
313
|
+
setEntityEnumProperty(enums, prop, result);
|
|
314
|
+
result.type = 'reference';
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
result.format = prop.format;
|
|
318
|
+
result.type = prop.type;
|
|
319
|
+
if (isArraySchemaObject(prop)) {
|
|
320
|
+
if (isReferenceObject(prop.items)) {
|
|
321
|
+
setEntityEnumProperty(enums, prop.items, result);
|
|
322
|
+
result.format = 'reference';
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
result.format = 'string';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const generateInlineComment = (comment) => `/** ${comment} */`;
|
|
332
|
+
const generateBlockComment = (comment, body) => `/**\n${comment.trim().replace(/^ */gm, ' * ')}\n */${body ? `\n${body}` : ''}`;
|
|
333
|
+
|
|
334
|
+
const generateType = (name, value) => {
|
|
335
|
+
return `export type ${name} = ${value.trim()};`;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const arrayify = (v) => Array.isArray(v) ? v : [v];
|
|
339
|
+
|
|
340
|
+
const generateInterfaceProperties = (entries) => {
|
|
341
|
+
const properties = entries
|
|
342
|
+
.filter(v => v.type !== undefined)
|
|
343
|
+
.filter((value, index, array) => array.findIndex(v => v.name === value.name) === index)
|
|
344
|
+
.map(({ name, type, required, readonly, comment }) => {
|
|
345
|
+
const cmd = comment ? `${generateInlineComment(comment)}\n` : '';
|
|
346
|
+
const req = required ? '' : '?';
|
|
347
|
+
const rol = readonly ? 'readonly ' : '';
|
|
348
|
+
return `${cmd + rol + name + req}: ${type};`;
|
|
349
|
+
})
|
|
350
|
+
.join('\n');
|
|
351
|
+
return properties.length ? `{\n${indent(properties)}\n}` : `{}`;
|
|
352
|
+
};
|
|
353
|
+
const generateInterfaceFromObject = (name, obj, propagateOptionalProperties) => `export interface ${name} ${obj.toString(propagateOptionalProperties)}`;
|
|
354
|
+
const generateInterface = (name, entries, extend) => {
|
|
355
|
+
const signature = `${name} ${extend ? `extends ${arrayify(extend).join(', ')}` : ''}`.trim();
|
|
356
|
+
const body = generateInterfaceProperties(entries);
|
|
357
|
+
return `export interface ${signature} ${body}`;
|
|
358
|
+
};
|
|
359
|
+
const generateInterfaceType = (name, entries, extend) => {
|
|
360
|
+
const body = generateInterfaceProperties(entries);
|
|
361
|
+
const bases = extend ? arrayify(extend).join(' & ') : undefined;
|
|
362
|
+
return generateType(name, `${bases ? `${bases} & ` : ''}${body}`);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const generateEntities = (schemas, enums) => {
|
|
366
|
+
const entities = new Map();
|
|
367
|
+
for (const [schemaName, schema] of schemas) {
|
|
368
|
+
// Enums are generated separately
|
|
369
|
+
if (isEnumSchemaObject(schema)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const entity = pascalCase(schemaName);
|
|
373
|
+
// Entity and filter
|
|
374
|
+
const entityInterface = [];
|
|
375
|
+
const filterInterface = [];
|
|
376
|
+
// Referenced entities and property-to-referenced-entity mapping
|
|
377
|
+
const referenceInterface = [];
|
|
378
|
+
const referenceMappingsInterface = [];
|
|
379
|
+
const properties = new Map();
|
|
380
|
+
// The parent entity
|
|
381
|
+
let extend = undefined;
|
|
382
|
+
const processProperties = (props = {}) => {
|
|
383
|
+
for (const [name, property] of Object.entries(props)) {
|
|
384
|
+
const meta = isRelatedEntitySchema(property) ? property['x-weclapp'] : {};
|
|
385
|
+
if (meta.entity) {
|
|
386
|
+
const type = `${pascalCase(meta.entity)}[]`;
|
|
387
|
+
referenceInterface.push({ name, type, required: true });
|
|
388
|
+
filterInterface.push({ name: meta.entity, type, required: true });
|
|
389
|
+
}
|
|
390
|
+
if (meta.service) {
|
|
391
|
+
referenceMappingsInterface.push({ name, type: generateString(meta.service), required: true });
|
|
392
|
+
}
|
|
393
|
+
const type = convertToTypeScriptType(property, name).toString();
|
|
394
|
+
const comment = isNonArraySchemaObject(property) ?
|
|
395
|
+
property.deprecated ? '@deprecated will be removed.' :
|
|
396
|
+
property.format ? `format: ${property.format}` :
|
|
397
|
+
undefined : undefined;
|
|
398
|
+
entityInterface.push({
|
|
399
|
+
name, type, comment,
|
|
400
|
+
required: meta.required,
|
|
401
|
+
readonly: !isReferenceObject(property) && property.readOnly
|
|
402
|
+
});
|
|
403
|
+
properties.set(name, extractPropertyMetaData(enums, meta, property));
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
if (schema.allOf?.length) {
|
|
407
|
+
for (const item of schema.allOf) {
|
|
408
|
+
if (isReferenceObject(item)) {
|
|
409
|
+
extend = convertToTypeScriptType(item).toString();
|
|
410
|
+
}
|
|
411
|
+
else if (isObjectSchemaObject(item)) {
|
|
412
|
+
processProperties(item.properties);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
processProperties(schema.properties);
|
|
417
|
+
const source = generateStatements(generateInterface(entity, entityInterface, extend), generateInterface(`${entity}_References`, referenceInterface, extend ? [`${extend}_References`] : undefined), generateInterface(`${entity}_Mappings`, referenceMappingsInterface, extend ? [`${extend}_Mappings`] : undefined), generateInterfaceType(`${entity}_Filter`, filterInterface, extend ? [entity, `${extend}_Filter`] : undefined));
|
|
418
|
+
entities.set(schemaName, {
|
|
419
|
+
extends: extend ? camelCase(extend) : extend,
|
|
420
|
+
properties,
|
|
421
|
+
source
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return entities;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Pluralizes a word, most of the time correct.
|
|
429
|
+
* @param s String to pluralize.
|
|
430
|
+
*/
|
|
431
|
+
const pluralize = (s) => {
|
|
432
|
+
return s.endsWith('s') ? s :
|
|
433
|
+
s.endsWith('y') ? `${s.slice(0, -1)}ies` :
|
|
434
|
+
`${s}s`;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/* eslint-disable no-console */
|
|
438
|
+
const logger = new class {
|
|
439
|
+
active = true;
|
|
440
|
+
warnings = 0;
|
|
441
|
+
errors = 0;
|
|
442
|
+
write(str = '') {
|
|
443
|
+
process.stdout.write(str);
|
|
444
|
+
}
|
|
445
|
+
blankLn(str = '') {
|
|
446
|
+
this.blank(`${str}\n`);
|
|
447
|
+
}
|
|
448
|
+
warnLn(str) {
|
|
449
|
+
this.warn(`${str}\n`);
|
|
450
|
+
}
|
|
451
|
+
errorLn(str) {
|
|
452
|
+
this.error(`${str}\n`);
|
|
453
|
+
}
|
|
454
|
+
successLn(str) {
|
|
455
|
+
this.success(`${str}\n`);
|
|
456
|
+
}
|
|
457
|
+
infoLn(str) {
|
|
458
|
+
this.info(`${str}\n`);
|
|
459
|
+
}
|
|
460
|
+
debugLn(str) {
|
|
461
|
+
this.debug(`${str}\n`);
|
|
462
|
+
}
|
|
463
|
+
blank(str) {
|
|
464
|
+
this.write(str);
|
|
465
|
+
}
|
|
466
|
+
warn(str) {
|
|
467
|
+
this.write(`${chalk.yellowBright('[!]')} ${str}`);
|
|
468
|
+
this.warnings++;
|
|
469
|
+
}
|
|
470
|
+
error(str) {
|
|
471
|
+
this.write(`${chalk.redBright('[X]')} ${str}`);
|
|
472
|
+
this.errors++;
|
|
473
|
+
}
|
|
474
|
+
success(str) {
|
|
475
|
+
this.write(`${chalk.greenBright('[✓]')} ${str}`);
|
|
476
|
+
}
|
|
477
|
+
info(str) {
|
|
478
|
+
this.write(`${chalk.blueBright('[i]')} ${str}`);
|
|
479
|
+
}
|
|
480
|
+
debug(str) {
|
|
481
|
+
this.write(`[-] ${str}`);
|
|
482
|
+
}
|
|
483
|
+
printSummary() {
|
|
484
|
+
const format = (v, name, fail, ok) => {
|
|
485
|
+
const color = v ? fail : ok;
|
|
486
|
+
return v === 0 ? `${color('zero')} ${pluralize(name)}` :
|
|
487
|
+
v === 1 ? `${color('one')} ${name}` : `${color(v)} ${pluralize(name)}`;
|
|
488
|
+
};
|
|
489
|
+
const warnings = format(this.warnings, 'warning', chalk.yellowBright, chalk.greenBright);
|
|
490
|
+
const errors = format(this.errors, 'error', chalk.redBright, chalk.greenBright);
|
|
491
|
+
const info = `Finished with ${warnings} and ${errors}.`;
|
|
492
|
+
this[this.errors ? 'errorLn' : this.warnings ? 'warnLn' : 'successLn'](info);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* ROOT => /article
|
|
498
|
+
* COUNT => /article/count
|
|
499
|
+
* ENTITY => /article/{id}
|
|
500
|
+
* SPECIAL_ROOT => /article/generateImage
|
|
501
|
+
* SPECIAL_ENTITY => /article/id/{id}/generateImag
|
|
502
|
+
*/
|
|
503
|
+
var WeclappEndpointType;
|
|
504
|
+
(function (WeclappEndpointType) {
|
|
505
|
+
WeclappEndpointType["ROOT"] = "ROOT";
|
|
506
|
+
WeclappEndpointType["COUNT"] = "COUNT";
|
|
507
|
+
WeclappEndpointType["ENTITY"] = "ENTITY";
|
|
508
|
+
WeclappEndpointType["GENERIC_ROOT"] = "GENERIC_ROOT";
|
|
509
|
+
WeclappEndpointType["GENERIC_ENTITY"] = "GENERIC_ENTITY";
|
|
510
|
+
})(WeclappEndpointType || (WeclappEndpointType = {}));
|
|
511
|
+
const parseEndpointPath = (path) => {
|
|
512
|
+
const [, entity, ...rest] = path.split('/');
|
|
513
|
+
if (!entity) {
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
if (!rest.length) {
|
|
517
|
+
return { path, entity, type: WeclappEndpointType.ROOT };
|
|
518
|
+
}
|
|
519
|
+
else if (rest[0] === 'count') {
|
|
520
|
+
return { path, entity, type: WeclappEndpointType.COUNT };
|
|
521
|
+
}
|
|
522
|
+
else if (rest[0] === 'id') {
|
|
523
|
+
return rest.length === 2 ?
|
|
524
|
+
{ path, entity, type: WeclappEndpointType.ENTITY } :
|
|
525
|
+
{ path, entity, method: rest[2], type: WeclappEndpointType.GENERIC_ENTITY };
|
|
526
|
+
}
|
|
527
|
+
else if (rest.length === 1) {
|
|
528
|
+
return { path, entity, method: rest[1], type: WeclappEndpointType.GENERIC_ROOT };
|
|
529
|
+
}
|
|
530
|
+
return undefined;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const generateArrowFunction = ({ name, signature, returns, params }) => {
|
|
534
|
+
return `const ${name}: ${signature} = (${params?.join(', ') ?? ''}) =>\n${indent(returns)};`;
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const generateArrowFunctionType = ({ type, returns = 'void', generics, params }) => {
|
|
538
|
+
const genericsString = generics?.length ? `<\n${indent(generics.join(',\n'))}\n>` : '';
|
|
539
|
+
const paramsString = params?.length ? `(${params.join(', ')})` : `()`;
|
|
540
|
+
return generateType(type, `${genericsString + paramsString} =>\n${indent(returns)}`);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const convertParametersToSchema = (parameters = []) => {
|
|
544
|
+
const properties = [];
|
|
545
|
+
const required = [];
|
|
546
|
+
for (const param of parameters) {
|
|
547
|
+
if (isParameterObject(param) && param.in === 'query') {
|
|
548
|
+
if (param.schema) {
|
|
549
|
+
properties.push([param.name, param.schema]);
|
|
550
|
+
param.required && required.push(param.name);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
type: 'object', required,
|
|
556
|
+
properties: Object.fromEntries(properties)
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const functionName$5 = 'count';
|
|
561
|
+
const generateCountEndpoint = ({ aliases, path, target, endpoint }) => {
|
|
562
|
+
const service = pascalCase(endpoint.entity);
|
|
563
|
+
const entity = aliases.get(endpoint.entity) ?? service;
|
|
564
|
+
const entityFilter = `${entity}_Filter`;
|
|
565
|
+
const interfaceName = `${service}Service_${pascalCase(functionName$5)}`;
|
|
566
|
+
const entityParameters = `${interfaceName}_Parameters`;
|
|
567
|
+
const parameterSchema = convertParametersToSchema(path.parameters);
|
|
568
|
+
const parameters = createObjectType({
|
|
569
|
+
params: convertToTypeScriptType(parameterSchema)
|
|
570
|
+
});
|
|
571
|
+
const functionSource = generateArrowFunction({
|
|
572
|
+
name: functionName$5,
|
|
573
|
+
signature: interfaceName,
|
|
574
|
+
returns: `_${functionName$5}(cfg, ${generateString(endpoint.path)}, query)`,
|
|
575
|
+
params: ['query']
|
|
576
|
+
});
|
|
577
|
+
const interfaceSource = generateArrowFunctionType({
|
|
578
|
+
type: interfaceName,
|
|
579
|
+
params: [`query${parameters.isFullyOptional() ? '?' : ''}: CountQuery<${entityFilter}> & ${entityParameters}`],
|
|
580
|
+
returns: `${resolveResponseType(target)}<number>`
|
|
581
|
+
});
|
|
582
|
+
return {
|
|
583
|
+
entity,
|
|
584
|
+
name: functionName$5,
|
|
585
|
+
type: { name: interfaceName, source: interfaceSource },
|
|
586
|
+
func: { name: functionName$5, source: functionSource },
|
|
587
|
+
interfaces: [
|
|
588
|
+
{
|
|
589
|
+
name: entityParameters,
|
|
590
|
+
source: generateInterfaceFromObject(entityParameters, parameters, true)
|
|
591
|
+
}
|
|
592
|
+
]
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const generateBodyType = (body) => {
|
|
597
|
+
if (isReferenceObject(body)) {
|
|
598
|
+
return convertToTypeScriptType(body);
|
|
599
|
+
}
|
|
600
|
+
const types = [];
|
|
601
|
+
for (const { schema } of Object.values(body?.content ?? {})) {
|
|
602
|
+
if (schema) {
|
|
603
|
+
types.push(convertToTypeScriptType(schema));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return types.length ? createTupleType(types) : undefined;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const generateRequestBodyType = ({ requestBody }) => {
|
|
610
|
+
return generateBodyType(requestBody) ?? createRawType('unknown');
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const generateResponseBodyType = ({ responses }) => {
|
|
614
|
+
return generateBodyType(Object.entries(responses)
|
|
615
|
+
.filter(v => v[0].startsWith('2'))[0]?.[1]) ?? createRawType('void');
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const functionName$4 = 'create';
|
|
619
|
+
const generateCreateEndpoint = ({ target, path, endpoint }) => {
|
|
620
|
+
const entity = pascalCase(endpoint.entity);
|
|
621
|
+
const interfaceName = `${entity}Service_${pascalCase(functionName$4)}`;
|
|
622
|
+
const functionSource = generateArrowFunction({
|
|
623
|
+
name: functionName$4,
|
|
624
|
+
signature: interfaceName,
|
|
625
|
+
returns: `_${functionName$4}(cfg, ${generateString(endpoint.path)}, data)`,
|
|
626
|
+
params: ['data']
|
|
627
|
+
});
|
|
628
|
+
const interfaceSource = generateArrowFunctionType({
|
|
629
|
+
type: interfaceName,
|
|
630
|
+
params: [`data: DeepPartial<${generateRequestBodyType(path).toString()}>`],
|
|
631
|
+
returns: `${resolveResponseType(target)}<${generateResponseBodyType(path).toString()}>`
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
entity,
|
|
635
|
+
name: functionName$4,
|
|
636
|
+
type: { name: interfaceName, source: interfaceSource },
|
|
637
|
+
func: { name: functionName$4, source: functionSource }
|
|
638
|
+
};
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const generateGenericFunctionName = (path, suffix = '', prefix = '') => {
|
|
642
|
+
return camelCase(`${prefix}_` +
|
|
643
|
+
path
|
|
644
|
+
.replace(/.*\//, '')
|
|
645
|
+
.replace(/\W+/, '_')
|
|
646
|
+
.replace(/[_]+/, '_') + `_${suffix}`);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const insertPathPlaceholder = (path, record) => {
|
|
650
|
+
return path.replace(/{(\w+)}/g, (_, name) => record[name]);
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const wrapBody = (type, target) => {
|
|
654
|
+
return type.toString() === 'binary' ?
|
|
655
|
+
createRawType(isNodeTarget(target) ? 'BodyInit' : 'Blob') :
|
|
656
|
+
type; // node-fetch returns a Blob as well
|
|
657
|
+
};
|
|
658
|
+
const generateGenericEndpoint = (suffix) => ({ target, method, path, endpoint }) => {
|
|
659
|
+
const functionName = generateGenericFunctionName(endpoint.path, suffix, method);
|
|
660
|
+
const entity = pascalCase(endpoint.entity);
|
|
661
|
+
const interfaceName = `${entity}Service_${pascalCase(functionName)}`;
|
|
662
|
+
const entityQuery = `${interfaceName}_Query`;
|
|
663
|
+
const hasId = endpoint.path.includes('{id}');
|
|
664
|
+
const params = createObjectType({
|
|
665
|
+
params: convertToTypeScriptType(convertParametersToSchema(path.parameters)),
|
|
666
|
+
body: method === 'get' ? undefined : wrapBody(generateRequestBodyType(path), target)
|
|
667
|
+
});
|
|
668
|
+
const responseBody = generateResponseBodyType(path);
|
|
669
|
+
const forceBlobResponse = String(responseBody.toString() === 'binary');
|
|
670
|
+
const functionSource = generateArrowFunction({
|
|
671
|
+
name: functionName,
|
|
672
|
+
signature: interfaceName,
|
|
673
|
+
params: hasId ? ['id', 'query'] : ['query'],
|
|
674
|
+
returns: `_generic(cfg, ${generateString(method.toUpperCase())}, \`${insertPathPlaceholder(endpoint.path, { id: '${id}' })}\`, query, ${forceBlobResponse})`
|
|
675
|
+
});
|
|
676
|
+
const interfaceSource = generateArrowFunctionType({
|
|
677
|
+
type: interfaceName,
|
|
678
|
+
params: [...(hasId ? ['id: string'] : []), `query${params.isFullyOptional() ? '?' : ''}: ${entityQuery}`],
|
|
679
|
+
returns: `${resolveResponseType(target)}<${wrapBody(responseBody, target).toString()}>`
|
|
680
|
+
});
|
|
681
|
+
return {
|
|
682
|
+
entity,
|
|
683
|
+
name: functionName,
|
|
684
|
+
type: { name: interfaceName, source: interfaceSource },
|
|
685
|
+
func: { name: functionName, source: functionSource },
|
|
686
|
+
interfaces: [
|
|
687
|
+
{
|
|
688
|
+
name: entityQuery,
|
|
689
|
+
source: generateInterfaceFromObject(entityQuery, params, true)
|
|
690
|
+
}
|
|
691
|
+
]
|
|
692
|
+
};
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
const functionName$3 = 'remove';
|
|
696
|
+
const generateRemoveEndpoint = ({ target, endpoint }) => {
|
|
697
|
+
const entity = pascalCase(endpoint.entity);
|
|
698
|
+
const interfaceName = `${entity}Service_${pascalCase(functionName$3)}`;
|
|
699
|
+
const functionSource = generateArrowFunction({
|
|
700
|
+
name: functionName$3,
|
|
701
|
+
signature: interfaceName,
|
|
702
|
+
returns: `_${functionName$3}(cfg, \`${insertPathPlaceholder(endpoint.path, { id: '${id}' })}\`)`,
|
|
703
|
+
params: ['id']
|
|
704
|
+
});
|
|
705
|
+
const interfaceSource = generateArrowFunctionType({
|
|
706
|
+
type: interfaceName,
|
|
707
|
+
params: ['id: string'],
|
|
708
|
+
returns: `${resolveResponseType(target)}<void>`
|
|
709
|
+
});
|
|
710
|
+
return {
|
|
711
|
+
entity,
|
|
712
|
+
name: functionName$3,
|
|
713
|
+
type: { name: interfaceName, source: interfaceSource },
|
|
714
|
+
func: { name: functionName$3, source: functionSource }
|
|
715
|
+
};
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const functionName$2 = 'some';
|
|
719
|
+
const excludedParameters = [
|
|
720
|
+
'page', 'pageSize', 'sort',
|
|
721
|
+
'serializeNulls', 'properties', 'includeReferencedEntities'
|
|
722
|
+
];
|
|
723
|
+
const generateSomeEndpoint = ({ aliases, target, path, endpoint }) => {
|
|
724
|
+
// Required interface names
|
|
725
|
+
const service = pascalCase(endpoint.entity);
|
|
726
|
+
const entity = aliases.get(endpoint.entity) ?? service;
|
|
727
|
+
const interfaceName = `${service}Service_${pascalCase(functionName$2)}`;
|
|
728
|
+
const entityFilter = `${entity}_Filter`;
|
|
729
|
+
const entityMappings = `${entity}_Mappings`;
|
|
730
|
+
const entityReferences = `${entity}_References`;
|
|
731
|
+
const entityParameters = `${service}_Parameters`;
|
|
732
|
+
const parameterSchema = convertParametersToSchema(path.parameters);
|
|
733
|
+
// We already cover page, pageSize and sort
|
|
734
|
+
parameterSchema.properties = Object.fromEntries(Object.entries(parameterSchema.properties ?? {})
|
|
735
|
+
.filter(v => !excludedParameters.includes(v[0])));
|
|
736
|
+
const parameters = createObjectType({
|
|
737
|
+
params: convertToTypeScriptType(parameterSchema)
|
|
738
|
+
});
|
|
739
|
+
const interfaceSource = generateArrowFunctionType({
|
|
740
|
+
type: interfaceName,
|
|
741
|
+
generics: [
|
|
742
|
+
`S extends (QuerySelect<${entity}> | undefined) = undefined`,
|
|
743
|
+
`I extends (QuerySelect<${entityMappings}> | undefined) = undefined`
|
|
744
|
+
],
|
|
745
|
+
params: [`query${parameters.isFullyOptional() ? '?' : ''}: SomeQuery<${entity}, ${entityFilter}, I, S> & ${entityParameters}`],
|
|
746
|
+
returns: `${resolveResponseType(target)}<SomeQueryReturn<${entity}, ${entityReferences}, ${entityMappings}, I, S>>`
|
|
747
|
+
});
|
|
748
|
+
const functionSource = generateArrowFunction({
|
|
749
|
+
name: functionName$2,
|
|
750
|
+
signature: interfaceName,
|
|
751
|
+
returns: `_${functionName$2}(cfg, ${generateString(endpoint.path)}, query)`,
|
|
752
|
+
params: ['query']
|
|
753
|
+
});
|
|
754
|
+
return {
|
|
755
|
+
entity,
|
|
756
|
+
name: functionName$2,
|
|
757
|
+
type: { name: interfaceName, source: interfaceSource },
|
|
758
|
+
func: { name: functionName$2, source: functionSource },
|
|
759
|
+
interfaces: [
|
|
760
|
+
{
|
|
761
|
+
name: entityParameters,
|
|
762
|
+
source: generateInterfaceFromObject(entityParameters, parameters, true)
|
|
763
|
+
}
|
|
764
|
+
]
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const functionName$1 = 'unique';
|
|
769
|
+
const generateUniqueEndpoint = ({ target, path, endpoint }) => {
|
|
770
|
+
const entity = pascalCase(endpoint.entity);
|
|
771
|
+
const interfaceName = `${entity}Service_${pascalCase(functionName$1)}`;
|
|
772
|
+
const functionSource = generateArrowFunction({
|
|
773
|
+
name: functionName$1,
|
|
774
|
+
signature: interfaceName,
|
|
775
|
+
params: ['id', 'query'],
|
|
776
|
+
returns: `_${functionName$1}(cfg, \`${insertPathPlaceholder(endpoint.path, { id: '${id}' })}\`, query)`
|
|
777
|
+
});
|
|
778
|
+
const interfaceSource = generateArrowFunctionType({
|
|
779
|
+
type: interfaceName,
|
|
780
|
+
params: ['id: string', 'query?: Q'],
|
|
781
|
+
generics: ['Q extends UniqueQuery'],
|
|
782
|
+
returns: `${resolveResponseType(target)}<${generateResponseBodyType(path).toString()}>`
|
|
783
|
+
});
|
|
784
|
+
return {
|
|
785
|
+
entity,
|
|
786
|
+
name: functionName$1,
|
|
787
|
+
type: { name: interfaceName, source: interfaceSource },
|
|
788
|
+
func: { name: functionName$1, source: functionSource }
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const functionName = 'update';
|
|
793
|
+
const generateUpdateEndpoint = ({ target, path, endpoint }) => {
|
|
794
|
+
const entity = pascalCase(endpoint.entity);
|
|
795
|
+
const interfaceName = `${entity}Service_${pascalCase(functionName)}`;
|
|
796
|
+
const interfaceSource = generateArrowFunctionType({
|
|
797
|
+
type: interfaceName,
|
|
798
|
+
params: ['id: string', `data: DeepPartial<${generateRequestBodyType(path).toString()}>`, 'options?: UpdateQuery'],
|
|
799
|
+
returns: `${resolveResponseType(target)}<${generateResponseBodyType(path).toString()}>`
|
|
800
|
+
});
|
|
801
|
+
const functionSource = generateArrowFunction({
|
|
802
|
+
name: functionName,
|
|
803
|
+
signature: interfaceName,
|
|
804
|
+
returns: `_${functionName}(cfg, \`${insertPathPlaceholder(endpoint.path, { id: '${id}' })}\`, data, options)`,
|
|
805
|
+
params: ['id', 'data', 'options']
|
|
806
|
+
});
|
|
807
|
+
return {
|
|
808
|
+
entity,
|
|
809
|
+
name: functionName,
|
|
810
|
+
type: { name: interfaceName, source: interfaceSource },
|
|
811
|
+
func: { name: functionName, source: functionSource }
|
|
812
|
+
};
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const groupEndpointsByEntity = (paths) => {
|
|
816
|
+
const endpoints = new Map();
|
|
817
|
+
for (const [rawPath, path] of Object.entries(paths)) {
|
|
818
|
+
const endpoint = parseEndpointPath(rawPath);
|
|
819
|
+
if (!endpoint || !path) {
|
|
820
|
+
logger.errorLn(`Failed to parse ${rawPath}`);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (endpoints.has(endpoint.entity)) {
|
|
824
|
+
endpoints.get(endpoint.entity)?.push({ endpoint, path });
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
endpoints.set(endpoint.entity, [{ endpoint, path }]);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return endpoints;
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const generators = {
|
|
834
|
+
/* /article */
|
|
835
|
+
[WeclappEndpointType.ROOT]: {
|
|
836
|
+
get: generateSomeEndpoint,
|
|
837
|
+
post: generateCreateEndpoint
|
|
838
|
+
},
|
|
839
|
+
/* /article/count */
|
|
840
|
+
[WeclappEndpointType.COUNT]: {
|
|
841
|
+
get: generateCountEndpoint
|
|
842
|
+
},
|
|
843
|
+
/* /article/:id */
|
|
844
|
+
[WeclappEndpointType.ENTITY]: {
|
|
845
|
+
get: generateUniqueEndpoint,
|
|
846
|
+
delete: generateRemoveEndpoint,
|
|
847
|
+
put: generateUpdateEndpoint
|
|
848
|
+
},
|
|
849
|
+
/* /article/:id/method */
|
|
850
|
+
[WeclappEndpointType.GENERIC_ENTITY]: {
|
|
851
|
+
get: generateGenericEndpoint('ById'),
|
|
852
|
+
post: generateGenericEndpoint('ById')
|
|
853
|
+
},
|
|
854
|
+
/* /article/method */
|
|
855
|
+
[WeclappEndpointType.GENERIC_ROOT]: {
|
|
856
|
+
get: generateGenericEndpoint(),
|
|
857
|
+
post: generateGenericEndpoint()
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
const generateServices = (doc, aliases, options) => {
|
|
861
|
+
const services = new Map();
|
|
862
|
+
const grouped = groupEndpointsByEntity(doc.paths);
|
|
863
|
+
for (const [endpoint, paths] of grouped) {
|
|
864
|
+
const serviceName = camelCase(`${endpoint}Service`);
|
|
865
|
+
const serviceTypeName = pascalCase(`${endpoint}Service`);
|
|
866
|
+
// Service functions
|
|
867
|
+
const functions = [];
|
|
868
|
+
for (const { path, endpoint } of paths) {
|
|
869
|
+
const resolver = generators[endpoint.type];
|
|
870
|
+
for (const [method, config] of Object.entries(path)) {
|
|
871
|
+
if (method === 'get' && endpoint.type === WeclappEndpointType.ENTITY && !options.generateUnique) {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (resolver[method]) {
|
|
875
|
+
const path = config;
|
|
876
|
+
const target = options.target;
|
|
877
|
+
if (!path.deprecated || options.deprecated) {
|
|
878
|
+
functions.push({
|
|
879
|
+
...resolver[method]({ endpoint, method, target, path, aliases }),
|
|
880
|
+
path
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
logger.errorLn(`Failed to generate a function for ${method.toUpperCase()}:${endpoint.type} ${endpoint.path}`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (!functions.length) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
// Construct service type
|
|
893
|
+
const types = generateStatements(...functions.flatMap(v => v.interfaces?.map(v => v.source) ?? []), ...functions.map(v => v.type.source), generateInterface(serviceTypeName, [
|
|
894
|
+
...functions.map(v => ({
|
|
895
|
+
required: true,
|
|
896
|
+
comment: v.path.deprecated ? '@deprecated' : undefined,
|
|
897
|
+
name: v.func.name,
|
|
898
|
+
type: v.type.name
|
|
899
|
+
}))
|
|
900
|
+
]));
|
|
901
|
+
// Construct service value
|
|
902
|
+
const funcBody = generateBlockStatements(...functions.map(v => v.func.source), `return {${concat(functions.map(v => v.func.name))}};`);
|
|
903
|
+
const func = `export const ${serviceName} = (cfg?: ServiceConfig): ${serviceTypeName} => ${funcBody};`;
|
|
904
|
+
const source = generateBlockComment(`${pascalCase(endpoint)} service`, generateStatements(types, func));
|
|
905
|
+
const deprecated = functions.every(v => v.path.deprecated);
|
|
906
|
+
services.set(endpoint, { entity: endpoint, deprecated, serviceName, serviceTypeName, source, functions });
|
|
907
|
+
}
|
|
908
|
+
return services;
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const generateCustomValueUtilities = (entities, services) => {
|
|
912
|
+
const customValueEntity = entities.get('customValue');
|
|
913
|
+
const customValueEntities = [];
|
|
914
|
+
if (!customValueEntity) {
|
|
915
|
+
logger.warn('Cannot generate custom value utils, type not found.');
|
|
916
|
+
return '';
|
|
917
|
+
}
|
|
918
|
+
serviceLoop: for (const service of services) {
|
|
919
|
+
const someFunction = service.functions.find(v => v.name === 'some');
|
|
920
|
+
if (!someFunction) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const entity = entities.get(camelCase(someFunction.entity));
|
|
924
|
+
if (entity?.properties.size !== customValueEntity.properties.size) {
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
for (const [prop, { type }] of entity.properties) {
|
|
928
|
+
if (customValueEntity.properties.get(prop)?.type !== type) {
|
|
929
|
+
continue serviceLoop;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
customValueEntities.push(service.entity);
|
|
933
|
+
}
|
|
934
|
+
return generateBlockComment('Utilities to identify services that return an entity that is an alias to CustomValue.', generateStatements(generateType('WCustomValueService', concat(generateStrings(customValueEntities), ' | ')), `export const wCustomValueServiceNames: WCustomValueService[] = [${concat(generateStrings(customValueEntities))}];`, `export const isWCustomValueService = (service: string | undefined): service is WCustomValueService =>\n${indent('wCustomValueServiceNames.includes(service as WCustomValueService);')}`));
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
const generateObject = (properties) => {
|
|
938
|
+
const body = [];
|
|
939
|
+
for (const { key, value } of properties) {
|
|
940
|
+
if (value === undefined) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (Array.isArray(value)) {
|
|
944
|
+
const str = generateObject(value);
|
|
945
|
+
if (str.length > 2) {
|
|
946
|
+
body.push(`${key}: ${str}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
body.push(`${key}: ${String(value)}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return body.length ? `{\n${indent(body.join(',\n'))}\n}` : `{}`;
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
const resolveInheritedEntities = (root, entities) => {
|
|
957
|
+
const parent = root.extends ? entities.get(root.extends) : undefined;
|
|
958
|
+
return parent ? [parent, ...resolveInheritedEntities(parent, entities)] : [];
|
|
959
|
+
};
|
|
960
|
+
const generatePropertyDescriptors = (entity, entities, services, options) => [
|
|
961
|
+
...resolveInheritedEntities(entity, entities).flatMap(v => [...v.properties]),
|
|
962
|
+
...entity.properties
|
|
963
|
+
].filter(([, meta]) => {
|
|
964
|
+
// If we generate deprecated things we can skip the filtering
|
|
965
|
+
if (options.deprecated) {
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
// Check if corresponding service is deprecated and can be removed
|
|
969
|
+
const service = services.find(v => v.entity === meta.service);
|
|
970
|
+
return !meta.service || (service && !service.deprecated);
|
|
971
|
+
}).map(([property, meta]) => ({
|
|
972
|
+
key: property,
|
|
973
|
+
value: Object.entries(meta).map(([key, value]) => ({
|
|
974
|
+
key,
|
|
975
|
+
value: value ? generateString(value) : undefined
|
|
976
|
+
}))
|
|
977
|
+
}));
|
|
978
|
+
const generateEntityPropertyMap = (entities, services, options) => {
|
|
979
|
+
const typeName = 'WEntityProperties';
|
|
980
|
+
const propertyMap = [...entities].map(([entity, data]) => ({
|
|
981
|
+
key: entity,
|
|
982
|
+
value: generatePropertyDescriptors(data, entities, services, options)
|
|
983
|
+
}));
|
|
984
|
+
return generateStatements(`export type ${typeName} = Partial<Record<WEntity, Partial<Record<string, WEntityPropertyMeta>>>>;`, `export const wEntityProperties: ${typeName} = ${generateObject(propertyMap)};`);
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
const generateArray = (values) => {
|
|
988
|
+
return `[${concat(values.map(v => generateString(String(v))))}]`;
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// Only functions matching this regex are included in the generation.
|
|
992
|
+
const FILTER_REGEX = /^(some|count|create|remove|unique|update)$/;
|
|
993
|
+
/**
|
|
994
|
+
* Generates for each function a map with the entity-name as key and service type as value.
|
|
995
|
+
* E.g. WServicesWith[Function] where [Function] may be something like "some" or "create".
|
|
996
|
+
*
|
|
997
|
+
* This function also generates an exported array with the names of each service for each name.
|
|
998
|
+
*/
|
|
999
|
+
const generateGroupedServices = (services) => {
|
|
1000
|
+
const entityDescriptors = new Map();
|
|
1001
|
+
for (const { entity, functions } of services) {
|
|
1002
|
+
for (const { name } of functions) {
|
|
1003
|
+
if (!FILTER_REGEX.test(name)) {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
entityDescriptors.set(name, [
|
|
1007
|
+
...(entityDescriptors.get(name) ?? []), {
|
|
1008
|
+
name: entity,
|
|
1009
|
+
required: true,
|
|
1010
|
+
type: `${pascalCase(entity)}Service_${pascalCase(name)}`
|
|
1011
|
+
}
|
|
1012
|
+
]);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const descriptors = [...entityDescriptors.entries()];
|
|
1016
|
+
const typeGuards = [];
|
|
1017
|
+
for (const [name] of descriptors) {
|
|
1018
|
+
const constant = camelCase(`wServiceWith_${name}_Names`);
|
|
1019
|
+
const service = pascalCase(`WServiceWith_${name}`);
|
|
1020
|
+
const guard = `(service: string | undefined): service is ${service} =>\n${indent(`${constant}.includes(service as ${service});`)}`;
|
|
1021
|
+
typeGuards.push(`export const is${service} = ${guard}`);
|
|
1022
|
+
}
|
|
1023
|
+
return [
|
|
1024
|
+
...descriptors.map(([name, props]) => generateInterface(pascalCase(`WServicesWith_${name}`), props)),
|
|
1025
|
+
...descriptors.map(([name]) => generateType(pascalCase(`WServiceWith_${name}`), `keyof ${pascalCase(`WServicesWith_${name}`)}`)),
|
|
1026
|
+
...descriptors.map(([name, props]) => {
|
|
1027
|
+
const constant = camelCase(`wServiceWith_${name}_Names`);
|
|
1028
|
+
const type = pascalCase(`WServiceWith_${name}`);
|
|
1029
|
+
const value = generateArray(props.map(v => v.name));
|
|
1030
|
+
return `export const ${constant}: ${type}[] = ${value};`;
|
|
1031
|
+
}),
|
|
1032
|
+
generateBlockComment('Type guards for service classes.', generateStatements(...typeGuards))
|
|
1033
|
+
];
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const obj = (list) => `{\n${indent(list.join(',\n'))}\n}`;
|
|
1037
|
+
const arr = (list) => `[\n${indent(list.join(',\n'))}\n]`;
|
|
1038
|
+
const generateMaps = ({ services, entities, aliases, enums, options }) => {
|
|
1039
|
+
const entitiesKeys = [...entities.keys()];
|
|
1040
|
+
const enumsArray = `export const wEnums = ${obj(enums)};`;
|
|
1041
|
+
const entityNames = `export const wEntityNames: WEntity[] = ${arr(entitiesKeys.map(v => `'${v}'`))};`;
|
|
1042
|
+
const serviceNames = `export const wServiceNames: WService[] = ${arr(services.map(v => `'${v.entity}'`))};`;
|
|
1043
|
+
const serviceValues = `export const wServiceFactories = ${obj(services.map(v => `${v.entity}: ${v.serviceName}`))};`;
|
|
1044
|
+
const serviceInstanceValues = `export const wServices = ${obj(services.map(v => {
|
|
1045
|
+
const src = `${v.entity}: ${v.serviceName}()`;
|
|
1046
|
+
return v.deprecated ? generateInlineComment('@deprecated') + `\n${src}` : src;
|
|
1047
|
+
}))};`;
|
|
1048
|
+
const entityInterfaces = [
|
|
1049
|
+
...entitiesKeys.map(entity => ({
|
|
1050
|
+
name: entity,
|
|
1051
|
+
type: pascalCase(entity),
|
|
1052
|
+
required: true
|
|
1053
|
+
})),
|
|
1054
|
+
...services.map(service => {
|
|
1055
|
+
const alias = aliases.get(service.entity);
|
|
1056
|
+
return {
|
|
1057
|
+
name: service.entity,
|
|
1058
|
+
type: alias ?? 'never',
|
|
1059
|
+
required: true,
|
|
1060
|
+
comment: alias ? undefined : 'no response defined or inlined'
|
|
1061
|
+
};
|
|
1062
|
+
})
|
|
1063
|
+
];
|
|
1064
|
+
const createMappingType = (type, prefix) => type !== 'never' ? `${type}_${prefix}` : type;
|
|
1065
|
+
const entitiesList = generateInterface('WEntities', entityInterfaces);
|
|
1066
|
+
const entityReferences = generateInterface('WEntityReferences', entityInterfaces.map(v => ({ ...v, type: createMappingType(v.type, 'References') })));
|
|
1067
|
+
const entityMappings = generateInterface('WEntityMappings', entityInterfaces.map(v => ({ ...v, type: createMappingType(v.type, 'Mappings') })));
|
|
1068
|
+
const entityFilter = generateInterface('WEntityFilters', entityInterfaces.map(v => ({ ...v, type: createMappingType(v.type, 'Filter') })));
|
|
1069
|
+
return {
|
|
1070
|
+
source: generateStatements(
|
|
1071
|
+
/* JS Values */
|
|
1072
|
+
serviceValues, serviceInstanceValues, entityNames, serviceNames, enumsArray, generateEntityPropertyMap(entities, services, options),
|
|
1073
|
+
/* Map of entity to references / mappings and filters*/
|
|
1074
|
+
entityReferences, entityMappings, entityFilter,
|
|
1075
|
+
/* List of all entities with their corresponding service */
|
|
1076
|
+
generateBlockComment(`
|
|
1077
|
+
This interfaces merges two maps:
|
|
1078
|
+
- Map<[entityName], [entityInterfaceName]>
|
|
1079
|
+
- Map<[serviceName], [entityInterfaceName]>
|
|
1080
|
+
|
|
1081
|
+
Where [entityName] is
|
|
1082
|
+
- the name of a nested entity (e.g. 'address' from Party)
|
|
1083
|
+
- the name of an entity (e.g. 'party', 'article' etc.)
|
|
1084
|
+
|
|
1085
|
+
Where [serviceName] is the name of an endpoint (e.g. for /article its 'article')
|
|
1086
|
+
|
|
1087
|
+
Where [entityInterfaceName] is
|
|
1088
|
+
- the underlying type for this entity
|
|
1089
|
+
- the type for what is returned by the api
|
|
1090
|
+
`, entitiesList),
|
|
1091
|
+
/* type-ofs and types */
|
|
1092
|
+
generateType('WServices', 'typeof wServices'), generateType('WServiceFactories', 'typeof wServiceFactories'), generateType('WService', 'keyof WServices'), generateType('WEntity', 'keyof WEntities'), generateType('WEnums', 'typeof wEnums'), generateType('WEnum', 'keyof WEnums'),
|
|
1093
|
+
/* Utilities. */
|
|
1094
|
+
generateCustomValueUtilities(entities, services),
|
|
1095
|
+
/* All functions grouped by service supporting it */
|
|
1096
|
+
...generateGroupedServices(services))
|
|
1097
|
+
};
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const parseReferencedEntity = (obj) => pascalCase(obj.$ref.replace(/.*\//, ''));
|
|
1101
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
1102
|
+
const extractSchemas = (doc) => {
|
|
1103
|
+
const schemas = new Map();
|
|
1104
|
+
const aliases = new Map();
|
|
1105
|
+
for (const [name, schema] of Object.entries(doc.components?.schemas ?? {})) {
|
|
1106
|
+
if (!isReferenceObject(schema)) {
|
|
1107
|
+
schemas.set(name, schema);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Referenced schemas in responses, in some case the response from the root endpoint
|
|
1112
|
+
* refers to a schema with a different name
|
|
1113
|
+
*/
|
|
1114
|
+
for (const [path, methods] of Object.entries(doc.paths)) {
|
|
1115
|
+
const parsed = parseEndpointPath(path);
|
|
1116
|
+
if (!parsed || schemas.has(parsed.entity)) {
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
for (const method of Object.values(OpenAPIV3.HttpMethods)) {
|
|
1120
|
+
const body = methods[method]?.responses['200'];
|
|
1121
|
+
if (isResponseObject(body) && body.content) {
|
|
1122
|
+
const responseSchema = Object.values(body.content)[0]?.schema;
|
|
1123
|
+
if (isReferenceObject(responseSchema)) {
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const itemsSchema = responseSchema?.properties?.result;
|
|
1127
|
+
if (isReferenceObject(itemsSchema)) {
|
|
1128
|
+
aliases.set(parsed.entity, parseReferencedEntity(itemsSchema));
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (isArraySchemaObject(itemsSchema)) {
|
|
1132
|
+
const { items } = itemsSchema;
|
|
1133
|
+
if (isReferenceObject(items)) {
|
|
1134
|
+
aliases.set(parsed.entity, parseReferencedEntity(items));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return { schemas, aliases };
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
const generate = (doc, options) => {
|
|
1144
|
+
const { schemas, aliases } = extractSchemas(doc);
|
|
1145
|
+
const enums = generateEnums(schemas);
|
|
1146
|
+
const entities = generateEntities(schemas, enums);
|
|
1147
|
+
const services = generateServices(doc, aliases, options);
|
|
1148
|
+
return generateStatements(generateBase(options.target), generateBlockComment('ENUMS', generateStatements(...[...enums.values()].map(v => v.source))), generateBlockComment('ENTITIES', generateStatements(...[...entities.values()].map(v => v.source))), generateBlockComment('SERVICES', generateStatements(...[...services.values()].map(v => v.source))), generateBlockComment('MAPS', generateMaps({
|
|
1149
|
+
services: [...services.values()],
|
|
1150
|
+
enums: [...enums.keys()],
|
|
1151
|
+
options,
|
|
1152
|
+
entities,
|
|
1153
|
+
aliases
|
|
1154
|
+
}).source));
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const hash = (content, algorithm = 'sha256') => {
|
|
1158
|
+
const hash = createHash(algorithm);
|
|
1159
|
+
if (Array.isArray(content)) {
|
|
1160
|
+
content.map(hash.update.bind(hash));
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
hash.update(content);
|
|
1164
|
+
}
|
|
1165
|
+
return hash.digest('hex');
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
var name = "@weclapp/sdk";
|
|
1169
|
+
var version = "^1.8.0";
|
|
1170
|
+
var description = "SDK generator based on a weclapp api swagger file";
|
|
1171
|
+
var author = "weclapp";
|
|
1172
|
+
var sideEffects = false;
|
|
1173
|
+
var bin = {
|
|
1174
|
+
"build-weclapp-sdk": "./bin/cli.js"
|
|
1175
|
+
};
|
|
1176
|
+
var files = [
|
|
1177
|
+
"bin",
|
|
1178
|
+
"dist",
|
|
1179
|
+
"tsconfig.lib.json"
|
|
1180
|
+
];
|
|
1181
|
+
var engines = {
|
|
1182
|
+
node: "^18 || ^20",
|
|
1183
|
+
npm: "^9 || ^8"
|
|
1184
|
+
};
|
|
1185
|
+
var types = "./sdk/dist/index.d.ts";
|
|
1186
|
+
var main = "./sdk/dist/index.cjs";
|
|
1187
|
+
var module = "./sdk/dist/index.js";
|
|
1188
|
+
var type = "module";
|
|
1189
|
+
var exports = {
|
|
1190
|
+
".": {
|
|
1191
|
+
types: "./sdk/dist/index.d.ts",
|
|
1192
|
+
"import": "./sdk/dist/index.js",
|
|
1193
|
+
require: "./sdk/dist/index.cjs"
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
var scripts = {
|
|
1197
|
+
"cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
|
|
1198
|
+
"cli:watch": "cross-env NODE_ENV=development rollup -c rollup.config.js --watch",
|
|
1199
|
+
"sdk:build": "./bin/cli.js test/openapi.json --target node",
|
|
1200
|
+
lint: "eslint {src,test}/**/*.ts",
|
|
1201
|
+
"lint:fix": "npm run lint -- --fix",
|
|
1202
|
+
"ci:test": "npm run lint:fix && npm run cli:build && npm run sdk:build",
|
|
1203
|
+
release: "standard-version"
|
|
1204
|
+
};
|
|
1205
|
+
var repository = {
|
|
1206
|
+
type: "git",
|
|
1207
|
+
url: "git@git.internal.weclapp.com:weclapp/weclapp-api/sdk-generator.git"
|
|
1208
|
+
};
|
|
1209
|
+
var devDependencies = {
|
|
1210
|
+
"@typescript-eslint/eslint-plugin": "^5.59.11",
|
|
1211
|
+
"@typescript-eslint/parser": "^5.59.11",
|
|
1212
|
+
eslint: "^8.42.0",
|
|
1213
|
+
"rollup-plugin-string": "^3.0.0",
|
|
1214
|
+
"standard-version": "^9.5.0"
|
|
1215
|
+
};
|
|
1216
|
+
var dependencies = {
|
|
1217
|
+
"@rollup/plugin-json": "^6.0.0",
|
|
1218
|
+
"@rollup/plugin-terser": "^0.4.3",
|
|
1219
|
+
"@types/fs-extra": "^11.0.1",
|
|
1220
|
+
"@types/yargs": "^17.0.24",
|
|
1221
|
+
chalk: "^5.2.0",
|
|
1222
|
+
"change-case": "^4.1.2",
|
|
1223
|
+
"cross-env": "^7.0.3",
|
|
1224
|
+
dotenv: "^16.1.4",
|
|
1225
|
+
"indent-string": "^5.0.0",
|
|
1226
|
+
"openapi-types": "^12.1.3",
|
|
1227
|
+
"pretty-ms": "^8.0.0",
|
|
1228
|
+
rollup: "^3.25.1",
|
|
1229
|
+
"rollup-plugin-ts": "^3.2.0",
|
|
1230
|
+
typescript: "^5.1.3",
|
|
1231
|
+
yargs: "^17.7.2"
|
|
1232
|
+
};
|
|
1233
|
+
var peerDependencies = {
|
|
1234
|
+
rxjs: "^7.5.5"
|
|
1235
|
+
};
|
|
1236
|
+
var pkg = {
|
|
1237
|
+
name: name,
|
|
1238
|
+
version: version,
|
|
1239
|
+
description: description,
|
|
1240
|
+
author: author,
|
|
1241
|
+
sideEffects: sideEffects,
|
|
1242
|
+
bin: bin,
|
|
1243
|
+
files: files,
|
|
1244
|
+
engines: engines,
|
|
1245
|
+
types: types,
|
|
1246
|
+
main: main,
|
|
1247
|
+
module: module,
|
|
1248
|
+
type: type,
|
|
1249
|
+
exports: exports,
|
|
1250
|
+
scripts: scripts,
|
|
1251
|
+
repository: repository,
|
|
1252
|
+
devDependencies: devDependencies,
|
|
1253
|
+
dependencies: dependencies,
|
|
1254
|
+
peerDependencies: peerDependencies
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
1258
|
+
const cli = async () => {
|
|
1259
|
+
const { argv } = yargs(hideBin(process.argv))
|
|
1260
|
+
.scriptName('build-weclapp-sdk')
|
|
1261
|
+
.usage('Usage: $0 <source> [flags]')
|
|
1262
|
+
.version(version)
|
|
1263
|
+
.example('$0 openapi.json', 'Generate the SDK based on a local openapi file')
|
|
1264
|
+
.example('$0 xxx.weclapp.com --key ...', 'Generate the SDK based on the openapi file from the given weclapp instance')
|
|
1265
|
+
.help('h')
|
|
1266
|
+
.alias('v', 'version')
|
|
1267
|
+
.alias('h', 'help')
|
|
1268
|
+
.option('k', {
|
|
1269
|
+
alias: 'key',
|
|
1270
|
+
describe: 'API Key (only needed when not using a local file)',
|
|
1271
|
+
type: 'string'
|
|
1272
|
+
})
|
|
1273
|
+
.option('c', {
|
|
1274
|
+
alias: 'cache',
|
|
1275
|
+
describe: 'If the generated SDK should cached',
|
|
1276
|
+
type: 'boolean'
|
|
1277
|
+
})
|
|
1278
|
+
.option('q', {
|
|
1279
|
+
alias: 'query',
|
|
1280
|
+
describe: 'Extra query params when fetching the openapi.json from a server',
|
|
1281
|
+
type: 'string'
|
|
1282
|
+
})
|
|
1283
|
+
.option('generate-unique', {
|
|
1284
|
+
describe: 'Generate .unique functions',
|
|
1285
|
+
type: 'boolean'
|
|
1286
|
+
})
|
|
1287
|
+
.option('d', {
|
|
1288
|
+
alias: 'deprecated',
|
|
1289
|
+
describe: 'Include deprecated functions and services',
|
|
1290
|
+
type: 'boolean'
|
|
1291
|
+
})
|
|
1292
|
+
.option('e', {
|
|
1293
|
+
alias: 'from-env',
|
|
1294
|
+
describe: 'Use env variables WECLAPP_BACKEND_URL and WECLAPP_API_KEY as credentials',
|
|
1295
|
+
type: 'boolean'
|
|
1296
|
+
})
|
|
1297
|
+
.option('t', {
|
|
1298
|
+
alias: 'target',
|
|
1299
|
+
describe: 'Specify the target platform',
|
|
1300
|
+
type: 'string',
|
|
1301
|
+
choices: ['browser', 'browser.rx', 'node', 'node.rx']
|
|
1302
|
+
})
|
|
1303
|
+
.option('d', {
|
|
1304
|
+
alias: 'deprecated',
|
|
1305
|
+
describe: 'Include deprecated functions and services',
|
|
1306
|
+
type: 'boolean'
|
|
1307
|
+
})
|
|
1308
|
+
.epilog(`Copyright ${new Date().getFullYear()} weclapp GmbH`);
|
|
1309
|
+
if (argv.fromEnv) {
|
|
1310
|
+
config();
|
|
1311
|
+
}
|
|
1312
|
+
const { WECLAPP_API_KEY, WECLAPP_BACKEND_URL } = process.env;
|
|
1313
|
+
const { query, cache = false, deprecated = false, key = WECLAPP_API_KEY, _: [src = WECLAPP_BACKEND_URL] } = argv;
|
|
1314
|
+
const options = {
|
|
1315
|
+
deprecated,
|
|
1316
|
+
generateUnique: argv.generateUnique ?? false,
|
|
1317
|
+
target: argv.target ?? Target.BROWSER_PROMISES
|
|
1318
|
+
};
|
|
1319
|
+
if (typeof src === 'number') {
|
|
1320
|
+
return Promise.reject('Expected string as command');
|
|
1321
|
+
}
|
|
1322
|
+
if (!Object.values(Target).includes(options.target)) {
|
|
1323
|
+
logger.errorLn(`Unknown target: ${options.target}. Possible values are ${Object.values(Target).join(', ')}`);
|
|
1324
|
+
return Promise.reject();
|
|
1325
|
+
}
|
|
1326
|
+
if (await stat(src).catch(() => false)) {
|
|
1327
|
+
logger.infoLn(`Source is a file`);
|
|
1328
|
+
const content = JSON.parse(await readFile(src, 'utf-8'));
|
|
1329
|
+
return { cache, content, options };
|
|
1330
|
+
}
|
|
1331
|
+
const url = new URL(src.startsWith('http') ? src : `https://${src}`);
|
|
1332
|
+
url.pathname = '/webapp/api/v1/meta/openapi.json';
|
|
1333
|
+
if (query?.length) {
|
|
1334
|
+
for (const param of query.split(',')) {
|
|
1335
|
+
const [name, value] = param.split('=');
|
|
1336
|
+
url.searchParams.set(name, value);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const content = await fetch(url.toString(), {
|
|
1340
|
+
headers: { 'Accept': 'application/json', 'AuthenticationToken': key }
|
|
1341
|
+
}).then(res => res.ok ? res.json() : undefined);
|
|
1342
|
+
if (!content) {
|
|
1343
|
+
logger.errorLn(`Couldn't fetch file ${url.toString()} `);
|
|
1344
|
+
return Promise.reject();
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
logger.infoLn(`Use remote file: ${url.toString()}`);
|
|
1348
|
+
}
|
|
1349
|
+
return { cache, content, options };
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
const workingDirectory = resolve(currentDirname(), './sdk');
|
|
1353
|
+
const folders = ['docs', 'main', 'node', 'raw', 'rx', 'utils'];
|
|
1354
|
+
void (async () => {
|
|
1355
|
+
const start = process.hrtime.bigint();
|
|
1356
|
+
const { content: doc, cache: useCache, options } = await cli();
|
|
1357
|
+
// Resolve cache dir and key
|
|
1358
|
+
const cacheKey = hash([pkg.version, JSON.stringify(doc), JSON.stringify(options)]).slice(-8);
|
|
1359
|
+
const cacheDir = resolve(currentDirname(), '.tmp', cacheKey);
|
|
1360
|
+
const dist = (...paths) => resolve(workingDirectory, ...paths);
|
|
1361
|
+
const tmp = async (...paths) => {
|
|
1362
|
+
const fullPath = resolve(cacheDir, ...paths);
|
|
1363
|
+
await mkdir(dirname(fullPath), { recursive: true }).catch(() => null);
|
|
1364
|
+
return fullPath;
|
|
1365
|
+
};
|
|
1366
|
+
if (useCache) {
|
|
1367
|
+
logger.infoLn(`Cache ID: ${cacheKey}`);
|
|
1368
|
+
}
|
|
1369
|
+
if (useCache && await stat(cacheDir).catch(() => false)) {
|
|
1370
|
+
logger.successLn(`Cache match! (${cacheDir})`);
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
// Store swagger.json file
|
|
1374
|
+
await writeFile(await tmp('openapi.json'), JSON.stringify(doc, null, 2));
|
|
1375
|
+
logger.infoLn(`Generate sdk (target: ${options.target})`);
|
|
1376
|
+
// Generate SDKs
|
|
1377
|
+
const sdk = generate(doc, options);
|
|
1378
|
+
await writeFile(await tmp('src', `${options.target}.ts`), sdk.trim() + '\n');
|
|
1379
|
+
// Bundle
|
|
1380
|
+
logger.infoLn('Bundle... (this may take some time)');
|
|
1381
|
+
await bundle(cacheDir, options.target);
|
|
1382
|
+
// Remove old SDK
|
|
1383
|
+
await Promise.all(folders.map(async (dir) => rm(dist(dir), { recursive: true }).catch(() => 0)));
|
|
1384
|
+
}
|
|
1385
|
+
// Copy bundled SDK
|
|
1386
|
+
await cp(cacheDir, workingDirectory, { recursive: true });
|
|
1387
|
+
// Print job summary
|
|
1388
|
+
const duration = (process.hrtime.bigint() - start) / 1000000n;
|
|
1389
|
+
logger.successLn(`SDK built in ${prettyMs(Number(duration))}`);
|
|
1390
|
+
logger.printSummary();
|
|
1391
|
+
})().catch((error) => {
|
|
1392
|
+
logger.errorLn(`Fatal error:`);
|
|
1393
|
+
/* eslint-disable no-console */
|
|
1394
|
+
console.error(error);
|
|
1395
|
+
}).finally(() => {
|
|
1396
|
+
logger.errors && process.exit(1);
|
|
1397
|
+
});
|