rastack 0.0.16 → 0.0.20
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 +43876 -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/analyze.ts
CHANGED
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
import * as ts from "typescript";
|
|
2
|
-
import {
|
|
3
|
-
AdminModel,
|
|
4
|
-
FieldModel,
|
|
5
|
-
RelationModel,
|
|
6
|
-
ResourceModel,
|
|
7
|
-
ScalarType,
|
|
8
|
-
SyncModel,
|
|
9
|
-
} from "./model";
|
|
10
|
-
import { resourceSourceFiles } from "./program";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Statically analyse resource definitions and produce the canonical model.
|
|
14
|
-
*
|
|
15
|
-
* The headline behaviour is **foreign-key inference from types**: for every
|
|
16
|
-
* field we ask the `TypeChecker` for the field's type and, if that type is a
|
|
17
|
-
* `Ref<App, Model>`, we recover the relation target from the `__app`/`__model`
|
|
18
|
-
* string-literal type arguments. No runtime execution, no string heuristics on
|
|
19
|
-
* the value — the relation is read straight out of the type system.
|
|
20
|
-
*/
|
|
21
|
-
export function analyze(
|
|
22
|
-
program: ts.Program,
|
|
23
|
-
fileNames: string[],
|
|
24
|
-
): ResourceModel[] {
|
|
25
|
-
const checker = program.getTypeChecker();
|
|
26
|
-
const resources: ResourceModel[] = [];
|
|
27
|
-
|
|
28
|
-
for (const sourceFile of resourceSourceFiles(program, fileNames)) {
|
|
29
|
-
ts.forEachChild(sourceFile, function walk(node) {
|
|
30
|
-
if (isResourceCall(node)) {
|
|
31
|
-
const model = analyzeResourceCall(node, checker);
|
|
32
|
-
if (model) resources.push(model);
|
|
33
|
-
}
|
|
34
|
-
ts.forEachChild(node, walk);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Deterministic order: app, then model.
|
|
39
|
-
resources.sort((a, b) =>
|
|
40
|
-
a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app),
|
|
41
|
-
);
|
|
42
|
-
return resources;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isResourceCall(node: ts.Node): node is ts.CallExpression {
|
|
46
|
-
return (
|
|
47
|
-
ts.isCallExpression(node) &&
|
|
48
|
-
ts.isIdentifier(node.expression) &&
|
|
49
|
-
node.expression.text === "resource" &&
|
|
50
|
-
node.arguments.length >= 3
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function analyzeResourceCall(
|
|
55
|
-
call: ts.CallExpression,
|
|
56
|
-
checker: ts.TypeChecker,
|
|
57
|
-
): ResourceModel | undefined {
|
|
58
|
-
const app = stringLiteralArg(call.arguments[0]);
|
|
59
|
-
const model = stringLiteralArg(call.arguments[1]);
|
|
60
|
-
const fieldsArg = call.arguments[2];
|
|
61
|
-
if (!app || !model || !fieldsArg || !ts.isObjectLiteralExpression(fieldsArg)) {
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const fields: FieldModel[] = [];
|
|
66
|
-
const relations: RelationModel[] = [];
|
|
67
|
-
|
|
68
|
-
for (const prop of fieldsArg.properties) {
|
|
69
|
-
if (!ts.isPropertyAssignment(prop)) continue;
|
|
70
|
-
const name = propertyName(prop.name);
|
|
71
|
-
if (!name) continue;
|
|
72
|
-
|
|
73
|
-
const field = analyzeField(name, prop.initializer, checker);
|
|
74
|
-
if (!field) continue;
|
|
75
|
-
fields.push(field);
|
|
76
|
-
if (field.type === "fk" && field.relation) {
|
|
77
|
-
relations.push({
|
|
78
|
-
field: name,
|
|
79
|
-
target: `${field.relation.app}.${field.relation.model}`,
|
|
80
|
-
kind: "fk",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const options = literalToValue(call.arguments[3]) ?? {};
|
|
86
|
-
return {
|
|
87
|
-
app,
|
|
88
|
-
model,
|
|
89
|
-
fields,
|
|
90
|
-
relations,
|
|
91
|
-
sync: normaliseSync(options.sync),
|
|
92
|
-
admin: normaliseAdmin(options.admin),
|
|
93
|
-
search: options.search,
|
|
94
|
-
ordering: options.ordering,
|
|
95
|
-
permission: options.permission,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Resolve a single field from its initializer. FK detection is type-driven:
|
|
101
|
-
* the initializer's *type* is inspected, and a `Ref` type yields a foreign key.
|
|
102
|
-
*/
|
|
103
|
-
function analyzeField(
|
|
104
|
-
name: string,
|
|
105
|
-
initializer: ts.Expression,
|
|
106
|
-
checker: ts.TypeChecker,
|
|
107
|
-
): FieldModel | undefined {
|
|
108
|
-
const fieldType = checker.getTypeAtLocation(initializer);
|
|
109
|
-
|
|
110
|
-
// Foreign key: the field type is Ref<App, Model>. Recover the target from
|
|
111
|
-
// the string-literal type arguments — this is the "infer FK from types" step.
|
|
112
|
-
if (fieldType.getProperty("__radRef")) {
|
|
113
|
-
const app = literalTypeProp(fieldType, "__app", checker, initializer);
|
|
114
|
-
const model = literalTypeProp(fieldType, "__model", checker, initializer);
|
|
115
|
-
if (app && model) {
|
|
116
|
-
return { name, type: "fk", relation: { app, model } };
|
|
117
|
-
}
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Scalar: the field type carries a `__radScalar` string-literal discriminant.
|
|
122
|
-
const scalar = literalTypeProp(fieldType, "__radScalar", checker, initializer);
|
|
123
|
-
if (!scalar) return undefined;
|
|
124
|
-
|
|
125
|
-
const field: FieldModel = { name, type: scalar as ScalarType };
|
|
126
|
-
applyConstraints(field, initializer);
|
|
127
|
-
return field;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Read scalar constraints (maxLength, unique, …) from the builder call args. */
|
|
131
|
-
function applyConstraints(field: FieldModel, initializer: ts.Expression): void {
|
|
132
|
-
if (!ts.isCallExpression(initializer)) return;
|
|
133
|
-
const opts = literalToValue(initializer.arguments[0]);
|
|
134
|
-
if (!opts || typeof opts !== "object") return;
|
|
135
|
-
if (typeof opts.maxLength === "number") field.maxLength = opts.maxLength;
|
|
136
|
-
if (opts.unique === true) field.unique = true;
|
|
137
|
-
if (opts.primaryKey === true) field.primaryKey = true;
|
|
138
|
-
if (opts.null === true) field.null = true;
|
|
139
|
-
if (opts.default !== undefined) field.default = opts.default;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// -- type/AST helpers -------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
/** Read a string-literal-typed property (e.g. `__app: "airports"`) off a type. */
|
|
145
|
-
function literalTypeProp(
|
|
146
|
-
type: ts.Type,
|
|
147
|
-
name: string,
|
|
148
|
-
checker: ts.TypeChecker,
|
|
149
|
-
location: ts.Node,
|
|
150
|
-
): string | undefined {
|
|
151
|
-
const symbol = type.getProperty(name);
|
|
152
|
-
if (!symbol) return undefined;
|
|
153
|
-
const propType = checker.getTypeOfSymbolAtLocation(symbol, location);
|
|
154
|
-
if (propType.isStringLiteral()) return propType.value;
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function stringLiteralArg(node: ts.Expression | undefined): string | undefined {
|
|
159
|
-
if (node && ts.isStringLiteralLike(node)) return node.text;
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function propertyName(name: ts.PropertyName): string | undefined {
|
|
164
|
-
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text;
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Evaluate a *static literal* AST node (string/number/bool/null/array/object)
|
|
170
|
-
* into a JS value. Anything non-literal (a function, an identifier) yields
|
|
171
|
-
* `undefined` — the options and constraint blocks are always literals.
|
|
172
|
-
*/
|
|
173
|
-
function literalToValue(node: ts.Expression | undefined): any {
|
|
174
|
-
if (!node) return undefined;
|
|
175
|
-
if (ts.isStringLiteralLike(node)) return node.text;
|
|
176
|
-
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
177
|
-
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
178
|
-
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
179
|
-
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
180
|
-
if (
|
|
181
|
-
ts.isPrefixUnaryExpression(node) &&
|
|
182
|
-
node.operator === ts.SyntaxKind.MinusToken
|
|
183
|
-
) {
|
|
184
|
-
const inner = literalToValue(node.operand);
|
|
185
|
-
return typeof inner === "number" ? -inner : undefined;
|
|
186
|
-
}
|
|
187
|
-
if (ts.isArrayLiteralExpression(node)) {
|
|
188
|
-
return node.elements.map((el) => literalToValue(el));
|
|
189
|
-
}
|
|
190
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
191
|
-
const obj: any = {};
|
|
192
|
-
for (const p of node.properties) {
|
|
193
|
-
if (ts.isPropertyAssignment(p)) {
|
|
194
|
-
const key = propertyName(p.name);
|
|
195
|
-
if (key) obj[key] = literalToValue(p.initializer);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return obj;
|
|
199
|
-
}
|
|
200
|
-
return undefined;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function normaliseSync(sync: any): SyncModel | undefined {
|
|
204
|
-
if (!sync || typeof sync !== "object") return undefined;
|
|
205
|
-
const mode = sync.mode === "local" ? "local" : "server";
|
|
206
|
-
if (mode === "server" && !sync.confirmed) {
|
|
207
|
-
// A bare server resource carries no sync semantics.
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
const model: SyncModel = { mode };
|
|
211
|
-
if (typeof sync.scope === "string") model.scope = sync.scope;
|
|
212
|
-
if (Array.isArray(sync.confirmed)) model.confirmed = sync.confirmed;
|
|
213
|
-
if (sync.load === "lazy") model.load = "lazy";
|
|
214
|
-
if (sync.clientIds === true) model.clientIds = true;
|
|
215
|
-
return model;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function normaliseAdmin(admin: any): AdminModel | false | undefined {
|
|
219
|
-
if (admin === false) return false;
|
|
220
|
-
if (!admin || typeof admin !== "object") return undefined;
|
|
221
|
-
const model: AdminModel = {};
|
|
222
|
-
if (Array.isArray(admin.listDisplay)) model.listDisplay = admin.listDisplay;
|
|
223
|
-
return model;
|
|
224
|
-
}
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import {
|
|
3
|
+
AdminModel,
|
|
4
|
+
FieldModel,
|
|
5
|
+
RelationModel,
|
|
6
|
+
ResourceModel,
|
|
7
|
+
ScalarType,
|
|
8
|
+
SyncModel,
|
|
9
|
+
} from "./model";
|
|
10
|
+
import { resourceSourceFiles } from "./program";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Statically analyse resource definitions and produce the canonical model.
|
|
14
|
+
*
|
|
15
|
+
* The headline behaviour is **foreign-key inference from types**: for every
|
|
16
|
+
* field we ask the `TypeChecker` for the field's type and, if that type is a
|
|
17
|
+
* `Ref<App, Model>`, we recover the relation target from the `__app`/`__model`
|
|
18
|
+
* string-literal type arguments. No runtime execution, no string heuristics on
|
|
19
|
+
* the value — the relation is read straight out of the type system.
|
|
20
|
+
*/
|
|
21
|
+
export function analyze(
|
|
22
|
+
program: ts.Program,
|
|
23
|
+
fileNames: string[],
|
|
24
|
+
): ResourceModel[] {
|
|
25
|
+
const checker = program.getTypeChecker();
|
|
26
|
+
const resources: ResourceModel[] = [];
|
|
27
|
+
|
|
28
|
+
for (const sourceFile of resourceSourceFiles(program, fileNames)) {
|
|
29
|
+
ts.forEachChild(sourceFile, function walk(node) {
|
|
30
|
+
if (isResourceCall(node)) {
|
|
31
|
+
const model = analyzeResourceCall(node, checker);
|
|
32
|
+
if (model) resources.push(model);
|
|
33
|
+
}
|
|
34
|
+
ts.forEachChild(node, walk);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Deterministic order: app, then model.
|
|
39
|
+
resources.sort((a, b) =>
|
|
40
|
+
a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app),
|
|
41
|
+
);
|
|
42
|
+
return resources;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isResourceCall(node: ts.Node): node is ts.CallExpression {
|
|
46
|
+
return (
|
|
47
|
+
ts.isCallExpression(node) &&
|
|
48
|
+
ts.isIdentifier(node.expression) &&
|
|
49
|
+
node.expression.text === "resource" &&
|
|
50
|
+
node.arguments.length >= 3
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function analyzeResourceCall(
|
|
55
|
+
call: ts.CallExpression,
|
|
56
|
+
checker: ts.TypeChecker,
|
|
57
|
+
): ResourceModel | undefined {
|
|
58
|
+
const app = stringLiteralArg(call.arguments[0]);
|
|
59
|
+
const model = stringLiteralArg(call.arguments[1]);
|
|
60
|
+
const fieldsArg = call.arguments[2];
|
|
61
|
+
if (!app || !model || !fieldsArg || !ts.isObjectLiteralExpression(fieldsArg)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const fields: FieldModel[] = [];
|
|
66
|
+
const relations: RelationModel[] = [];
|
|
67
|
+
|
|
68
|
+
for (const prop of fieldsArg.properties) {
|
|
69
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
70
|
+
const name = propertyName(prop.name);
|
|
71
|
+
if (!name) continue;
|
|
72
|
+
|
|
73
|
+
const field = analyzeField(name, prop.initializer, checker);
|
|
74
|
+
if (!field) continue;
|
|
75
|
+
fields.push(field);
|
|
76
|
+
if (field.type === "fk" && field.relation) {
|
|
77
|
+
relations.push({
|
|
78
|
+
field: name,
|
|
79
|
+
target: `${field.relation.app}.${field.relation.model}`,
|
|
80
|
+
kind: "fk",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const options = literalToValue(call.arguments[3]) ?? {};
|
|
86
|
+
return {
|
|
87
|
+
app,
|
|
88
|
+
model,
|
|
89
|
+
fields,
|
|
90
|
+
relations,
|
|
91
|
+
sync: normaliseSync(options.sync),
|
|
92
|
+
admin: normaliseAdmin(options.admin),
|
|
93
|
+
search: options.search,
|
|
94
|
+
ordering: options.ordering,
|
|
95
|
+
permission: options.permission,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a single field from its initializer. FK detection is type-driven:
|
|
101
|
+
* the initializer's *type* is inspected, and a `Ref` type yields a foreign key.
|
|
102
|
+
*/
|
|
103
|
+
function analyzeField(
|
|
104
|
+
name: string,
|
|
105
|
+
initializer: ts.Expression,
|
|
106
|
+
checker: ts.TypeChecker,
|
|
107
|
+
): FieldModel | undefined {
|
|
108
|
+
const fieldType = checker.getTypeAtLocation(initializer);
|
|
109
|
+
|
|
110
|
+
// Foreign key: the field type is Ref<App, Model>. Recover the target from
|
|
111
|
+
// the string-literal type arguments — this is the "infer FK from types" step.
|
|
112
|
+
if (fieldType.getProperty("__radRef")) {
|
|
113
|
+
const app = literalTypeProp(fieldType, "__app", checker, initializer);
|
|
114
|
+
const model = literalTypeProp(fieldType, "__model", checker, initializer);
|
|
115
|
+
if (app && model) {
|
|
116
|
+
return { name, type: "fk", relation: { app, model } };
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Scalar: the field type carries a `__radScalar` string-literal discriminant.
|
|
122
|
+
const scalar = literalTypeProp(fieldType, "__radScalar", checker, initializer);
|
|
123
|
+
if (!scalar) return undefined;
|
|
124
|
+
|
|
125
|
+
const field: FieldModel = { name, type: scalar as ScalarType };
|
|
126
|
+
applyConstraints(field, initializer);
|
|
127
|
+
return field;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Read scalar constraints (maxLength, unique, …) from the builder call args. */
|
|
131
|
+
function applyConstraints(field: FieldModel, initializer: ts.Expression): void {
|
|
132
|
+
if (!ts.isCallExpression(initializer)) return;
|
|
133
|
+
const opts = literalToValue(initializer.arguments[0]);
|
|
134
|
+
if (!opts || typeof opts !== "object") return;
|
|
135
|
+
if (typeof opts.maxLength === "number") field.maxLength = opts.maxLength;
|
|
136
|
+
if (opts.unique === true) field.unique = true;
|
|
137
|
+
if (opts.primaryKey === true) field.primaryKey = true;
|
|
138
|
+
if (opts.null === true) field.null = true;
|
|
139
|
+
if (opts.default !== undefined) field.default = opts.default;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// -- type/AST helpers -------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/** Read a string-literal-typed property (e.g. `__app: "airports"`) off a type. */
|
|
145
|
+
function literalTypeProp(
|
|
146
|
+
type: ts.Type,
|
|
147
|
+
name: string,
|
|
148
|
+
checker: ts.TypeChecker,
|
|
149
|
+
location: ts.Node,
|
|
150
|
+
): string | undefined {
|
|
151
|
+
const symbol = type.getProperty(name);
|
|
152
|
+
if (!symbol) return undefined;
|
|
153
|
+
const propType = checker.getTypeOfSymbolAtLocation(symbol, location);
|
|
154
|
+
if (propType.isStringLiteral()) return propType.value;
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function stringLiteralArg(node: ts.Expression | undefined): string | undefined {
|
|
159
|
+
if (node && ts.isStringLiteralLike(node)) return node.text;
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function propertyName(name: ts.PropertyName): string | undefined {
|
|
164
|
+
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) return name.text;
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Evaluate a *static literal* AST node (string/number/bool/null/array/object)
|
|
170
|
+
* into a JS value. Anything non-literal (a function, an identifier) yields
|
|
171
|
+
* `undefined` — the options and constraint blocks are always literals.
|
|
172
|
+
*/
|
|
173
|
+
function literalToValue(node: ts.Expression | undefined): any {
|
|
174
|
+
if (!node) return undefined;
|
|
175
|
+
if (ts.isStringLiteralLike(node)) return node.text;
|
|
176
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
177
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
178
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
179
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
180
|
+
if (
|
|
181
|
+
ts.isPrefixUnaryExpression(node) &&
|
|
182
|
+
node.operator === ts.SyntaxKind.MinusToken
|
|
183
|
+
) {
|
|
184
|
+
const inner = literalToValue(node.operand);
|
|
185
|
+
return typeof inner === "number" ? -inner : undefined;
|
|
186
|
+
}
|
|
187
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
188
|
+
return node.elements.map((el) => literalToValue(el));
|
|
189
|
+
}
|
|
190
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
191
|
+
const obj: any = {};
|
|
192
|
+
for (const p of node.properties) {
|
|
193
|
+
if (ts.isPropertyAssignment(p)) {
|
|
194
|
+
const key = propertyName(p.name);
|
|
195
|
+
if (key) obj[key] = literalToValue(p.initializer);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return obj;
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normaliseSync(sync: any): SyncModel | undefined {
|
|
204
|
+
if (!sync || typeof sync !== "object") return undefined;
|
|
205
|
+
const mode = sync.mode === "local" ? "local" : "server";
|
|
206
|
+
if (mode === "server" && !sync.confirmed) {
|
|
207
|
+
// A bare server resource carries no sync semantics.
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const model: SyncModel = { mode };
|
|
211
|
+
if (typeof sync.scope === "string") model.scope = sync.scope;
|
|
212
|
+
if (Array.isArray(sync.confirmed)) model.confirmed = sync.confirmed;
|
|
213
|
+
if (sync.load === "lazy") model.load = "lazy";
|
|
214
|
+
if (sync.clientIds === true) model.clientIds = true;
|
|
215
|
+
return model;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normaliseAdmin(admin: any): AdminModel | false | undefined {
|
|
219
|
+
if (admin === false) return false;
|
|
220
|
+
if (!admin || typeof admin !== "object") return undefined;
|
|
221
|
+
const model: AdminModel = {};
|
|
222
|
+
if (Array.isArray(admin.listDisplay)) model.listDisplay = admin.listDisplay;
|
|
223
|
+
return model;
|
|
224
|
+
}
|
package/src/compile/index.ts
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { analyze } from "./analyze";
|
|
4
|
-
import { buildManifest, manifestToJson } from "./manifest";
|
|
5
|
-
import { Manifest, ResourceModel } from "./model";
|
|
6
|
-
import { buildOpenApi, openApiToJson } from "./openapi";
|
|
7
|
-
import { createProgram } from "./program";
|
|
8
|
-
|
|
9
|
-
export { analyze } from "./analyze";
|
|
10
|
-
export { buildManifest } from "./manifest";
|
|
11
|
-
export { buildOpenApi } from "./openapi";
|
|
12
|
-
export * from "./model";
|
|
13
|
-
|
|
14
|
-
export interface CompileResult {
|
|
15
|
-
manifest: Manifest;
|
|
16
|
-
openapi: any;
|
|
17
|
-
files: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** List every `.ts` resource file under a directory (or accept explicit files). */
|
|
21
|
-
export function resolveResourceFiles(input: string): string[] {
|
|
22
|
-
const stat = fs.existsSync(input) ? fs.statSync(input) : undefined;
|
|
23
|
-
if (stat?.isFile()) return [path.resolve(input)];
|
|
24
|
-
if (stat?.isDirectory()) {
|
|
25
|
-
return fs
|
|
26
|
-
.readdirSync(input)
|
|
27
|
-
.filter((f) => f.endsWith(".ts") && !f.endsWith(".d.ts"))
|
|
28
|
-
.map((f) => path.resolve(input, f))
|
|
29
|
-
.sort();
|
|
30
|
-
}
|
|
31
|
-
throw new Error(`No such resources path: ${input}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Analyse resource definitions into the canonical model (no file output). */
|
|
35
|
-
export function analyzeResources(input: string): ResourceModel[] {
|
|
36
|
-
const files = resolveResourceFiles(input);
|
|
37
|
-
const program = createProgram(files);
|
|
38
|
-
return analyze(program, files);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Compile resource definitions to a manifest + OpenAPI, writing both to disk. */
|
|
42
|
-
export function compile(input: string, outDir: string): CompileResult {
|
|
43
|
-
const resources = analyzeResources(input);
|
|
44
|
-
const manifest = buildManifest(resources);
|
|
45
|
-
const openapi = buildOpenApi(resources);
|
|
46
|
-
|
|
47
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
48
|
-
const manifestPath = path.join(outDir, "schema.rad.json");
|
|
49
|
-
const openapiPath = path.join(outDir, "openapi.json");
|
|
50
|
-
fs.writeFileSync(manifestPath, manifestToJson(manifest), { flag: "w" });
|
|
51
|
-
fs.writeFileSync(openapiPath, openApiToJson(openapi), { flag: "w" });
|
|
52
|
-
|
|
53
|
-
return { manifest, openapi, files: [manifestPath, openapiPath] };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// -- introspection (replaces `manage.py rastack check|list|urls`) -----------
|
|
57
|
-
|
|
58
|
-
/** Detect duplicate `app.model` definitions and dangling FK targets. */
|
|
59
|
-
export function collisions(resources: ResourceModel[]): string[] {
|
|
60
|
-
const errors: string[] = [];
|
|
61
|
-
const seen = new Map<string, number>();
|
|
62
|
-
const keys = new Set(resources.map((r) => `${r.app}.${r.model}`));
|
|
63
|
-
|
|
64
|
-
for (const r of resources) {
|
|
65
|
-
const key = `${r.app}.${r.model}`;
|
|
66
|
-
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
67
|
-
for (const rel of r.relations) {
|
|
68
|
-
if (!keys.has(rel.target)) {
|
|
69
|
-
errors.push(
|
|
70
|
-
`${key}.${rel.field} references unknown resource ${rel.target}.`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
for (const [key, count] of seen) {
|
|
76
|
-
if (count > 1) errors.push(`Duplicate resource ${key} defined ${count} times.`);
|
|
77
|
-
}
|
|
78
|
-
return errors;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function urlsFor(resources: ResourceModel[]): string[] {
|
|
82
|
-
return resources.flatMap((r) => [
|
|
83
|
-
`/api/${r.app}/v1/${r.model}/`,
|
|
84
|
-
`/api/${r.app}/v1/${r.model}/{id}/`,
|
|
85
|
-
]);
|
|
86
|
-
}
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { analyze } from "./analyze";
|
|
4
|
+
import { buildManifest, manifestToJson } from "./manifest";
|
|
5
|
+
import { Manifest, ResourceModel } from "./model";
|
|
6
|
+
import { buildOpenApi, openApiToJson } from "./openapi";
|
|
7
|
+
import { createProgram } from "./program";
|
|
8
|
+
|
|
9
|
+
export { analyze } from "./analyze";
|
|
10
|
+
export { buildManifest } from "./manifest";
|
|
11
|
+
export { buildOpenApi } from "./openapi";
|
|
12
|
+
export * from "./model";
|
|
13
|
+
|
|
14
|
+
export interface CompileResult {
|
|
15
|
+
manifest: Manifest;
|
|
16
|
+
openapi: any;
|
|
17
|
+
files: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** List every `.ts` resource file under a directory (or accept explicit files). */
|
|
21
|
+
export function resolveResourceFiles(input: string): string[] {
|
|
22
|
+
const stat = fs.existsSync(input) ? fs.statSync(input) : undefined;
|
|
23
|
+
if (stat?.isFile()) return [path.resolve(input)];
|
|
24
|
+
if (stat?.isDirectory()) {
|
|
25
|
+
return fs
|
|
26
|
+
.readdirSync(input)
|
|
27
|
+
.filter((f) => f.endsWith(".ts") && !f.endsWith(".d.ts"))
|
|
28
|
+
.map((f) => path.resolve(input, f))
|
|
29
|
+
.sort();
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`No such resources path: ${input}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Analyse resource definitions into the canonical model (no file output). */
|
|
35
|
+
export function analyzeResources(input: string): ResourceModel[] {
|
|
36
|
+
const files = resolveResourceFiles(input);
|
|
37
|
+
const program = createProgram(files);
|
|
38
|
+
return analyze(program, files);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Compile resource definitions to a manifest + OpenAPI, writing both to disk. */
|
|
42
|
+
export function compile(input: string, outDir: string): CompileResult {
|
|
43
|
+
const resources = analyzeResources(input);
|
|
44
|
+
const manifest = buildManifest(resources);
|
|
45
|
+
const openapi = buildOpenApi(resources);
|
|
46
|
+
|
|
47
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
48
|
+
const manifestPath = path.join(outDir, "schema.rad.json");
|
|
49
|
+
const openapiPath = path.join(outDir, "openapi.json");
|
|
50
|
+
fs.writeFileSync(manifestPath, manifestToJson(manifest), { flag: "w" });
|
|
51
|
+
fs.writeFileSync(openapiPath, openApiToJson(openapi), { flag: "w" });
|
|
52
|
+
|
|
53
|
+
return { manifest, openapi, files: [manifestPath, openapiPath] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -- introspection (replaces `manage.py rastack check|list|urls`) -----------
|
|
57
|
+
|
|
58
|
+
/** Detect duplicate `app.model` definitions and dangling FK targets. */
|
|
59
|
+
export function collisions(resources: ResourceModel[]): string[] {
|
|
60
|
+
const errors: string[] = [];
|
|
61
|
+
const seen = new Map<string, number>();
|
|
62
|
+
const keys = new Set(resources.map((r) => `${r.app}.${r.model}`));
|
|
63
|
+
|
|
64
|
+
for (const r of resources) {
|
|
65
|
+
const key = `${r.app}.${r.model}`;
|
|
66
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
67
|
+
for (const rel of r.relations) {
|
|
68
|
+
if (!keys.has(rel.target)) {
|
|
69
|
+
errors.push(
|
|
70
|
+
`${key}.${rel.field} references unknown resource ${rel.target}.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const [key, count] of seen) {
|
|
76
|
+
if (count > 1) errors.push(`Duplicate resource ${key} defined ${count} times.`);
|
|
77
|
+
}
|
|
78
|
+
return errors;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function urlsFor(resources: ResourceModel[]): string[] {
|
|
82
|
+
return resources.flatMap((r) => [
|
|
83
|
+
`/api/${r.app}/v1/${r.model}/`,
|
|
84
|
+
`/api/${r.app}/v1/${r.model}/{id}/`,
|
|
85
|
+
]);
|
|
86
|
+
}
|
package/src/compile/manifest.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Manifest, ResourceModel } from "./model";
|
|
2
|
-
|
|
3
|
-
/** Assemble the canonical manifest from analysed resources. */
|
|
4
|
-
export function buildManifest(resources: ResourceModel[]): Manifest {
|
|
5
|
-
return { version: 1, resources };
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function manifestToJson(manifest: Manifest): string {
|
|
9
|
-
return JSON.stringify(manifest, null, 2) + "\n";
|
|
10
|
-
}
|
|
1
|
+
import { Manifest, ResourceModel } from "./model";
|
|
2
|
+
|
|
3
|
+
/** Assemble the canonical manifest from analysed resources. */
|
|
4
|
+
export function buildManifest(resources: ResourceModel[]): Manifest {
|
|
5
|
+
return { version: 1, resources };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function manifestToJson(manifest: Manifest): string {
|
|
9
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
10
|
+
}
|