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.
Files changed (105) hide show
  1. package/CHANGELOG.md +43874 -43013
  2. package/dist/.prettierrc +7 -7
  3. package/dist/compile/analyze.js +19 -11
  4. package/dist/compile/index.js +26 -17
  5. package/dist/compile/manifest.js +2 -3
  6. package/dist/compile/openapi.js +5 -8
  7. package/dist/compile/program.js +19 -10
  8. package/dist/csv-schema.js +5 -6
  9. package/dist/define/index.js +2 -2
  10. package/dist/entity-generation/delete-method.js +8 -10
  11. package/dist/entity-generation/form.js +80 -82
  12. package/dist/entity-generation/get-method.js +35 -36
  13. package/dist/entity-generation/imports.js +24 -20
  14. package/dist/entity-generation/sync-method.d.ts +1 -1
  15. package/dist/entity-generation/sync-method.js +45 -46
  16. package/dist/entity-generation/update-method.js +9 -10
  17. package/dist/rad-wasm-build.d.ts +1 -1
  18. package/dist/rad-wasm-build.js +15 -6
  19. package/dist/rad.js +9 -2
  20. package/dist/schema/camel-to-pastel.js +1 -2
  21. package/dist/schema/capitalise-first-letter.js +1 -2
  22. package/dist/schema/extract-response.js +3 -4
  23. package/dist/schema/fetch-schema.js +4 -16
  24. package/dist/schema/remove-non-model-paths.js +1 -2
  25. package/dist/schema/to-camel-case.js +1 -2
  26. package/dist/schema/to-pastel-case.js +1 -2
  27. package/dist/schema-convert.js +22 -33
  28. package/dist/schema-entities.js +177 -189
  29. package/dist/schema-fetch.js +46 -63
  30. package/dist/schema-full.js +23 -36
  31. package/dist/schema-index.js +16 -27
  32. package/dist/schema-params.js +11 -22
  33. package/dist/seed.js +95 -97
  34. package/hooks/form/form.ts +207 -207
  35. package/hooks/form/index.ts +8 -8
  36. package/hooks/form/interfaces.ts +217 -217
  37. package/hooks/form/structure.ts +39 -39
  38. package/hooks/form/validate-schema.ts +49 -49
  39. package/hooks/index.ts +3 -3
  40. package/hooks/query/api.ts +42 -42
  41. package/hooks/query/delete.ts +45 -45
  42. package/hooks/query/fetch.ts +48 -48
  43. package/hooks/query/index.ts +21 -21
  44. package/hooks/query/interfaces.ts +111 -111
  45. package/hooks/query/list.ts +286 -286
  46. package/hooks/query/update.ts +88 -88
  47. package/hooks/query/url.ts +31 -31
  48. package/hooks/real-time/index.ts +1 -1
  49. package/hooks/real-time/pusher.ts +43 -43
  50. package/jest.config.cjs +6 -6
  51. package/package.json +57 -57
  52. package/provider/index.ts +13 -13
  53. package/provider/provider.tsx +187 -187
  54. package/provider/types.ts +114 -114
  55. package/provider/warehouse.ts +167 -167
  56. package/provider/wasm.ts +124 -124
  57. package/runtime.ts +3 -3
  58. package/src/.prettierrc +7 -7
  59. package/src/compile/analyze.ts +224 -224
  60. package/src/compile/index.ts +86 -86
  61. package/src/compile/manifest.ts +10 -10
  62. package/src/compile/model.ts +69 -69
  63. package/src/compile/openapi.ts +266 -266
  64. package/src/compile/program.ts +40 -40
  65. package/src/csv-schema.ts +187 -187
  66. package/src/define/index.ts +162 -162
  67. package/src/entity-generation/delete-method.ts +37 -37
  68. package/src/entity-generation/form.ts +169 -169
  69. package/src/entity-generation/get-method.ts +80 -80
  70. package/src/entity-generation/imports.ts +110 -110
  71. package/src/entity-generation/sync-method.ts +222 -222
  72. package/src/entity-generation/update-method.ts +38 -38
  73. package/src/index.ts +77 -77
  74. package/src/rad-compile.ts +88 -88
  75. package/src/rad-wasm-build.ts +102 -102
  76. package/src/rad.ts +101 -101
  77. package/src/scan.ts +89 -89
  78. package/src/schema/camel-to-pastel.ts +3 -3
  79. package/src/schema/capitalise-first-letter.ts +3 -3
  80. package/src/schema/extract-response.ts +38 -38
  81. package/src/schema/fetch-schema.ts +12 -12
  82. package/src/schema/remove-non-model-paths.ts +13 -13
  83. package/src/schema/to-camel-case.ts +7 -7
  84. package/src/schema/to-pastel-case.ts +6 -6
  85. package/src/schema-convert.ts +61 -61
  86. package/src/schema-entities.ts +401 -401
  87. package/src/schema-fetch.ts +82 -82
  88. package/src/schema-full.ts +38 -38
  89. package/src/schema-index.ts +29 -29
  90. package/src/schema-params.ts +116 -116
  91. package/src/seed.ts +297 -297
  92. package/sync/engine.ts +392 -392
  93. package/sync/hooks.ts +450 -450
  94. package/sync/index.ts +8 -8
  95. package/sync/persistence.ts +237 -237
  96. package/sync/provider.tsx +91 -91
  97. package/sync/registry.ts +36 -36
  98. package/sync/store.ts +126 -126
  99. package/sync/transactions.ts +300 -300
  100. package/sync/types.ts +94 -94
  101. package/test/compile.spec.ts +192 -192
  102. package/test/csv-schema.spec.ts +143 -143
  103. package/test/schema-entities.spec.ts +688 -688
  104. package/tsconfig.json +24 -24
  105. package/types.ts +18 -18
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }