rastack 0.0.46 → 0.0.48
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 +4 -0
- package/dist/compile/entities.d.ts +0 -49
- package/dist/compile/entities.js +110 -22
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -1
- package/dist/compile/program.js +1 -1
- package/dist/define/db.d.ts +1 -1
- package/dist/define/db.js +1 -1
- package/dist/import/index.d.ts +18 -0
- package/dist/import/index.js +57 -0
- package/dist/import/pdf.d.ts +25 -0
- package/dist/import/pdf.js +326 -0
- package/dist/import/tabular.d.ts +76 -0
- package/dist/import/tabular.js +98 -0
- package/dist/import/xlsx.d.ts +18 -0
- package/dist/import/xlsx.js +160 -0
- package/dist/rastack-import.d.ts +22 -0
- package/dist/rastack-import.js +165 -0
- package/dist/rastack.d.ts +1 -0
- package/dist/rastack.js +7 -0
- package/dist/validate/adapters.d.ts +42 -0
- package/dist/validate/adapters.js +109 -0
- package/dist/validate/index.d.ts +2 -0
- package/dist/validate/index.js +18 -0
- package/dist/validate/machine.d.ts +175 -0
- package/dist/validate/machine.js +347 -0
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/hooks/form/form.ts +16 -5
- package/hooks/form/interfaces.ts +15 -0
- package/hooks/form/structure.ts +28 -0
- package/package.json +1 -1
- package/src/compile/entities.ts +161 -59
- package/src/compile/index.ts +1 -1
- package/src/compile/program.ts +1 -1
- package/src/define/db.ts +1 -1
- package/src/import/index.ts +41 -0
- package/src/import/pdf.ts +304 -0
- package/src/import/tabular.ts +159 -0
- package/src/import/xlsx.ts +186 -0
- package/src/rastack-import.ts +203 -0
- package/src/rastack.ts +7 -0
- package/src/validate/adapters.ts +118 -0
- package/src/validate/index.ts +2 -0
- package/src/validate/machine.ts +525 -0
- package/test/entities.spec.ts +99 -0
- package/test/import.spec.ts +241 -0
- package/test/validate.spec.ts +319 -0
- package/validate.ts +10 -0
- package/wasm/rastack_wasm_bg.wasm +0 -0
package/src/compile/entities.ts
CHANGED
|
@@ -4,55 +4,67 @@ import { FieldModel, RelationModel, ResourceModel, ScalarType } from "./model";
|
|
|
4
4
|
import { resourceSourceFiles } from "./program";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Type-driven resource inference — "the entities in the app are a list of
|
|
8
|
+
* plain TypeScript types".
|
|
9
9
|
*
|
|
10
10
|
* Instead of (or alongside) the `resource()` DSL, an app can describe its data
|
|
11
|
-
* as plain TypeScript interfaces and consume them through typed
|
|
12
|
-
* analyser works the resource graph out from the *usage*:
|
|
11
|
+
* as plain TypeScript **classes or interfaces** and consume them through typed
|
|
12
|
+
* hooks. The analyser works the resource graph out from the *usage*:
|
|
13
13
|
*
|
|
14
14
|
* 1. **Roots come from the hooks.** Every hook call that carries a type
|
|
15
15
|
* argument — `useEntityList<Airport>(…)`, `useQuery<Terminal>(…)` — marks
|
|
16
|
-
* the interface behind that type argument as an entity.
|
|
17
|
-
* also opt in explicitly with a `@rastackResource` JSDoc tag.
|
|
18
|
-
*
|
|
16
|
+
* the class/interface behind that type argument as an entity. A declaration
|
|
17
|
+
* can also opt in explicitly with a `@rastackResource` JSDoc tag. And a
|
|
18
|
+
* **plain data class** — no heritage clause, no methods or constructor,
|
|
19
|
+
* only property declarations — is an entity by virtue of being declared:
|
|
20
|
+
* writing `export class Airport { code: string }` is the whole authoring
|
|
21
|
+
* step, no DSL and no tag required. (Interfaces are ubiquitous in app code,
|
|
22
|
+
* so they still need a hook or a tag to root; an entity-shaped class is a
|
|
23
|
+
* deliberate declaration.)
|
|
24
|
+
* 2. **Datatypes come from the properties.** Each property's TypeScript type
|
|
19
25
|
* maps to a manifest scalar (`string` → string, `number` → float,
|
|
20
26
|
* `boolean` → bool, `Date` → datetime). Constraints are carried in the
|
|
21
27
|
* type system with `DbConfig` brands from `rastack/types` —
|
|
22
28
|
* `code: string & Unique & MaxLength<3>`, `age: Int` — or, equivalently,
|
|
23
29
|
* as JSDoc tags (`@unique`, `@maxLength 3`, `@int`, `@uuid`, `@default`).
|
|
24
|
-
* Optional / `| null` properties become nullable fields.
|
|
30
|
+
* Optional / `| null` properties become nullable fields. On a class, a
|
|
31
|
+
* literal property initializer (`status = "scheduled"`) doubles as the
|
|
32
|
+
* column default, and the property type may be inferred from it.
|
|
25
33
|
* 3. **Foreign keys come from the type system.** A property whose type is
|
|
26
|
-
* another entity interface is a foreign key, and the referenced
|
|
27
|
-
* is pulled into the resource graph transitively — the closure
|
|
28
|
-
*
|
|
34
|
+
* another entity class/interface is a foreign key, and the referenced
|
|
35
|
+
* declaration is pulled into the resource graph transitively — the closure
|
|
36
|
+
* of the rooted declarations is the app's resource set.
|
|
29
37
|
* 4. **Circular dependencies are detected.** The FK graph is walked for
|
|
30
38
|
* cycles; a cycle is an *error* when every edge in it is required (no row
|
|
31
39
|
* could ever be inserted) and a *warning* when it is breakable through a
|
|
32
40
|
* nullable field (e.g. an optional self-referencing `manager`).
|
|
33
41
|
*
|
|
34
42
|
* ```ts
|
|
35
|
-
* // entities/airports.ts
|
|
36
|
-
* export
|
|
43
|
+
* // entities/airports.ts — plain data classes are entities by declaration
|
|
44
|
+
* export class Airport {
|
|
37
45
|
* code: string; // JSDoc tags refine: @maxLength 3, @unique
|
|
38
46
|
* name: string;
|
|
39
47
|
* }
|
|
40
|
-
* export
|
|
48
|
+
* export class Terminal {
|
|
41
49
|
* airport: Airport; // FK — inferred from the property type
|
|
42
50
|
* label: string;
|
|
43
51
|
* }
|
|
44
52
|
*
|
|
53
|
+
* // interfaces root through usage instead:
|
|
45
54
|
* // app/screen.tsx — the hook call is what roots the graph
|
|
46
55
|
* const terminals = useEntityList<Terminal>("terminals");
|
|
47
|
-
* // → resources: airports.terminal (root) + airports.airport (transitive)
|
|
48
56
|
* ```
|
|
49
57
|
*
|
|
50
|
-
* Naming: `model` is the lower-camel interface name (a leading `I`
|
|
51
|
-
* in generated `IFlight` interfaces is stripped, so hook usage of
|
|
52
|
-
* types round-trips onto the same resource); `app` defaults to the
|
|
53
|
-
* file's basename. Both are overridable with `@rastackApp` /
|
|
58
|
+
* Naming: `model` is the lower-camel class/interface name (a leading `I`
|
|
59
|
+
* prefix as in generated `IFlight` interfaces is stripped, so hook usage of
|
|
60
|
+
* generated types round-trips onto the same resource); `app` defaults to the
|
|
61
|
+
* defining file's basename. Both are overridable with `@rastackApp` /
|
|
62
|
+
* `@rastackModel` tags.
|
|
54
63
|
*/
|
|
55
64
|
|
|
65
|
+
/** An entity can be declared as an interface or as a (named) class. */
|
|
66
|
+
type EntityDecl = ts.InterfaceDeclaration | ts.ClassDeclaration;
|
|
67
|
+
|
|
56
68
|
export interface EntityDiagnostic {
|
|
57
69
|
severity: "error" | "warning";
|
|
58
70
|
message: string;
|
|
@@ -72,20 +84,22 @@ export function analyzeEntities(
|
|
|
72
84
|
const sourceFiles = resourceSourceFiles(program, fileNames);
|
|
73
85
|
const scanned = new Set(sourceFiles.map((sf) => path.resolve(sf.fileName)));
|
|
74
86
|
|
|
75
|
-
// Every interface declared in the scanned files is a *candidate* entity.
|
|
76
|
-
const candidates = new Set<
|
|
87
|
+
// Every class/interface declared in the scanned files is a *candidate* entity.
|
|
88
|
+
const candidates = new Set<EntityDecl>();
|
|
77
89
|
for (const sf of sourceFiles) {
|
|
78
90
|
ts.forEachChild(sf, function walk(node) {
|
|
79
91
|
if (ts.isInterfaceDeclaration(node)) candidates.add(node);
|
|
92
|
+
if (ts.isClassDeclaration(node) && node.name) candidates.add(node);
|
|
80
93
|
ts.forEachChild(node, walk);
|
|
81
94
|
});
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
// 1. Roots:
|
|
85
|
-
// `@rastackResource` opt-ins
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const
|
|
97
|
+
// 1. Roots: declarations used as type arguments of hook calls, explicit
|
|
98
|
+
// `@rastackResource` opt-ins, and plain data classes (declaring one *is*
|
|
99
|
+
// the opt-in).
|
|
100
|
+
const roots: EntityDecl[] = [];
|
|
101
|
+
const rooted = new Set<EntityDecl>();
|
|
102
|
+
const addRoot = (decl: EntityDecl) => {
|
|
89
103
|
if (!rooted.has(decl)) {
|
|
90
104
|
rooted.add(decl);
|
|
91
105
|
roots.push(decl);
|
|
@@ -95,7 +109,7 @@ export function analyzeEntities(
|
|
|
95
109
|
ts.forEachChild(sf, function walk(node) {
|
|
96
110
|
if (ts.isCallExpression(node) && isHookCall(node)) {
|
|
97
111
|
for (const typeArg of node.typeArguments ?? []) {
|
|
98
|
-
const decl =
|
|
112
|
+
const decl = entityBehindTypeNode(typeArg, checker, scanned);
|
|
99
113
|
if (decl && candidates.has(decl)) addRoot(decl);
|
|
100
114
|
}
|
|
101
115
|
}
|
|
@@ -104,12 +118,13 @@ export function analyzeEntities(
|
|
|
104
118
|
}
|
|
105
119
|
for (const decl of candidates) {
|
|
106
120
|
if (jsdocTags(decl).has("rastackresource")) addRoot(decl);
|
|
121
|
+
else if (ts.isClassDeclaration(decl) && isDataClass(decl)) addRoot(decl);
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
// 2. Transitive closure over FK references: an entity's
|
|
110
|
-
// property pulls the referenced
|
|
111
|
-
const propsByDecl = new Map<
|
|
112
|
-
const analyzed = (decl:
|
|
124
|
+
// 2. Transitive closure over FK references: an entity's entity-typed
|
|
125
|
+
// property pulls the referenced declaration in as an entity too.
|
|
126
|
+
const propsByDecl = new Map<EntityDecl, PropInfo[]>();
|
|
127
|
+
const analyzed = (decl: EntityDecl): PropInfo[] => {
|
|
113
128
|
let props = propsByDecl.get(decl);
|
|
114
129
|
if (!props) {
|
|
115
130
|
props = analyzeProperties(decl, checker, scanned);
|
|
@@ -118,8 +133,8 @@ export function analyzeEntities(
|
|
|
118
133
|
return props;
|
|
119
134
|
};
|
|
120
135
|
|
|
121
|
-
const included:
|
|
122
|
-
const seen = new Set<
|
|
136
|
+
const included: EntityDecl[] = [];
|
|
137
|
+
const seen = new Set<EntityDecl>();
|
|
123
138
|
const queue = [...roots];
|
|
124
139
|
while (queue.length) {
|
|
125
140
|
const decl = queue.shift()!;
|
|
@@ -200,23 +215,24 @@ function isHookCall(call: ts.CallExpression): boolean {
|
|
|
200
215
|
return !!name && /^use[A-Z0-9_]/.test(name);
|
|
201
216
|
}
|
|
202
217
|
|
|
203
|
-
/** Resolve a type node to
|
|
204
|
-
function
|
|
218
|
+
/** Resolve a type node to a class/interface declared in a scanned file. */
|
|
219
|
+
function entityBehindTypeNode(
|
|
205
220
|
node: ts.TypeNode,
|
|
206
221
|
checker: ts.TypeChecker,
|
|
207
222
|
scanned: Set<string>,
|
|
208
|
-
):
|
|
209
|
-
return
|
|
223
|
+
): EntityDecl | undefined {
|
|
224
|
+
return entityBehindType(checker.getTypeFromTypeNode(node), scanned);
|
|
210
225
|
}
|
|
211
226
|
|
|
212
|
-
function
|
|
227
|
+
function entityBehindType(
|
|
213
228
|
type: ts.Type,
|
|
214
229
|
scanned: Set<string>,
|
|
215
|
-
):
|
|
230
|
+
): EntityDecl | undefined {
|
|
216
231
|
const symbol = type.aliasSymbol ?? type.getSymbol();
|
|
217
232
|
for (const decl of symbol?.declarations ?? []) {
|
|
218
233
|
if (
|
|
219
|
-
ts.isInterfaceDeclaration(decl)
|
|
234
|
+
(ts.isInterfaceDeclaration(decl) ||
|
|
235
|
+
(ts.isClassDeclaration(decl) && decl.name)) &&
|
|
220
236
|
scanned.has(path.resolve(decl.getSourceFile().fileName))
|
|
221
237
|
) {
|
|
222
238
|
return decl;
|
|
@@ -225,6 +241,31 @@ function interfaceBehindType(
|
|
|
225
241
|
return undefined;
|
|
226
242
|
}
|
|
227
243
|
|
|
244
|
+
/**
|
|
245
|
+
* A *plain data class* — the class-authored equivalent of a `resource()` call,
|
|
246
|
+
* so it roots itself. It must carry no heritage clause (a subclass of
|
|
247
|
+
* `React.Component` or a service base is app code, not a table), not be
|
|
248
|
+
* abstract, contain nothing but property declarations, and have at least one
|
|
249
|
+
* instance property. Anything richer can still opt in via a hook or the
|
|
250
|
+
* `@rastackResource` tag.
|
|
251
|
+
*/
|
|
252
|
+
function isDataClass(decl: ts.ClassDeclaration): boolean {
|
|
253
|
+
if (decl.heritageClauses?.length) return false;
|
|
254
|
+
if (
|
|
255
|
+
ts.getCombinedModifierFlags(decl) &
|
|
256
|
+
(ts.ModifierFlags.Abstract | ts.ModifierFlags.Ambient)
|
|
257
|
+
) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
let properties = 0;
|
|
261
|
+
for (const member of decl.members) {
|
|
262
|
+
if (!ts.isPropertyDeclaration(member)) return false; // method/ctor/accessor
|
|
263
|
+
if (ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static) continue;
|
|
264
|
+
properties += 1;
|
|
265
|
+
}
|
|
266
|
+
return properties > 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
228
269
|
// -- property analysis --------------------------------------------------------
|
|
229
270
|
|
|
230
271
|
interface PropInfo {
|
|
@@ -232,43 +273,79 @@ interface PropInfo {
|
|
|
232
273
|
nullable: boolean;
|
|
233
274
|
/** Scalar field (mutually exclusive with `target`). */
|
|
234
275
|
field?: FieldModel;
|
|
235
|
-
/** FK target
|
|
236
|
-
target?:
|
|
276
|
+
/** FK target declaration (relation resolved after naming). */
|
|
277
|
+
target?: EntityDecl;
|
|
237
278
|
/** `Entity[]` — the reverse side of a FK; contributes no column. */
|
|
238
279
|
inverse?: boolean;
|
|
239
280
|
/** Unsupported type — reported as a warning and skipped. */
|
|
240
281
|
skip?: string;
|
|
241
282
|
}
|
|
242
283
|
|
|
284
|
+
/** A column-shaped member: an interface property or a class instance field. */
|
|
285
|
+
type EntityProp = ts.PropertySignature | ts.PropertyDeclaration;
|
|
286
|
+
|
|
243
287
|
function analyzeProperties(
|
|
244
|
-
decl:
|
|
288
|
+
decl: EntityDecl,
|
|
245
289
|
checker: ts.TypeChecker,
|
|
246
290
|
scanned: Set<string>,
|
|
247
291
|
): PropInfo[] {
|
|
248
292
|
const props: PropInfo[] = [];
|
|
249
293
|
for (const member of decl.members) {
|
|
250
|
-
|
|
294
|
+
let prop: EntityProp | undefined;
|
|
295
|
+
if (ts.isPropertySignature(member) && member.type) {
|
|
296
|
+
prop = member;
|
|
297
|
+
} else if (
|
|
298
|
+
ts.isPropertyDeclaration(member) &&
|
|
299
|
+
(member.type || member.initializer) &&
|
|
300
|
+
!ts.isPrivateIdentifier(member.name) &&
|
|
301
|
+
!(
|
|
302
|
+
ts.getCombinedModifierFlags(member) &
|
|
303
|
+
(ts.ModifierFlags.Static |
|
|
304
|
+
ts.ModifierFlags.Private |
|
|
305
|
+
ts.ModifierFlags.Protected)
|
|
306
|
+
)
|
|
307
|
+
) {
|
|
308
|
+
prop = member;
|
|
309
|
+
}
|
|
310
|
+
if (!prop) continue;
|
|
251
311
|
const name =
|
|
252
|
-
ts.isIdentifier(
|
|
253
|
-
?
|
|
312
|
+
ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name)
|
|
313
|
+
? prop.name.text
|
|
254
314
|
: undefined;
|
|
255
315
|
if (!name) continue;
|
|
256
|
-
props.push(analyzeProperty(name,
|
|
316
|
+
props.push(analyzeProperty(name, prop, checker, scanned));
|
|
257
317
|
}
|
|
258
318
|
return props;
|
|
259
319
|
}
|
|
260
320
|
|
|
261
321
|
function analyzeProperty(
|
|
262
322
|
name: string,
|
|
263
|
-
member:
|
|
323
|
+
member: EntityProp,
|
|
264
324
|
checker: ts.TypeChecker,
|
|
265
325
|
scanned: Set<string>,
|
|
266
326
|
): PropInfo {
|
|
267
327
|
let nullable = !!member.questionToken;
|
|
268
328
|
|
|
329
|
+
// A class field with no annotation infers its (widened) type from the
|
|
330
|
+
// initializer — `region = "EU"` is a string column defaulting to "EU".
|
|
331
|
+
if (!member.type) {
|
|
332
|
+
const type = checker.getTypeAtLocation(member);
|
|
333
|
+
const base = classifyType(type, scanned);
|
|
334
|
+
if (base === undefined) {
|
|
335
|
+
return { name, nullable, skip: "unsupported type — skipped" };
|
|
336
|
+
}
|
|
337
|
+
if (typeof base !== "string") return { name, nullable, target: base };
|
|
338
|
+
const config = configFromTags(jsdocTags(member));
|
|
339
|
+
if (config.default === undefined) {
|
|
340
|
+
const dflt = initializerDefault(member);
|
|
341
|
+
if (dflt !== undefined) config.default = dflt;
|
|
342
|
+
}
|
|
343
|
+
return scalarProp(name, base, config, nullable);
|
|
344
|
+
}
|
|
345
|
+
|
|
269
346
|
// Unwrap parens and unions: strip null/undefined (→ nullable), and require a
|
|
270
347
|
// single remaining arm (unions of string literals collapse to `string`).
|
|
271
|
-
let typeNode = unwrapParens(member.type
|
|
348
|
+
let typeNode = unwrapParens(member.type);
|
|
272
349
|
if (ts.isUnionTypeNode(typeNode)) {
|
|
273
350
|
const arms = typeNode.types.filter((t) => {
|
|
274
351
|
const isNullish =
|
|
@@ -290,7 +367,7 @@ function analyzeProperty(
|
|
|
290
367
|
// but is intentionally not an error. Arrays of scalars have no manifest shape.
|
|
291
368
|
const element = arrayElement(typeNode);
|
|
292
369
|
if (element) {
|
|
293
|
-
if (
|
|
370
|
+
if (entityBehindTypeNode(element, checker, scanned)) {
|
|
294
371
|
return { name, nullable, inverse: true };
|
|
295
372
|
}
|
|
296
373
|
return { name, nullable, skip: "array fields are not supported — skipped" };
|
|
@@ -302,26 +379,50 @@ function analyzeProperty(
|
|
|
302
379
|
return { name, nullable, skip: "unsupported type — skipped" };
|
|
303
380
|
}
|
|
304
381
|
if (typeof base !== "string") {
|
|
305
|
-
return { name, nullable, target: base }; // entity interface → FK
|
|
382
|
+
return { name, nullable, target: base }; // entity class/interface → FK
|
|
306
383
|
}
|
|
307
384
|
|
|
308
385
|
const config = {
|
|
309
386
|
...configFromTags(jsdocTags(member)),
|
|
310
387
|
...configFromType(type, checker, member),
|
|
311
388
|
};
|
|
389
|
+
if (config.default === undefined) {
|
|
390
|
+
const dflt = initializerDefault(member);
|
|
391
|
+
if (dflt !== undefined) config.default = dflt;
|
|
392
|
+
}
|
|
312
393
|
return scalarProp(name, base, config, nullable);
|
|
313
394
|
}
|
|
314
395
|
|
|
396
|
+
/** The literal value of a class field's initializer, used as the default. */
|
|
397
|
+
function initializerDefault(
|
|
398
|
+
member: EntityProp,
|
|
399
|
+
): string | number | boolean | undefined {
|
|
400
|
+
if (!ts.isPropertyDeclaration(member) || !member.initializer) return undefined;
|
|
401
|
+
const init = member.initializer;
|
|
402
|
+
if (ts.isStringLiteralLike(init)) return init.text;
|
|
403
|
+
if (ts.isNumericLiteral(init)) return Number(init.text);
|
|
404
|
+
if (
|
|
405
|
+
ts.isPrefixUnaryExpression(init) &&
|
|
406
|
+
init.operator === ts.SyntaxKind.MinusToken &&
|
|
407
|
+
ts.isNumericLiteral(init.operand)
|
|
408
|
+
) {
|
|
409
|
+
return -Number(init.operand.text);
|
|
410
|
+
}
|
|
411
|
+
if (init.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
412
|
+
if (init.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
315
416
|
/**
|
|
316
|
-
* What a property type *is*: a manifest scalar, an entity interface
|
|
317
|
-
* unsupported. Intersections classify by their first classifiable
|
|
318
|
-
* which is what lets `DbConfig` brands ride along — `string &
|
|
319
|
-
* string; the brand carries configuration, not a datatype.
|
|
417
|
+
* What a property type *is*: a manifest scalar, an entity class/interface
|
|
418
|
+
* (→ FK), or unsupported. Intersections classify by their first classifiable
|
|
419
|
+
* constituent, which is what lets `DbConfig` brands ride along — `string &
|
|
420
|
+
* Unique` is a string; the brand carries configuration, not a datatype.
|
|
320
421
|
*/
|
|
321
422
|
function classifyType(
|
|
322
423
|
type: ts.Type,
|
|
323
424
|
scanned: Set<string>,
|
|
324
|
-
): ScalarType |
|
|
425
|
+
): ScalarType | EntityDecl | undefined {
|
|
325
426
|
if (type.flags & ts.TypeFlags.StringLike) return "string";
|
|
326
427
|
if (type.flags & ts.TypeFlags.NumberLike) return "float";
|
|
327
428
|
if (type.flags & ts.TypeFlags.BooleanLike) return "bool";
|
|
@@ -333,7 +434,7 @@ function classifyType(
|
|
|
333
434
|
return undefined;
|
|
334
435
|
}
|
|
335
436
|
if (type.getProperty("__rastackDb")) return undefined; // a bare config brand
|
|
336
|
-
const decl =
|
|
437
|
+
const decl = entityBehindType(type, scanned);
|
|
337
438
|
if (decl) return decl;
|
|
338
439
|
if (type.getSymbol()?.getName() === "Date") return "datetime";
|
|
339
440
|
return undefined;
|
|
@@ -476,9 +577,10 @@ interface EntityNaming {
|
|
|
476
577
|
permission?: ResourceModel["permission"];
|
|
477
578
|
}
|
|
478
579
|
|
|
479
|
-
function entityNaming(decl:
|
|
580
|
+
function entityNaming(decl: EntityDecl): EntityNaming {
|
|
480
581
|
const tags = jsdocTags(decl);
|
|
481
|
-
|
|
582
|
+
// Class candidates are only ever collected with a name (see analyzeEntities).
|
|
583
|
+
const raw = decl.name!.text;
|
|
482
584
|
// Generated entity interfaces are `I`-prefixed (`IFlight`); strip the prefix
|
|
483
585
|
// so hook usage of generated types resolves to the same resource.
|
|
484
586
|
const stripped = /^I[A-Z]/.test(raw) ? raw.slice(1) : raw;
|
package/src/compile/index.ts
CHANGED
|
@@ -39,7 +39,7 @@ const IGNORE_DIRS = new Set([
|
|
|
39
39
|
* beside `app/airports/index.tsx` — instead of in one top-level `resources/`
|
|
40
40
|
* folder. `.tsx` files carry no `resource(...)` definitions but are scanned
|
|
41
41
|
* for the typed hook calls (`useEntityList<Airport>(…)`) that root the
|
|
42
|
-
* interface
|
|
42
|
+
* class/interface entity graph; co-located helpers are harmless.
|
|
43
43
|
*/
|
|
44
44
|
export function resolveResourceFiles(input: string): string[] {
|
|
45
45
|
const stat = fs.existsSync(input) ? fs.statSync(input) : undefined;
|
package/src/compile/program.ts
CHANGED
|
@@ -20,7 +20,7 @@ export function createProgram(fileNames: string[]): ts.Program {
|
|
|
20
20
|
esModuleInterop: true,
|
|
21
21
|
noEmit: true,
|
|
22
22
|
// `.tsx` app files are scanned for the typed hook calls that root the
|
|
23
|
-
// interface
|
|
23
|
+
// class/interface entity graph.
|
|
24
24
|
jsx: ts.JsxEmit.ReactJSX,
|
|
25
25
|
baseUrl: process.cwd(),
|
|
26
26
|
paths: {
|
package/src/define/db.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Type-level column configuration for interface-driven entities.
|
|
2
|
+
* Type-level column configuration for class- and interface-driven entities.
|
|
3
3
|
*
|
|
4
4
|
* Interface entities are plain TypeScript, so their column constraints are
|
|
5
5
|
* expressed *in the type system*: intersect a property type with one or more
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rastack import` — load external data (CSV, Excel, PDF) and validate it
|
|
3
|
+
* against a resource's constraint state machine.
|
|
4
|
+
*
|
|
5
|
+
* `parseTabular` dispatches on the file extension to the format parsers, all
|
|
6
|
+
* of which produce the same {@link TabularData}; `validateDataset` (in
|
|
7
|
+
* `tabular.ts`) is the shared, pure validation core.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { parseCsv } from "../csv-schema";
|
|
11
|
+
import { parseXlsx } from "./xlsx";
|
|
12
|
+
import { parsePdfTable } from "./pdf";
|
|
13
|
+
import type { TabularData } from "./tabular";
|
|
14
|
+
|
|
15
|
+
export * from "./tabular";
|
|
16
|
+
export { parseXlsx } from "./xlsx";
|
|
17
|
+
export { parsePdfTable } from "./pdf";
|
|
18
|
+
|
|
19
|
+
/** The source formats `rastack import` understands. */
|
|
20
|
+
export type TabularFormat = "csv" | "xlsx" | "pdf";
|
|
21
|
+
|
|
22
|
+
/** Infer the source format from a filename, or `null` if unsupported. */
|
|
23
|
+
export function formatOf(fileName: string): TabularFormat | null {
|
|
24
|
+
const ext = fileName.toLowerCase().split(".").pop();
|
|
25
|
+
if (ext === "csv") return "csv";
|
|
26
|
+
if (ext === "xlsx" || ext === "xlsm") return "xlsx";
|
|
27
|
+
if (ext === "pdf") return "pdf";
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Parse a source file's bytes into headers + rows, by format. */
|
|
32
|
+
export function parseTabular(format: TabularFormat, data: Buffer): TabularData {
|
|
33
|
+
switch (format) {
|
|
34
|
+
case "csv":
|
|
35
|
+
return parseCsv(data.toString("utf-8"));
|
|
36
|
+
case "xlsx":
|
|
37
|
+
return parseXlsx(data);
|
|
38
|
+
case "pdf":
|
|
39
|
+
return parsePdfTable(data);
|
|
40
|
+
}
|
|
41
|
+
}
|