rastack 0.0.16 → 0.0.19
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/CHANGELOG.md +43874 -43013
- package/dist/.prettierrc +7 -7
- package/dist/compile/analyze.js +19 -11
- package/dist/compile/index.js +26 -17
- package/dist/compile/manifest.js +2 -3
- package/dist/compile/openapi.js +5 -8
- package/dist/compile/program.js +19 -10
- package/dist/csv-schema.js +5 -6
- package/dist/define/index.js +2 -2
- package/dist/entity-generation/delete-method.js +8 -10
- package/dist/entity-generation/form.js +80 -82
- package/dist/entity-generation/get-method.js +35 -36
- package/dist/entity-generation/imports.js +24 -20
- package/dist/entity-generation/sync-method.d.ts +1 -1
- package/dist/entity-generation/sync-method.js +45 -46
- package/dist/entity-generation/update-method.js +9 -10
- package/dist/rad-wasm-build.d.ts +1 -1
- package/dist/rad-wasm-build.js +15 -6
- package/dist/rad.js +9 -2
- package/dist/schema/camel-to-pastel.js +1 -2
- package/dist/schema/capitalise-first-letter.js +1 -2
- package/dist/schema/extract-response.js +3 -4
- package/dist/schema/fetch-schema.js +4 -16
- package/dist/schema/remove-non-model-paths.js +1 -2
- package/dist/schema/to-camel-case.js +1 -2
- package/dist/schema/to-pastel-case.js +1 -2
- package/dist/schema-convert.js +22 -33
- package/dist/schema-entities.js +177 -189
- package/dist/schema-fetch.js +46 -63
- package/dist/schema-full.js +23 -36
- package/dist/schema-index.js +16 -27
- package/dist/schema-params.js +11 -22
- package/dist/seed.js +95 -97
- package/hooks/form/form.ts +207 -207
- package/hooks/form/index.ts +8 -8
- package/hooks/form/interfaces.ts +217 -217
- package/hooks/form/structure.ts +39 -39
- package/hooks/form/validate-schema.ts +49 -49
- package/hooks/index.ts +3 -3
- package/hooks/query/api.ts +42 -42
- package/hooks/query/delete.ts +45 -45
- package/hooks/query/fetch.ts +48 -48
- package/hooks/query/index.ts +21 -21
- package/hooks/query/interfaces.ts +111 -111
- package/hooks/query/list.ts +286 -286
- package/hooks/query/update.ts +88 -88
- package/hooks/query/url.ts +31 -31
- package/hooks/real-time/index.ts +1 -1
- package/hooks/real-time/pusher.ts +43 -43
- package/jest.config.cjs +6 -6
- package/package.json +57 -57
- package/provider/index.ts +13 -13
- package/provider/provider.tsx +187 -187
- package/provider/types.ts +114 -114
- package/provider/warehouse.ts +167 -167
- package/provider/wasm.ts +124 -124
- package/runtime.ts +3 -3
- package/src/.prettierrc +7 -7
- package/src/compile/analyze.ts +224 -224
- package/src/compile/index.ts +86 -86
- package/src/compile/manifest.ts +10 -10
- package/src/compile/model.ts +69 -69
- package/src/compile/openapi.ts +266 -266
- package/src/compile/program.ts +40 -40
- package/src/csv-schema.ts +187 -187
- package/src/define/index.ts +162 -162
- package/src/entity-generation/delete-method.ts +37 -37
- package/src/entity-generation/form.ts +169 -169
- package/src/entity-generation/get-method.ts +80 -80
- package/src/entity-generation/imports.ts +110 -110
- package/src/entity-generation/sync-method.ts +222 -222
- package/src/entity-generation/update-method.ts +38 -38
- package/src/index.ts +77 -77
- package/src/rad-compile.ts +88 -88
- package/src/rad-wasm-build.ts +102 -102
- package/src/rad.ts +101 -101
- package/src/scan.ts +89 -89
- package/src/schema/camel-to-pastel.ts +3 -3
- package/src/schema/capitalise-first-letter.ts +3 -3
- package/src/schema/extract-response.ts +38 -38
- package/src/schema/fetch-schema.ts +12 -12
- package/src/schema/remove-non-model-paths.ts +13 -13
- package/src/schema/to-camel-case.ts +7 -7
- package/src/schema/to-pastel-case.ts +6 -6
- package/src/schema-convert.ts +61 -61
- package/src/schema-entities.ts +401 -401
- package/src/schema-fetch.ts +82 -82
- package/src/schema-full.ts +38 -38
- package/src/schema-index.ts +29 -29
- package/src/schema-params.ts +116 -116
- package/src/seed.ts +297 -297
- package/sync/engine.ts +392 -392
- package/sync/hooks.ts +450 -450
- package/sync/index.ts +8 -8
- package/sync/persistence.ts +237 -237
- package/sync/provider.tsx +91 -91
- package/sync/registry.ts +36 -36
- package/sync/store.ts +126 -126
- package/sync/transactions.ts +300 -300
- package/sync/types.ts +94 -94
- package/test/compile.spec.ts +192 -192
- package/test/csv-schema.spec.ts +143 -143
- package/test/schema-entities.spec.ts +688 -688
- package/tsconfig.json +24 -24
- package/types.ts +18 -18
package/src/schema-entities.ts
CHANGED
|
@@ -1,401 +1,401 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import {
|
|
4
|
-
OperationObject,
|
|
5
|
-
ParameterObject,
|
|
6
|
-
PathsObject,
|
|
7
|
-
RequestBodyObject,
|
|
8
|
-
} from "@ts-stack/openapi-spec";
|
|
9
|
-
import { extractResponseBodySchema } from "./schema/extract-response";
|
|
10
|
-
import { fetchSchema } from "./schema/fetch-schema";
|
|
11
|
-
import { removeNonModelPaths } from "./schema/remove-non-model-paths";
|
|
12
|
-
import { camelToPastel } from "./schema/camel-to-pastel";
|
|
13
|
-
import { generateGetAPIMethod } from "./entity-generation/get-method";
|
|
14
|
-
import { generateUpdateAPIMethod } from "./entity-generation/update-method";
|
|
15
|
-
import {
|
|
16
|
-
generateFolderImports,
|
|
17
|
-
generateImportStatements,
|
|
18
|
-
generateIndexImports,
|
|
19
|
-
generateKeyImports,
|
|
20
|
-
} from "./entity-generation/imports";
|
|
21
|
-
import { generateUpdateFormMethod } from "./entity-generation/form";
|
|
22
|
-
import { generateDeleteAPIMethod } from "./entity-generation/delete-method";
|
|
23
|
-
import {
|
|
24
|
-
EntitySyncMeta,
|
|
25
|
-
generateConfirmedMethod,
|
|
26
|
-
generateEntityDescriptor,
|
|
27
|
-
generateSyncDeleteMethod,
|
|
28
|
-
generateSyncListMethod,
|
|
29
|
-
generateSyncRecordMethod,
|
|
30
|
-
generateSyncUpdateMethod,
|
|
31
|
-
} from "./entity-generation/sync-method";
|
|
32
|
-
import { toCamelCase } from "./schema/to-camel-case";
|
|
33
|
-
|
|
34
|
-
function pathSegments(pathKey: string): string[] {
|
|
35
|
-
return pathKey.replace(/\/$/, "").split("/");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Pre-pass over the OpenAPI paths: collects, per entity, the sync mode
|
|
40
|
-
* (from rastack's `x-rastack-sync` vendor extension), confirmed operations,
|
|
41
|
-
* the list/detail URLs, the row schema, and the list/detail operation ids
|
|
42
|
-
* (used to wire cache invalidation for non-sync entities).
|
|
43
|
-
*/
|
|
44
|
-
export function collectEntityMeta(paths: PathsObject): {
|
|
45
|
-
[indexKey: string]: EntitySyncMeta;
|
|
46
|
-
} {
|
|
47
|
-
const meta: { [indexKey: string]: EntitySyncMeta } = {};
|
|
48
|
-
|
|
49
|
-
Object.keys(paths).forEach((pathKey) => {
|
|
50
|
-
const appName = pathKey.split("/")[2];
|
|
51
|
-
const modelName = pathKey.split("/")[4];
|
|
52
|
-
const indexKey = `${appName}|${modelName}`;
|
|
53
|
-
if (!meta[indexKey]) {
|
|
54
|
-
meta[indexKey] = { sync: false, confirmedOperationIds: new Set() };
|
|
55
|
-
}
|
|
56
|
-
const entityMeta = meta[indexKey];
|
|
57
|
-
|
|
58
|
-
const segments = pathSegments(pathKey);
|
|
59
|
-
const isListPath = segments.length === 5;
|
|
60
|
-
const isDetailPath = segments.length === 6 && segments[5].startsWith("{");
|
|
61
|
-
if (isListPath) {
|
|
62
|
-
entityMeta.listUrl = pathKey;
|
|
63
|
-
}
|
|
64
|
-
if (isDetailPath) {
|
|
65
|
-
entityMeta.detailUrl = pathKey;
|
|
66
|
-
entityMeta.detailParam = segments[5].slice(1, -1);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
Object.keys(paths[pathKey]).forEach((method) => {
|
|
70
|
-
// @ts-ignore
|
|
71
|
-
const operation = paths[pathKey][method] as OperationObject;
|
|
72
|
-
const syncExtension = (operation as any)["x-rastack-sync"];
|
|
73
|
-
if (syncExtension === "local" || syncExtension === "confirmed") {
|
|
74
|
-
entityMeta.sync = true;
|
|
75
|
-
}
|
|
76
|
-
if ((operation as any)["x-rastack-sync-load"] === "lazy") {
|
|
77
|
-
entityMeta.lazy = true;
|
|
78
|
-
}
|
|
79
|
-
if ((operation as any)["x-rastack-sync-client-ids"] === true) {
|
|
80
|
-
entityMeta.clientIds = true;
|
|
81
|
-
}
|
|
82
|
-
if (syncExtension === "confirmed" && operation.operationId) {
|
|
83
|
-
entityMeta.confirmedOperationIds.add(operation.operationId as string);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const [responseBodySchema, isList] =
|
|
87
|
-
extractResponseBodySchema(operation);
|
|
88
|
-
const operationId = (operation.operationId as string) || "";
|
|
89
|
-
const isPostList =
|
|
90
|
-
method === "post" &&
|
|
91
|
-
(operationId.includes("List") || operationId.includes("list"));
|
|
92
|
-
|
|
93
|
-
if (method === "get" || isPostList) {
|
|
94
|
-
if (isList || isPostList) {
|
|
95
|
-
if (isListPath && !entityMeta.listOperationId) {
|
|
96
|
-
entityMeta.listOperationId = operationId;
|
|
97
|
-
}
|
|
98
|
-
} else if (isDetailPath && !entityMeta.detailOperationId) {
|
|
99
|
-
entityMeta.detailOperationId = operationId;
|
|
100
|
-
}
|
|
101
|
-
if (responseBodySchema && !entityMeta.rowSchema) {
|
|
102
|
-
entityMeta.rowSchema = `I${responseBodySchema}`;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
return meta;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export async function generateFolders(
|
|
112
|
-
entitiesPath: string,
|
|
113
|
-
paths: PathsObject,
|
|
114
|
-
): Promise<void> {
|
|
115
|
-
Object.keys(paths).forEach((key) => {
|
|
116
|
-
const appName = key.split("/")[2];
|
|
117
|
-
const modelName = key.split("/")[4];
|
|
118
|
-
const appFolder = join(entitiesPath, appName);
|
|
119
|
-
const modelFolder = join(appFolder, modelName);
|
|
120
|
-
|
|
121
|
-
mkdirSync(appFolder, { recursive: true });
|
|
122
|
-
mkdirSync(modelFolder, { recursive: true });
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function generateFiles(
|
|
127
|
-
entitiesPath: string,
|
|
128
|
-
paths: PathsObject,
|
|
129
|
-
): Promise<void> {
|
|
130
|
-
const uniqueOperationNames: string[] = [];
|
|
131
|
-
const modelFileImports: { [key: string]: Set<string> } = {};
|
|
132
|
-
const modelFileData: { [key: string]: string } = {};
|
|
133
|
-
const keysFileData: { [key: string]: string[] } = {};
|
|
134
|
-
const modelImportsFileData: { [key: string]: string[] } = {};
|
|
135
|
-
const formMutationOperations: { [key: string]: any } = {};
|
|
136
|
-
const entityMeta = collectEntityMeta(paths);
|
|
137
|
-
|
|
138
|
-
Object.keys(paths).forEach((pathKey) => {
|
|
139
|
-
const appName = pathKey.split("/")[2];
|
|
140
|
-
const modelName = pathKey.split("/")[4];
|
|
141
|
-
|
|
142
|
-
const indexKey = `${appName}|${modelName}`;
|
|
143
|
-
if (!formMutationOperations[indexKey]) {
|
|
144
|
-
formMutationOperations[indexKey] = {
|
|
145
|
-
post: {
|
|
146
|
-
operationId: undefined,
|
|
147
|
-
schema: undefined,
|
|
148
|
-
},
|
|
149
|
-
put: {
|
|
150
|
-
operationId: undefined,
|
|
151
|
-
schema: undefined,
|
|
152
|
-
},
|
|
153
|
-
patch: {
|
|
154
|
-
operationId: undefined,
|
|
155
|
-
schema: undefined,
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
if (!modelImportsFileData[appName]) {
|
|
160
|
-
modelImportsFileData[appName] = [modelName];
|
|
161
|
-
} else {
|
|
162
|
-
if (!modelImportsFileData[appName].includes(modelName)) {
|
|
163
|
-
modelImportsFileData[appName].push(modelName);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
Object.keys(paths[pathKey]).forEach((method) => {
|
|
168
|
-
// @ts-ignore
|
|
169
|
-
const methodInformation = paths[pathKey][method] as OperationObject;
|
|
170
|
-
|
|
171
|
-
const methodParameters =
|
|
172
|
-
methodInformation.parameters as ParameterObject[];
|
|
173
|
-
const pathParameters = methodParameters
|
|
174
|
-
? methodParameters
|
|
175
|
-
.filter((item) => item.required)
|
|
176
|
-
.filter((item) => item.in === "path")
|
|
177
|
-
.map((item) => item.name)
|
|
178
|
-
: [];
|
|
179
|
-
|
|
180
|
-
const requestBody = methodInformation.requestBody as RequestBodyObject;
|
|
181
|
-
// @ts-ignore
|
|
182
|
-
const requestBodySchema = requestBody?.content?.[
|
|
183
|
-
"application/json"
|
|
184
|
-
]?.schema["$ref"]?.replace("#/components/schemas/", "");
|
|
185
|
-
|
|
186
|
-
let [responseBodySchema, isList] =
|
|
187
|
-
extractResponseBodySchema(methodInformation);
|
|
188
|
-
|
|
189
|
-
const requiredParameters = methodParameters
|
|
190
|
-
? methodParameters
|
|
191
|
-
.filter((item) => item.required)
|
|
192
|
-
.map((item) => item.name)
|
|
193
|
-
: [];
|
|
194
|
-
|
|
195
|
-
const operationId = methodInformation.operationId as string;
|
|
196
|
-
|
|
197
|
-
if (!uniqueOperationNames.includes(operationId)) {
|
|
198
|
-
uniqueOperationNames.push(operationId);
|
|
199
|
-
|
|
200
|
-
let apiMethods = "";
|
|
201
|
-
let importStatements = new Set<string>();
|
|
202
|
-
|
|
203
|
-
const isPostListEndpoint =
|
|
204
|
-
method === "post" &&
|
|
205
|
-
(operationId.includes("List") || operationId.includes("list"));
|
|
206
|
-
const isListEndpoint = isList || isPostListEndpoint;
|
|
207
|
-
|
|
208
|
-
const meta = entityMeta[indexKey];
|
|
209
|
-
const isSyncEntity = meta?.sync === true;
|
|
210
|
-
const isConfirmedOperation =
|
|
211
|
-
(methodInformation as any)["x-rastack-sync"] === "confirmed" &&
|
|
212
|
-
method !== "get";
|
|
213
|
-
const modelCamel = toCamelCase(modelName);
|
|
214
|
-
const listKeyRef = meta?.listOperationId
|
|
215
|
-
? `${modelCamel}QueriesKeys.${meta.listOperationId}`
|
|
216
|
-
: undefined;
|
|
217
|
-
const itemKeyRef = meta?.detailOperationId
|
|
218
|
-
? `${modelCamel}QueriesKeys.${meta.detailOperationId}`
|
|
219
|
-
: undefined;
|
|
220
|
-
|
|
221
|
-
if (isConfirmedOperation) {
|
|
222
|
-
[apiMethods, importStatements] = generateConfirmedMethod(
|
|
223
|
-
pathKey,
|
|
224
|
-
method,
|
|
225
|
-
operationId,
|
|
226
|
-
requestBodySchema ? `I${requestBodySchema}` : undefined,
|
|
227
|
-
responseBodySchema ? `I${responseBodySchema}` : undefined,
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
formMutationOperations[indexKey] = {
|
|
231
|
-
...formMutationOperations[indexKey],
|
|
232
|
-
[pathKey]: {
|
|
233
|
-
[method]: {
|
|
234
|
-
operationId: camelToPastel(operationId),
|
|
235
|
-
schema: requestBodySchema ? `I${requestBodySchema}` : "",
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
} else if (method === "get" || isPostListEndpoint) {
|
|
240
|
-
if (isSyncEntity && isListEndpoint) {
|
|
241
|
-
[apiMethods, importStatements] = generateSyncListMethod(
|
|
242
|
-
modelName,
|
|
243
|
-
operationId,
|
|
244
|
-
meta.rowSchema,
|
|
245
|
-
);
|
|
246
|
-
} else if (isSyncEntity && pathKey.includes("{")) {
|
|
247
|
-
const urlParams =
|
|
248
|
-
pathKey.match(/{([^}]+)}/g)?.map((param) => param.slice(1, -1)) ||
|
|
249
|
-
[];
|
|
250
|
-
[apiMethods, importStatements] = generateSyncRecordMethod(
|
|
251
|
-
modelName,
|
|
252
|
-
operationId,
|
|
253
|
-
meta.rowSchema,
|
|
254
|
-
pathParameters.length > 0 ? pathParameters : urlParams,
|
|
255
|
-
);
|
|
256
|
-
} else {
|
|
257
|
-
[apiMethods, importStatements] = generateGetAPIMethod(
|
|
258
|
-
pathKey,
|
|
259
|
-
modelName,
|
|
260
|
-
operationId,
|
|
261
|
-
responseBodySchema ? `I${responseBodySchema}` : undefined,
|
|
262
|
-
isListEndpoint,
|
|
263
|
-
pathParameters,
|
|
264
|
-
requiredParameters,
|
|
265
|
-
method,
|
|
266
|
-
requestBodySchema !== undefined ? requestBodySchema : "",
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
} else if (
|
|
270
|
-
method === "post" ||
|
|
271
|
-
method === "put" ||
|
|
272
|
-
method === "patch"
|
|
273
|
-
) {
|
|
274
|
-
if (isSyncEntity) {
|
|
275
|
-
[apiMethods, importStatements] = generateSyncUpdateMethod(
|
|
276
|
-
pathKey,
|
|
277
|
-
method,
|
|
278
|
-
modelName,
|
|
279
|
-
operationId,
|
|
280
|
-
requestBodySchema ? `I${requestBodySchema}` : undefined,
|
|
281
|
-
responseBodySchema ? `I${responseBodySchema}` : undefined,
|
|
282
|
-
pathParameters,
|
|
283
|
-
);
|
|
284
|
-
} else {
|
|
285
|
-
[apiMethods, importStatements] = generateUpdateAPIMethod(
|
|
286
|
-
pathKey,
|
|
287
|
-
method,
|
|
288
|
-
operationId,
|
|
289
|
-
requestBodySchema ? `I${requestBodySchema}` : undefined,
|
|
290
|
-
responseBodySchema ? `I${responseBodySchema}` : undefined,
|
|
291
|
-
listKeyRef,
|
|
292
|
-
itemKeyRef,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
formMutationOperations[indexKey] = {
|
|
297
|
-
...formMutationOperations[indexKey],
|
|
298
|
-
[pathKey]: {
|
|
299
|
-
[method]: {
|
|
300
|
-
operationId: camelToPastel(operationId),
|
|
301
|
-
schema: requestBodySchema ? `I${requestBodySchema}` : "",
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
} else if (method === "delete") {
|
|
306
|
-
if (isSyncEntity) {
|
|
307
|
-
[apiMethods, importStatements] = generateSyncDeleteMethod(
|
|
308
|
-
pathKey,
|
|
309
|
-
modelName,
|
|
310
|
-
operationId,
|
|
311
|
-
);
|
|
312
|
-
} else {
|
|
313
|
-
[apiMethods, importStatements] = generateDeleteAPIMethod(
|
|
314
|
-
pathKey,
|
|
315
|
-
operationId,
|
|
316
|
-
listKeyRef,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
modelFileData[indexKey] = (modelFileData[indexKey] || "") + apiMethods;
|
|
322
|
-
modelFileImports[indexKey] = new Set([
|
|
323
|
-
...(modelFileImports[indexKey] || []),
|
|
324
|
-
...importStatements,
|
|
325
|
-
]);
|
|
326
|
-
keysFileData[indexKey] = [
|
|
327
|
-
...(keysFileData[indexKey] || []),
|
|
328
|
-
operationId,
|
|
329
|
-
];
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
Object.keys(modelFileData).forEach((key) => {
|
|
335
|
-
const [appName, modelName] = key.split("|");
|
|
336
|
-
const modelFolder = join(entitiesPath, appName, modelName);
|
|
337
|
-
const apiFile = join(modelFolder, "api.ts");
|
|
338
|
-
const indexFile = join(modelFolder, "index.ts");
|
|
339
|
-
const keyImports = generateKeyImports(modelName, keysFileData[key]);
|
|
340
|
-
|
|
341
|
-
const [updateFormMethod, importStatements] = generateUpdateFormMethod(
|
|
342
|
-
appName,
|
|
343
|
-
modelName,
|
|
344
|
-
formMutationOperations[key],
|
|
345
|
-
);
|
|
346
|
-
const indexKey = `${appName}|${modelName}`;
|
|
347
|
-
modelFileImports[indexKey] = new Set([
|
|
348
|
-
...(modelFileImports[indexKey] || []),
|
|
349
|
-
...importStatements,
|
|
350
|
-
]);
|
|
351
|
-
|
|
352
|
-
let entityDescriptor = "";
|
|
353
|
-
if (entityMeta[key]?.sync) {
|
|
354
|
-
const [descriptorCode, descriptorImports] = generateEntityDescriptor(
|
|
355
|
-
appName,
|
|
356
|
-
modelName,
|
|
357
|
-
entityMeta[key],
|
|
358
|
-
);
|
|
359
|
-
entityDescriptor = descriptorCode;
|
|
360
|
-
modelFileImports[indexKey] = new Set([
|
|
361
|
-
...(modelFileImports[indexKey] || []),
|
|
362
|
-
...descriptorImports,
|
|
363
|
-
]);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const importData = generateImportStatements(
|
|
367
|
-
modelFileImports[key],
|
|
368
|
-
modelName,
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
const apiFileText = `${importData}\n${keyImports}\n${entityDescriptor}${modelFileData[key]}\n${updateFormMethod}`;
|
|
372
|
-
const indexImports = generateIndexImports(modelName, keysFileData[key]);
|
|
373
|
-
|
|
374
|
-
writeFileSync(apiFile, apiFileText, { flag: "w" });
|
|
375
|
-
writeFileSync(indexFile, indexImports, { flag: "w" });
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// create an index file with all the file names per model
|
|
379
|
-
Object.keys(modelImportsFileData).forEach((key) => {
|
|
380
|
-
const modelFolder = join(entitiesPath, key);
|
|
381
|
-
const indexFile = join(modelFolder, "index.ts");
|
|
382
|
-
|
|
383
|
-
const indexImports = generateFolderImports(modelImportsFileData[key]);
|
|
384
|
-
writeFileSync(indexFile, indexImports, { flag: "w" });
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export async function main(url: string, entitiesPath: string): Promise<void> {
|
|
389
|
-
console.log("Generating Entities");
|
|
390
|
-
const data = await fetchSchema(url);
|
|
391
|
-
const filteredPaths = removeNonModelPaths(data.paths);
|
|
392
|
-
await generateFolders(entitiesPath, filteredPaths);
|
|
393
|
-
await generateFiles(entitiesPath, filteredPaths);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (require.main === module) {
|
|
397
|
-
const args: string[] = process.argv.slice(2);
|
|
398
|
-
const url = args[0];
|
|
399
|
-
const entitiesPath = args[1];
|
|
400
|
-
main(url, entitiesPath);
|
|
401
|
-
}
|
|
1
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import {
|
|
4
|
+
OperationObject,
|
|
5
|
+
ParameterObject,
|
|
6
|
+
PathsObject,
|
|
7
|
+
RequestBodyObject,
|
|
8
|
+
} from "@ts-stack/openapi-spec";
|
|
9
|
+
import { extractResponseBodySchema } from "./schema/extract-response";
|
|
10
|
+
import { fetchSchema } from "./schema/fetch-schema";
|
|
11
|
+
import { removeNonModelPaths } from "./schema/remove-non-model-paths";
|
|
12
|
+
import { camelToPastel } from "./schema/camel-to-pastel";
|
|
13
|
+
import { generateGetAPIMethod } from "./entity-generation/get-method";
|
|
14
|
+
import { generateUpdateAPIMethod } from "./entity-generation/update-method";
|
|
15
|
+
import {
|
|
16
|
+
generateFolderImports,
|
|
17
|
+
generateImportStatements,
|
|
18
|
+
generateIndexImports,
|
|
19
|
+
generateKeyImports,
|
|
20
|
+
} from "./entity-generation/imports";
|
|
21
|
+
import { generateUpdateFormMethod } from "./entity-generation/form";
|
|
22
|
+
import { generateDeleteAPIMethod } from "./entity-generation/delete-method";
|
|
23
|
+
import {
|
|
24
|
+
EntitySyncMeta,
|
|
25
|
+
generateConfirmedMethod,
|
|
26
|
+
generateEntityDescriptor,
|
|
27
|
+
generateSyncDeleteMethod,
|
|
28
|
+
generateSyncListMethod,
|
|
29
|
+
generateSyncRecordMethod,
|
|
30
|
+
generateSyncUpdateMethod,
|
|
31
|
+
} from "./entity-generation/sync-method";
|
|
32
|
+
import { toCamelCase } from "./schema/to-camel-case";
|
|
33
|
+
|
|
34
|
+
function pathSegments(pathKey: string): string[] {
|
|
35
|
+
return pathKey.replace(/\/$/, "").split("/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Pre-pass over the OpenAPI paths: collects, per entity, the sync mode
|
|
40
|
+
* (from rastack's `x-rastack-sync` vendor extension), confirmed operations,
|
|
41
|
+
* the list/detail URLs, the row schema, and the list/detail operation ids
|
|
42
|
+
* (used to wire cache invalidation for non-sync entities).
|
|
43
|
+
*/
|
|
44
|
+
export function collectEntityMeta(paths: PathsObject): {
|
|
45
|
+
[indexKey: string]: EntitySyncMeta;
|
|
46
|
+
} {
|
|
47
|
+
const meta: { [indexKey: string]: EntitySyncMeta } = {};
|
|
48
|
+
|
|
49
|
+
Object.keys(paths).forEach((pathKey) => {
|
|
50
|
+
const appName = pathKey.split("/")[2];
|
|
51
|
+
const modelName = pathKey.split("/")[4];
|
|
52
|
+
const indexKey = `${appName}|${modelName}`;
|
|
53
|
+
if (!meta[indexKey]) {
|
|
54
|
+
meta[indexKey] = { sync: false, confirmedOperationIds: new Set() };
|
|
55
|
+
}
|
|
56
|
+
const entityMeta = meta[indexKey];
|
|
57
|
+
|
|
58
|
+
const segments = pathSegments(pathKey);
|
|
59
|
+
const isListPath = segments.length === 5;
|
|
60
|
+
const isDetailPath = segments.length === 6 && segments[5].startsWith("{");
|
|
61
|
+
if (isListPath) {
|
|
62
|
+
entityMeta.listUrl = pathKey;
|
|
63
|
+
}
|
|
64
|
+
if (isDetailPath) {
|
|
65
|
+
entityMeta.detailUrl = pathKey;
|
|
66
|
+
entityMeta.detailParam = segments[5].slice(1, -1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Object.keys(paths[pathKey]).forEach((method) => {
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
const operation = paths[pathKey][method] as OperationObject;
|
|
72
|
+
const syncExtension = (operation as any)["x-rastack-sync"];
|
|
73
|
+
if (syncExtension === "local" || syncExtension === "confirmed") {
|
|
74
|
+
entityMeta.sync = true;
|
|
75
|
+
}
|
|
76
|
+
if ((operation as any)["x-rastack-sync-load"] === "lazy") {
|
|
77
|
+
entityMeta.lazy = true;
|
|
78
|
+
}
|
|
79
|
+
if ((operation as any)["x-rastack-sync-client-ids"] === true) {
|
|
80
|
+
entityMeta.clientIds = true;
|
|
81
|
+
}
|
|
82
|
+
if (syncExtension === "confirmed" && operation.operationId) {
|
|
83
|
+
entityMeta.confirmedOperationIds.add(operation.operationId as string);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const [responseBodySchema, isList] =
|
|
87
|
+
extractResponseBodySchema(operation);
|
|
88
|
+
const operationId = (operation.operationId as string) || "";
|
|
89
|
+
const isPostList =
|
|
90
|
+
method === "post" &&
|
|
91
|
+
(operationId.includes("List") || operationId.includes("list"));
|
|
92
|
+
|
|
93
|
+
if (method === "get" || isPostList) {
|
|
94
|
+
if (isList || isPostList) {
|
|
95
|
+
if (isListPath && !entityMeta.listOperationId) {
|
|
96
|
+
entityMeta.listOperationId = operationId;
|
|
97
|
+
}
|
|
98
|
+
} else if (isDetailPath && !entityMeta.detailOperationId) {
|
|
99
|
+
entityMeta.detailOperationId = operationId;
|
|
100
|
+
}
|
|
101
|
+
if (responseBodySchema && !entityMeta.rowSchema) {
|
|
102
|
+
entityMeta.rowSchema = `I${responseBodySchema}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return meta;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function generateFolders(
|
|
112
|
+
entitiesPath: string,
|
|
113
|
+
paths: PathsObject,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
Object.keys(paths).forEach((key) => {
|
|
116
|
+
const appName = key.split("/")[2];
|
|
117
|
+
const modelName = key.split("/")[4];
|
|
118
|
+
const appFolder = join(entitiesPath, appName);
|
|
119
|
+
const modelFolder = join(appFolder, modelName);
|
|
120
|
+
|
|
121
|
+
mkdirSync(appFolder, { recursive: true });
|
|
122
|
+
mkdirSync(modelFolder, { recursive: true });
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function generateFiles(
|
|
127
|
+
entitiesPath: string,
|
|
128
|
+
paths: PathsObject,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const uniqueOperationNames: string[] = [];
|
|
131
|
+
const modelFileImports: { [key: string]: Set<string> } = {};
|
|
132
|
+
const modelFileData: { [key: string]: string } = {};
|
|
133
|
+
const keysFileData: { [key: string]: string[] } = {};
|
|
134
|
+
const modelImportsFileData: { [key: string]: string[] } = {};
|
|
135
|
+
const formMutationOperations: { [key: string]: any } = {};
|
|
136
|
+
const entityMeta = collectEntityMeta(paths);
|
|
137
|
+
|
|
138
|
+
Object.keys(paths).forEach((pathKey) => {
|
|
139
|
+
const appName = pathKey.split("/")[2];
|
|
140
|
+
const modelName = pathKey.split("/")[4];
|
|
141
|
+
|
|
142
|
+
const indexKey = `${appName}|${modelName}`;
|
|
143
|
+
if (!formMutationOperations[indexKey]) {
|
|
144
|
+
formMutationOperations[indexKey] = {
|
|
145
|
+
post: {
|
|
146
|
+
operationId: undefined,
|
|
147
|
+
schema: undefined,
|
|
148
|
+
},
|
|
149
|
+
put: {
|
|
150
|
+
operationId: undefined,
|
|
151
|
+
schema: undefined,
|
|
152
|
+
},
|
|
153
|
+
patch: {
|
|
154
|
+
operationId: undefined,
|
|
155
|
+
schema: undefined,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (!modelImportsFileData[appName]) {
|
|
160
|
+
modelImportsFileData[appName] = [modelName];
|
|
161
|
+
} else {
|
|
162
|
+
if (!modelImportsFileData[appName].includes(modelName)) {
|
|
163
|
+
modelImportsFileData[appName].push(modelName);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Object.keys(paths[pathKey]).forEach((method) => {
|
|
168
|
+
// @ts-ignore
|
|
169
|
+
const methodInformation = paths[pathKey][method] as OperationObject;
|
|
170
|
+
|
|
171
|
+
const methodParameters =
|
|
172
|
+
methodInformation.parameters as ParameterObject[];
|
|
173
|
+
const pathParameters = methodParameters
|
|
174
|
+
? methodParameters
|
|
175
|
+
.filter((item) => item.required)
|
|
176
|
+
.filter((item) => item.in === "path")
|
|
177
|
+
.map((item) => item.name)
|
|
178
|
+
: [];
|
|
179
|
+
|
|
180
|
+
const requestBody = methodInformation.requestBody as RequestBodyObject;
|
|
181
|
+
// @ts-ignore
|
|
182
|
+
const requestBodySchema = requestBody?.content?.[
|
|
183
|
+
"application/json"
|
|
184
|
+
]?.schema["$ref"]?.replace("#/components/schemas/", "");
|
|
185
|
+
|
|
186
|
+
let [responseBodySchema, isList] =
|
|
187
|
+
extractResponseBodySchema(methodInformation);
|
|
188
|
+
|
|
189
|
+
const requiredParameters = methodParameters
|
|
190
|
+
? methodParameters
|
|
191
|
+
.filter((item) => item.required)
|
|
192
|
+
.map((item) => item.name)
|
|
193
|
+
: [];
|
|
194
|
+
|
|
195
|
+
const operationId = methodInformation.operationId as string;
|
|
196
|
+
|
|
197
|
+
if (!uniqueOperationNames.includes(operationId)) {
|
|
198
|
+
uniqueOperationNames.push(operationId);
|
|
199
|
+
|
|
200
|
+
let apiMethods = "";
|
|
201
|
+
let importStatements = new Set<string>();
|
|
202
|
+
|
|
203
|
+
const isPostListEndpoint =
|
|
204
|
+
method === "post" &&
|
|
205
|
+
(operationId.includes("List") || operationId.includes("list"));
|
|
206
|
+
const isListEndpoint = isList || isPostListEndpoint;
|
|
207
|
+
|
|
208
|
+
const meta = entityMeta[indexKey];
|
|
209
|
+
const isSyncEntity = meta?.sync === true;
|
|
210
|
+
const isConfirmedOperation =
|
|
211
|
+
(methodInformation as any)["x-rastack-sync"] === "confirmed" &&
|
|
212
|
+
method !== "get";
|
|
213
|
+
const modelCamel = toCamelCase(modelName);
|
|
214
|
+
const listKeyRef = meta?.listOperationId
|
|
215
|
+
? `${modelCamel}QueriesKeys.${meta.listOperationId}`
|
|
216
|
+
: undefined;
|
|
217
|
+
const itemKeyRef = meta?.detailOperationId
|
|
218
|
+
? `${modelCamel}QueriesKeys.${meta.detailOperationId}`
|
|
219
|
+
: undefined;
|
|
220
|
+
|
|
221
|
+
if (isConfirmedOperation) {
|
|
222
|
+
[apiMethods, importStatements] = generateConfirmedMethod(
|
|
223
|
+
pathKey,
|
|
224
|
+
method,
|
|
225
|
+
operationId,
|
|
226
|
+
requestBodySchema ? `I${requestBodySchema}` : undefined,
|
|
227
|
+
responseBodySchema ? `I${responseBodySchema}` : undefined,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
formMutationOperations[indexKey] = {
|
|
231
|
+
...formMutationOperations[indexKey],
|
|
232
|
+
[pathKey]: {
|
|
233
|
+
[method]: {
|
|
234
|
+
operationId: camelToPastel(operationId),
|
|
235
|
+
schema: requestBodySchema ? `I${requestBodySchema}` : "",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
} else if (method === "get" || isPostListEndpoint) {
|
|
240
|
+
if (isSyncEntity && isListEndpoint) {
|
|
241
|
+
[apiMethods, importStatements] = generateSyncListMethod(
|
|
242
|
+
modelName,
|
|
243
|
+
operationId,
|
|
244
|
+
meta.rowSchema,
|
|
245
|
+
);
|
|
246
|
+
} else if (isSyncEntity && pathKey.includes("{")) {
|
|
247
|
+
const urlParams =
|
|
248
|
+
pathKey.match(/{([^}]+)}/g)?.map((param) => param.slice(1, -1)) ||
|
|
249
|
+
[];
|
|
250
|
+
[apiMethods, importStatements] = generateSyncRecordMethod(
|
|
251
|
+
modelName,
|
|
252
|
+
operationId,
|
|
253
|
+
meta.rowSchema,
|
|
254
|
+
pathParameters.length > 0 ? pathParameters : urlParams,
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
[apiMethods, importStatements] = generateGetAPIMethod(
|
|
258
|
+
pathKey,
|
|
259
|
+
modelName,
|
|
260
|
+
operationId,
|
|
261
|
+
responseBodySchema ? `I${responseBodySchema}` : undefined,
|
|
262
|
+
isListEndpoint,
|
|
263
|
+
pathParameters,
|
|
264
|
+
requiredParameters,
|
|
265
|
+
method,
|
|
266
|
+
requestBodySchema !== undefined ? requestBodySchema : "",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
} else if (
|
|
270
|
+
method === "post" ||
|
|
271
|
+
method === "put" ||
|
|
272
|
+
method === "patch"
|
|
273
|
+
) {
|
|
274
|
+
if (isSyncEntity) {
|
|
275
|
+
[apiMethods, importStatements] = generateSyncUpdateMethod(
|
|
276
|
+
pathKey,
|
|
277
|
+
method,
|
|
278
|
+
modelName,
|
|
279
|
+
operationId,
|
|
280
|
+
requestBodySchema ? `I${requestBodySchema}` : undefined,
|
|
281
|
+
responseBodySchema ? `I${responseBodySchema}` : undefined,
|
|
282
|
+
pathParameters,
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
[apiMethods, importStatements] = generateUpdateAPIMethod(
|
|
286
|
+
pathKey,
|
|
287
|
+
method,
|
|
288
|
+
operationId,
|
|
289
|
+
requestBodySchema ? `I${requestBodySchema}` : undefined,
|
|
290
|
+
responseBodySchema ? `I${responseBodySchema}` : undefined,
|
|
291
|
+
listKeyRef,
|
|
292
|
+
itemKeyRef,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
formMutationOperations[indexKey] = {
|
|
297
|
+
...formMutationOperations[indexKey],
|
|
298
|
+
[pathKey]: {
|
|
299
|
+
[method]: {
|
|
300
|
+
operationId: camelToPastel(operationId),
|
|
301
|
+
schema: requestBodySchema ? `I${requestBodySchema}` : "",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
} else if (method === "delete") {
|
|
306
|
+
if (isSyncEntity) {
|
|
307
|
+
[apiMethods, importStatements] = generateSyncDeleteMethod(
|
|
308
|
+
pathKey,
|
|
309
|
+
modelName,
|
|
310
|
+
operationId,
|
|
311
|
+
);
|
|
312
|
+
} else {
|
|
313
|
+
[apiMethods, importStatements] = generateDeleteAPIMethod(
|
|
314
|
+
pathKey,
|
|
315
|
+
operationId,
|
|
316
|
+
listKeyRef,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
modelFileData[indexKey] = (modelFileData[indexKey] || "") + apiMethods;
|
|
322
|
+
modelFileImports[indexKey] = new Set([
|
|
323
|
+
...(modelFileImports[indexKey] || []),
|
|
324
|
+
...importStatements,
|
|
325
|
+
]);
|
|
326
|
+
keysFileData[indexKey] = [
|
|
327
|
+
...(keysFileData[indexKey] || []),
|
|
328
|
+
operationId,
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
Object.keys(modelFileData).forEach((key) => {
|
|
335
|
+
const [appName, modelName] = key.split("|");
|
|
336
|
+
const modelFolder = join(entitiesPath, appName, modelName);
|
|
337
|
+
const apiFile = join(modelFolder, "api.ts");
|
|
338
|
+
const indexFile = join(modelFolder, "index.ts");
|
|
339
|
+
const keyImports = generateKeyImports(modelName, keysFileData[key]);
|
|
340
|
+
|
|
341
|
+
const [updateFormMethod, importStatements] = generateUpdateFormMethod(
|
|
342
|
+
appName,
|
|
343
|
+
modelName,
|
|
344
|
+
formMutationOperations[key],
|
|
345
|
+
);
|
|
346
|
+
const indexKey = `${appName}|${modelName}`;
|
|
347
|
+
modelFileImports[indexKey] = new Set([
|
|
348
|
+
...(modelFileImports[indexKey] || []),
|
|
349
|
+
...importStatements,
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
let entityDescriptor = "";
|
|
353
|
+
if (entityMeta[key]?.sync) {
|
|
354
|
+
const [descriptorCode, descriptorImports] = generateEntityDescriptor(
|
|
355
|
+
appName,
|
|
356
|
+
modelName,
|
|
357
|
+
entityMeta[key],
|
|
358
|
+
);
|
|
359
|
+
entityDescriptor = descriptorCode;
|
|
360
|
+
modelFileImports[indexKey] = new Set([
|
|
361
|
+
...(modelFileImports[indexKey] || []),
|
|
362
|
+
...descriptorImports,
|
|
363
|
+
]);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const importData = generateImportStatements(
|
|
367
|
+
modelFileImports[key],
|
|
368
|
+
modelName,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const apiFileText = `${importData}\n${keyImports}\n${entityDescriptor}${modelFileData[key]}\n${updateFormMethod}`;
|
|
372
|
+
const indexImports = generateIndexImports(modelName, keysFileData[key]);
|
|
373
|
+
|
|
374
|
+
writeFileSync(apiFile, apiFileText, { flag: "w" });
|
|
375
|
+
writeFileSync(indexFile, indexImports, { flag: "w" });
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// create an index file with all the file names per model
|
|
379
|
+
Object.keys(modelImportsFileData).forEach((key) => {
|
|
380
|
+
const modelFolder = join(entitiesPath, key);
|
|
381
|
+
const indexFile = join(modelFolder, "index.ts");
|
|
382
|
+
|
|
383
|
+
const indexImports = generateFolderImports(modelImportsFileData[key]);
|
|
384
|
+
writeFileSync(indexFile, indexImports, { flag: "w" });
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function main(url: string, entitiesPath: string): Promise<void> {
|
|
389
|
+
console.log("Generating Entities");
|
|
390
|
+
const data = await fetchSchema(url);
|
|
391
|
+
const filteredPaths = removeNonModelPaths(data.paths);
|
|
392
|
+
await generateFolders(entitiesPath, filteredPaths);
|
|
393
|
+
await generateFiles(entitiesPath, filteredPaths);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (require.main === module) {
|
|
397
|
+
const args: string[] = process.argv.slice(2);
|
|
398
|
+
const url = args[0];
|
|
399
|
+
const entitiesPath = args[1];
|
|
400
|
+
main(url, entitiesPath);
|
|
401
|
+
}
|