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/compile/model.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The canonical, backend-agnostic data model produced by the compiler. This is
|
|
3
|
-
* what `schema.rad.json` serializes and what both the OpenAPI emitter and the
|
|
4
|
-
* Rust API consume.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export type ScalarType =
|
|
8
|
-
| "string"
|
|
9
|
-
| "int"
|
|
10
|
-
| "float"
|
|
11
|
-
| "bool"
|
|
12
|
-
| "datetime"
|
|
13
|
-
| "uuid";
|
|
14
|
-
|
|
15
|
-
export type FieldType = ScalarType | "fk";
|
|
16
|
-
|
|
17
|
-
export interface RelationTarget {
|
|
18
|
-
app: string;
|
|
19
|
-
model: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface FieldModel {
|
|
23
|
-
name: string;
|
|
24
|
-
type: FieldType;
|
|
25
|
-
maxLength?: number;
|
|
26
|
-
unique?: boolean;
|
|
27
|
-
primaryKey?: boolean;
|
|
28
|
-
null?: boolean;
|
|
29
|
-
default?: unknown;
|
|
30
|
-
/** Present iff `type === "fk"` — the resource this field points at. */
|
|
31
|
-
relation?: RelationTarget;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface RelationModel {
|
|
35
|
-
/** The field on *this* resource that holds the foreign key. */
|
|
36
|
-
field: string;
|
|
37
|
-
/** `"app.model"` of the referenced resource. */
|
|
38
|
-
target: string;
|
|
39
|
-
kind: "fk";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface SyncModel {
|
|
43
|
-
mode: "server" | "local";
|
|
44
|
-
scope?: string;
|
|
45
|
-
confirmed?: string[];
|
|
46
|
-
load?: "instant" | "lazy";
|
|
47
|
-
clientIds?: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface AdminModel {
|
|
51
|
-
listDisplay?: string[];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface ResourceModel {
|
|
55
|
-
app: string;
|
|
56
|
-
model: string;
|
|
57
|
-
fields: FieldModel[];
|
|
58
|
-
relations: RelationModel[];
|
|
59
|
-
sync?: SyncModel;
|
|
60
|
-
admin?: AdminModel | false;
|
|
61
|
-
search?: string[];
|
|
62
|
-
ordering?: string[];
|
|
63
|
-
permission?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface Manifest {
|
|
67
|
-
version: 1;
|
|
68
|
-
resources: ResourceModel[];
|
|
69
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The canonical, backend-agnostic data model produced by the compiler. This is
|
|
3
|
+
* what `schema.rad.json` serializes and what both the OpenAPI emitter and the
|
|
4
|
+
* Rust API consume.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type ScalarType =
|
|
8
|
+
| "string"
|
|
9
|
+
| "int"
|
|
10
|
+
| "float"
|
|
11
|
+
| "bool"
|
|
12
|
+
| "datetime"
|
|
13
|
+
| "uuid";
|
|
14
|
+
|
|
15
|
+
export type FieldType = ScalarType | "fk";
|
|
16
|
+
|
|
17
|
+
export interface RelationTarget {
|
|
18
|
+
app: string;
|
|
19
|
+
model: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FieldModel {
|
|
23
|
+
name: string;
|
|
24
|
+
type: FieldType;
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
unique?: boolean;
|
|
27
|
+
primaryKey?: boolean;
|
|
28
|
+
null?: boolean;
|
|
29
|
+
default?: unknown;
|
|
30
|
+
/** Present iff `type === "fk"` — the resource this field points at. */
|
|
31
|
+
relation?: RelationTarget;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RelationModel {
|
|
35
|
+
/** The field on *this* resource that holds the foreign key. */
|
|
36
|
+
field: string;
|
|
37
|
+
/** `"app.model"` of the referenced resource. */
|
|
38
|
+
target: string;
|
|
39
|
+
kind: "fk";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SyncModel {
|
|
43
|
+
mode: "server" | "local";
|
|
44
|
+
scope?: string;
|
|
45
|
+
confirmed?: string[];
|
|
46
|
+
load?: "instant" | "lazy";
|
|
47
|
+
clientIds?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AdminModel {
|
|
51
|
+
listDisplay?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ResourceModel {
|
|
55
|
+
app: string;
|
|
56
|
+
model: string;
|
|
57
|
+
fields: FieldModel[];
|
|
58
|
+
relations: RelationModel[];
|
|
59
|
+
sync?: SyncModel;
|
|
60
|
+
admin?: AdminModel | false;
|
|
61
|
+
search?: string[];
|
|
62
|
+
ordering?: string[];
|
|
63
|
+
permission?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface Manifest {
|
|
67
|
+
version: 1;
|
|
68
|
+
resources: ResourceModel[];
|
|
69
|
+
}
|
package/src/compile/openapi.ts
CHANGED
|
@@ -1,266 +1,266 @@
|
|
|
1
|
-
import { FieldModel, ResourceModel } from "./model";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Emit an OpenAPI 3 document from the analysed resources. The output reproduces
|
|
5
|
-
* the *exact* contract the existing `tools/` codegen and `stack/` runtime rely
|
|
6
|
-
* on — `/api/{app}/v1/{model}/` URL segments and the `x-rastack-sync*` vendor
|
|
7
|
-
* extensions — so those packages are unchanged. Foreign keys additionally carry
|
|
8
|
-
* a new `x-rastack-relation` extension so downstream tooling (and the admin) can
|
|
9
|
-
* follow relations.
|
|
10
|
-
*/
|
|
11
|
-
export function buildOpenApi(resources: ResourceModel[]): any {
|
|
12
|
-
const paths: Record<string, any> = {};
|
|
13
|
-
const schemas: Record<string, any> = {};
|
|
14
|
-
|
|
15
|
-
for (const resource of resources) {
|
|
16
|
-
const P = pascal(resource.model);
|
|
17
|
-
const listUrl = `/api/${resource.app}/v1/${resource.model}/`;
|
|
18
|
-
const detailUrl = `${listUrl}{id}/`;
|
|
19
|
-
const local = resource.sync?.mode === "local";
|
|
20
|
-
|
|
21
|
-
schemas[P] = readSchema(resource);
|
|
22
|
-
schemas[`Paginated${P}List`] = paginatedSchema(P);
|
|
23
|
-
schemas[`${P}Request`] = writeSchema(resource, false);
|
|
24
|
-
schemas[`Patched${P}Request`] = writeSchema(resource, true);
|
|
25
|
-
|
|
26
|
-
paths[listUrl] = {
|
|
27
|
-
get: syncStamp(
|
|
28
|
-
{
|
|
29
|
-
operationId: `list${P}`,
|
|
30
|
-
parameters: [
|
|
31
|
-
queryParam("page", "integer"),
|
|
32
|
-
queryParam("search", "string"),
|
|
33
|
-
],
|
|
34
|
-
responses: jsonResponse("200", `Paginated${P}List`),
|
|
35
|
-
},
|
|
36
|
-
resource,
|
|
37
|
-
"list",
|
|
38
|
-
),
|
|
39
|
-
post: syncStamp(
|
|
40
|
-
{
|
|
41
|
-
operationId: `create${P}`,
|
|
42
|
-
requestBody: jsonBody(`${P}Request`),
|
|
43
|
-
responses: jsonResponse("201", P),
|
|
44
|
-
},
|
|
45
|
-
resource,
|
|
46
|
-
"create",
|
|
47
|
-
),
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
paths[detailUrl] = {
|
|
51
|
-
get: syncStamp(
|
|
52
|
-
{
|
|
53
|
-
operationId: `get${P}ById`,
|
|
54
|
-
parameters: [idParam()],
|
|
55
|
-
responses: jsonResponse("200", P),
|
|
56
|
-
},
|
|
57
|
-
resource,
|
|
58
|
-
"retrieve",
|
|
59
|
-
),
|
|
60
|
-
put: syncStamp(
|
|
61
|
-
{
|
|
62
|
-
operationId: `update${P}`,
|
|
63
|
-
parameters: [idParam()],
|
|
64
|
-
requestBody: jsonBody(`${P}Request`),
|
|
65
|
-
responses: jsonResponse("200", P),
|
|
66
|
-
},
|
|
67
|
-
resource,
|
|
68
|
-
"update",
|
|
69
|
-
),
|
|
70
|
-
patch: syncStamp(
|
|
71
|
-
{
|
|
72
|
-
operationId: `partialUpdate${P}`,
|
|
73
|
-
parameters: [idParam()],
|
|
74
|
-
requestBody: jsonBody(`Patched${P}Request`),
|
|
75
|
-
responses: jsonResponse("200", P),
|
|
76
|
-
},
|
|
77
|
-
resource,
|
|
78
|
-
"partial_update",
|
|
79
|
-
),
|
|
80
|
-
delete: syncStamp(
|
|
81
|
-
{
|
|
82
|
-
operationId: `delete${P}`,
|
|
83
|
-
parameters: [idParam()],
|
|
84
|
-
responses: { "204": { description: "No response body" } },
|
|
85
|
-
},
|
|
86
|
-
resource,
|
|
87
|
-
"destroy",
|
|
88
|
-
),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
openapi: "3.0.3",
|
|
94
|
-
info: { title: "React API Stack", version: "1.0.0" },
|
|
95
|
-
paths,
|
|
96
|
-
components: { schemas },
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function openApiToJson(doc: any): string {
|
|
101
|
-
return JSON.stringify(doc, null, 2) + "\n";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// -- schema builders --------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
function readSchema(resource: ResourceModel): any {
|
|
107
|
-
const properties: Record<string, any> = {
|
|
108
|
-
id: idProperty(resource),
|
|
109
|
-
};
|
|
110
|
-
const required: string[] = [];
|
|
111
|
-
for (const field of resource.fields) {
|
|
112
|
-
properties[field.name] = fieldProperty(field);
|
|
113
|
-
if (!field.null && field.default === undefined) required.push(field.name);
|
|
114
|
-
}
|
|
115
|
-
return { type: "object", properties, required };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function writeSchema(resource: ResourceModel, patched: boolean): any {
|
|
119
|
-
const properties: Record<string, any> = {};
|
|
120
|
-
const required: string[] = [];
|
|
121
|
-
for (const field of resource.fields) {
|
|
122
|
-
if (field.primaryKey) continue;
|
|
123
|
-
properties[field.name] = fieldProperty(field);
|
|
124
|
-
if (!patched && !field.null && field.default === undefined) {
|
|
125
|
-
required.push(field.name);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const schema: any = { type: "object", properties };
|
|
129
|
-
if (required.length) schema.required = required;
|
|
130
|
-
return schema;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function paginatedSchema(pascalModel: string): any {
|
|
134
|
-
return {
|
|
135
|
-
type: "object",
|
|
136
|
-
properties: {
|
|
137
|
-
count: { type: "integer" },
|
|
138
|
-
next: { type: "string", nullable: true },
|
|
139
|
-
previous: { type: "string", nullable: true },
|
|
140
|
-
results: {
|
|
141
|
-
type: "array",
|
|
142
|
-
items: { $ref: `#/components/schemas/${pascalModel}` },
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
required: ["count", "results"],
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function fieldProperty(field: FieldModel): any {
|
|
150
|
-
if (field.type === "fk") {
|
|
151
|
-
return {
|
|
152
|
-
type: "integer",
|
|
153
|
-
"x-rastack-relation": field.relation,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
const base = scalarProperty(field.type);
|
|
157
|
-
if (field.maxLength !== undefined) base.maxLength = field.maxLength;
|
|
158
|
-
if (field.null) base.nullable = true;
|
|
159
|
-
return base;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function idProperty(resource: ResourceModel): any {
|
|
163
|
-
const pk = resource.fields.find((f) => f.primaryKey);
|
|
164
|
-
if (pk) return { ...scalarProperty(pk.type), readOnly: true };
|
|
165
|
-
if (resource.sync?.clientIds) {
|
|
166
|
-
return { type: "string", format: "uuid" };
|
|
167
|
-
}
|
|
168
|
-
return { type: "integer", readOnly: true };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function scalarProperty(type: string): any {
|
|
172
|
-
switch (type) {
|
|
173
|
-
case "int":
|
|
174
|
-
return { type: "integer" };
|
|
175
|
-
case "float":
|
|
176
|
-
return { type: "number" };
|
|
177
|
-
case "bool":
|
|
178
|
-
return { type: "boolean" };
|
|
179
|
-
case "datetime":
|
|
180
|
-
return { type: "string", format: "date-time" };
|
|
181
|
-
case "uuid":
|
|
182
|
-
return { type: "string", format: "uuid" };
|
|
183
|
-
default:
|
|
184
|
-
return { type: "string" };
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// -- operation helpers ------------------------------------------------------
|
|
189
|
-
|
|
190
|
-
function jsonResponse(status: string, schemaName: string): any {
|
|
191
|
-
return {
|
|
192
|
-
[status]: {
|
|
193
|
-
description: "",
|
|
194
|
-
content: {
|
|
195
|
-
"application/json": {
|
|
196
|
-
schema: { $ref: `#/components/schemas/${schemaName}` },
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function jsonBody(schemaName: string): any {
|
|
204
|
-
return {
|
|
205
|
-
required: true,
|
|
206
|
-
content: {
|
|
207
|
-
"application/json": {
|
|
208
|
-
schema: { $ref: `#/components/schemas/${schemaName}` },
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function idParam(): any {
|
|
215
|
-
return {
|
|
216
|
-
name: "id",
|
|
217
|
-
in: "path",
|
|
218
|
-
required: true,
|
|
219
|
-
schema: { type: "string" },
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function queryParam(name: string, type: string): any {
|
|
224
|
-
return { name, in: "query", required: false, schema: { type } };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Stamp the `x-rastack-sync*` vendor extensions on an operation of a local-mode
|
|
229
|
-
* resource. Ported from the old `python/rastack/spectacular.py` postprocess
|
|
230
|
-
* hook: confirmed operations become blocking client hooks, everything else on a
|
|
231
|
-
* local resource replicates.
|
|
232
|
-
*/
|
|
233
|
-
function syncStamp(operation: any, resource: ResourceModel, verb: string): any {
|
|
234
|
-
const sync = resource.sync;
|
|
235
|
-
if (!sync || sync.mode !== "local") return operation;
|
|
236
|
-
|
|
237
|
-
operation["x-rastack-sync"] = isConfirmed(sync.confirmed, verb, operation)
|
|
238
|
-
? "confirmed"
|
|
239
|
-
: "local";
|
|
240
|
-
if (sync.load === "lazy") operation["x-rastack-sync-load"] = "lazy";
|
|
241
|
-
if (sync.clientIds) operation["x-rastack-sync-client-ids"] = true;
|
|
242
|
-
return operation;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function isConfirmed(
|
|
246
|
-
confirmed: string[] | undefined,
|
|
247
|
-
verb: string,
|
|
248
|
-
operation: any,
|
|
249
|
-
): boolean {
|
|
250
|
-
if (!confirmed || !confirmed.length) return false;
|
|
251
|
-
for (const name of confirmed) {
|
|
252
|
-
if (name === verb) return true;
|
|
253
|
-
if (name === "update" && verb === "partial_update") return true;
|
|
254
|
-
if (operation.operationId && operation.operationId.includes(name)) {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function pascal(str: string): string {
|
|
262
|
-
return str
|
|
263
|
-
.split(/[-_]/)
|
|
264
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
265
|
-
.join("");
|
|
266
|
-
}
|
|
1
|
+
import { FieldModel, ResourceModel } from "./model";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Emit an OpenAPI 3 document from the analysed resources. The output reproduces
|
|
5
|
+
* the *exact* contract the existing `tools/` codegen and `stack/` runtime rely
|
|
6
|
+
* on — `/api/{app}/v1/{model}/` URL segments and the `x-rastack-sync*` vendor
|
|
7
|
+
* extensions — so those packages are unchanged. Foreign keys additionally carry
|
|
8
|
+
* a new `x-rastack-relation` extension so downstream tooling (and the admin) can
|
|
9
|
+
* follow relations.
|
|
10
|
+
*/
|
|
11
|
+
export function buildOpenApi(resources: ResourceModel[]): any {
|
|
12
|
+
const paths: Record<string, any> = {};
|
|
13
|
+
const schemas: Record<string, any> = {};
|
|
14
|
+
|
|
15
|
+
for (const resource of resources) {
|
|
16
|
+
const P = pascal(resource.model);
|
|
17
|
+
const listUrl = `/api/${resource.app}/v1/${resource.model}/`;
|
|
18
|
+
const detailUrl = `${listUrl}{id}/`;
|
|
19
|
+
const local = resource.sync?.mode === "local";
|
|
20
|
+
|
|
21
|
+
schemas[P] = readSchema(resource);
|
|
22
|
+
schemas[`Paginated${P}List`] = paginatedSchema(P);
|
|
23
|
+
schemas[`${P}Request`] = writeSchema(resource, false);
|
|
24
|
+
schemas[`Patched${P}Request`] = writeSchema(resource, true);
|
|
25
|
+
|
|
26
|
+
paths[listUrl] = {
|
|
27
|
+
get: syncStamp(
|
|
28
|
+
{
|
|
29
|
+
operationId: `list${P}`,
|
|
30
|
+
parameters: [
|
|
31
|
+
queryParam("page", "integer"),
|
|
32
|
+
queryParam("search", "string"),
|
|
33
|
+
],
|
|
34
|
+
responses: jsonResponse("200", `Paginated${P}List`),
|
|
35
|
+
},
|
|
36
|
+
resource,
|
|
37
|
+
"list",
|
|
38
|
+
),
|
|
39
|
+
post: syncStamp(
|
|
40
|
+
{
|
|
41
|
+
operationId: `create${P}`,
|
|
42
|
+
requestBody: jsonBody(`${P}Request`),
|
|
43
|
+
responses: jsonResponse("201", P),
|
|
44
|
+
},
|
|
45
|
+
resource,
|
|
46
|
+
"create",
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
paths[detailUrl] = {
|
|
51
|
+
get: syncStamp(
|
|
52
|
+
{
|
|
53
|
+
operationId: `get${P}ById`,
|
|
54
|
+
parameters: [idParam()],
|
|
55
|
+
responses: jsonResponse("200", P),
|
|
56
|
+
},
|
|
57
|
+
resource,
|
|
58
|
+
"retrieve",
|
|
59
|
+
),
|
|
60
|
+
put: syncStamp(
|
|
61
|
+
{
|
|
62
|
+
operationId: `update${P}`,
|
|
63
|
+
parameters: [idParam()],
|
|
64
|
+
requestBody: jsonBody(`${P}Request`),
|
|
65
|
+
responses: jsonResponse("200", P),
|
|
66
|
+
},
|
|
67
|
+
resource,
|
|
68
|
+
"update",
|
|
69
|
+
),
|
|
70
|
+
patch: syncStamp(
|
|
71
|
+
{
|
|
72
|
+
operationId: `partialUpdate${P}`,
|
|
73
|
+
parameters: [idParam()],
|
|
74
|
+
requestBody: jsonBody(`Patched${P}Request`),
|
|
75
|
+
responses: jsonResponse("200", P),
|
|
76
|
+
},
|
|
77
|
+
resource,
|
|
78
|
+
"partial_update",
|
|
79
|
+
),
|
|
80
|
+
delete: syncStamp(
|
|
81
|
+
{
|
|
82
|
+
operationId: `delete${P}`,
|
|
83
|
+
parameters: [idParam()],
|
|
84
|
+
responses: { "204": { description: "No response body" } },
|
|
85
|
+
},
|
|
86
|
+
resource,
|
|
87
|
+
"destroy",
|
|
88
|
+
),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
openapi: "3.0.3",
|
|
94
|
+
info: { title: "React API Stack", version: "1.0.0" },
|
|
95
|
+
paths,
|
|
96
|
+
components: { schemas },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function openApiToJson(doc: any): string {
|
|
101
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// -- schema builders --------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function readSchema(resource: ResourceModel): any {
|
|
107
|
+
const properties: Record<string, any> = {
|
|
108
|
+
id: idProperty(resource),
|
|
109
|
+
};
|
|
110
|
+
const required: string[] = [];
|
|
111
|
+
for (const field of resource.fields) {
|
|
112
|
+
properties[field.name] = fieldProperty(field);
|
|
113
|
+
if (!field.null && field.default === undefined) required.push(field.name);
|
|
114
|
+
}
|
|
115
|
+
return { type: "object", properties, required };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeSchema(resource: ResourceModel, patched: boolean): any {
|
|
119
|
+
const properties: Record<string, any> = {};
|
|
120
|
+
const required: string[] = [];
|
|
121
|
+
for (const field of resource.fields) {
|
|
122
|
+
if (field.primaryKey) continue;
|
|
123
|
+
properties[field.name] = fieldProperty(field);
|
|
124
|
+
if (!patched && !field.null && field.default === undefined) {
|
|
125
|
+
required.push(field.name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const schema: any = { type: "object", properties };
|
|
129
|
+
if (required.length) schema.required = required;
|
|
130
|
+
return schema;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function paginatedSchema(pascalModel: string): any {
|
|
134
|
+
return {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
count: { type: "integer" },
|
|
138
|
+
next: { type: "string", nullable: true },
|
|
139
|
+
previous: { type: "string", nullable: true },
|
|
140
|
+
results: {
|
|
141
|
+
type: "array",
|
|
142
|
+
items: { $ref: `#/components/schemas/${pascalModel}` },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ["count", "results"],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function fieldProperty(field: FieldModel): any {
|
|
150
|
+
if (field.type === "fk") {
|
|
151
|
+
return {
|
|
152
|
+
type: "integer",
|
|
153
|
+
"x-rastack-relation": field.relation,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const base = scalarProperty(field.type);
|
|
157
|
+
if (field.maxLength !== undefined) base.maxLength = field.maxLength;
|
|
158
|
+
if (field.null) base.nullable = true;
|
|
159
|
+
return base;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function idProperty(resource: ResourceModel): any {
|
|
163
|
+
const pk = resource.fields.find((f) => f.primaryKey);
|
|
164
|
+
if (pk) return { ...scalarProperty(pk.type), readOnly: true };
|
|
165
|
+
if (resource.sync?.clientIds) {
|
|
166
|
+
return { type: "string", format: "uuid" };
|
|
167
|
+
}
|
|
168
|
+
return { type: "integer", readOnly: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function scalarProperty(type: string): any {
|
|
172
|
+
switch (type) {
|
|
173
|
+
case "int":
|
|
174
|
+
return { type: "integer" };
|
|
175
|
+
case "float":
|
|
176
|
+
return { type: "number" };
|
|
177
|
+
case "bool":
|
|
178
|
+
return { type: "boolean" };
|
|
179
|
+
case "datetime":
|
|
180
|
+
return { type: "string", format: "date-time" };
|
|
181
|
+
case "uuid":
|
|
182
|
+
return { type: "string", format: "uuid" };
|
|
183
|
+
default:
|
|
184
|
+
return { type: "string" };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// -- operation helpers ------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
function jsonResponse(status: string, schemaName: string): any {
|
|
191
|
+
return {
|
|
192
|
+
[status]: {
|
|
193
|
+
description: "",
|
|
194
|
+
content: {
|
|
195
|
+
"application/json": {
|
|
196
|
+
schema: { $ref: `#/components/schemas/${schemaName}` },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function jsonBody(schemaName: string): any {
|
|
204
|
+
return {
|
|
205
|
+
required: true,
|
|
206
|
+
content: {
|
|
207
|
+
"application/json": {
|
|
208
|
+
schema: { $ref: `#/components/schemas/${schemaName}` },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function idParam(): any {
|
|
215
|
+
return {
|
|
216
|
+
name: "id",
|
|
217
|
+
in: "path",
|
|
218
|
+
required: true,
|
|
219
|
+
schema: { type: "string" },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function queryParam(name: string, type: string): any {
|
|
224
|
+
return { name, in: "query", required: false, schema: { type } };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Stamp the `x-rastack-sync*` vendor extensions on an operation of a local-mode
|
|
229
|
+
* resource. Ported from the old `python/rastack/spectacular.py` postprocess
|
|
230
|
+
* hook: confirmed operations become blocking client hooks, everything else on a
|
|
231
|
+
* local resource replicates.
|
|
232
|
+
*/
|
|
233
|
+
function syncStamp(operation: any, resource: ResourceModel, verb: string): any {
|
|
234
|
+
const sync = resource.sync;
|
|
235
|
+
if (!sync || sync.mode !== "local") return operation;
|
|
236
|
+
|
|
237
|
+
operation["x-rastack-sync"] = isConfirmed(sync.confirmed, verb, operation)
|
|
238
|
+
? "confirmed"
|
|
239
|
+
: "local";
|
|
240
|
+
if (sync.load === "lazy") operation["x-rastack-sync-load"] = "lazy";
|
|
241
|
+
if (sync.clientIds) operation["x-rastack-sync-client-ids"] = true;
|
|
242
|
+
return operation;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isConfirmed(
|
|
246
|
+
confirmed: string[] | undefined,
|
|
247
|
+
verb: string,
|
|
248
|
+
operation: any,
|
|
249
|
+
): boolean {
|
|
250
|
+
if (!confirmed || !confirmed.length) return false;
|
|
251
|
+
for (const name of confirmed) {
|
|
252
|
+
if (name === verb) return true;
|
|
253
|
+
if (name === "update" && verb === "partial_update") return true;
|
|
254
|
+
if (operation.operationId && operation.operationId.includes(name)) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function pascal(str: string): string {
|
|
262
|
+
return str
|
|
263
|
+
.split(/[-_]/)
|
|
264
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
265
|
+
.join("");
|
|
266
|
+
}
|