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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.0.48](https://github.com/theserverkid/reactapistack/compare/v0.0.47...v0.0.48) (2026-07-13)
|
|
6
|
+
|
|
7
|
+
### [0.0.47](https://github.com/theserverkid/reactapistack/compare/v0.0.46...v0.0.47) (2026-07-13)
|
|
8
|
+
|
|
5
9
|
### [0.0.46](https://github.com/theserverkid/reactapistack/compare/v0.0.45...v0.0.46) (2026-07-13)
|
|
6
10
|
|
|
7
11
|
### [0.0.45](https://github.com/theserverkid/reactapistack/compare/v0.0.44...v0.0.45) (2026-07-13)
|
|
@@ -1,54 +1,5 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import { ResourceModel } from "./model";
|
|
3
|
-
/**
|
|
4
|
-
* Interface-driven resource inference — "the entities in the app are a list of
|
|
5
|
-
* interfaces".
|
|
6
|
-
*
|
|
7
|
-
* Instead of (or alongside) the `resource()` DSL, an app can describe its data
|
|
8
|
-
* as plain TypeScript interfaces and consume them through typed hooks. The
|
|
9
|
-
* analyser works the resource graph out from the *usage*:
|
|
10
|
-
*
|
|
11
|
-
* 1. **Roots come from the hooks.** Every hook call that carries a type
|
|
12
|
-
* argument — `useEntityList<Airport>(…)`, `useQuery<Terminal>(…)` — marks
|
|
13
|
-
* the interface behind that type argument as an entity. An interface can
|
|
14
|
-
* also opt in explicitly with a `@rastackResource` JSDoc tag.
|
|
15
|
-
* 2. **Datatypes come from the interfaces.** Each property's TypeScript type
|
|
16
|
-
* maps to a manifest scalar (`string` → string, `number` → float,
|
|
17
|
-
* `boolean` → bool, `Date` → datetime). Constraints are carried in the
|
|
18
|
-
* type system with `DbConfig` brands from `rastack/types` —
|
|
19
|
-
* `code: string & Unique & MaxLength<3>`, `age: Int` — or, equivalently,
|
|
20
|
-
* as JSDoc tags (`@unique`, `@maxLength 3`, `@int`, `@uuid`, `@default`).
|
|
21
|
-
* Optional / `| null` properties become nullable fields.
|
|
22
|
-
* 3. **Foreign keys come from the type system.** A property whose type is
|
|
23
|
-
* another entity interface is a foreign key, and the referenced interface
|
|
24
|
-
* is pulled into the resource graph transitively — the closure of the
|
|
25
|
-
* hook-rooted interfaces is the app's resource set.
|
|
26
|
-
* 4. **Circular dependencies are detected.** The FK graph is walked for
|
|
27
|
-
* cycles; a cycle is an *error* when every edge in it is required (no row
|
|
28
|
-
* could ever be inserted) and a *warning* when it is breakable through a
|
|
29
|
-
* nullable field (e.g. an optional self-referencing `manager`).
|
|
30
|
-
*
|
|
31
|
-
* ```ts
|
|
32
|
-
* // entities/airports.ts
|
|
33
|
-
* export interface Airport {
|
|
34
|
-
* code: string; // JSDoc tags refine: @maxLength 3, @unique
|
|
35
|
-
* name: string;
|
|
36
|
-
* }
|
|
37
|
-
* export interface Terminal {
|
|
38
|
-
* airport: Airport; // FK — inferred from the property type
|
|
39
|
-
* label: string;
|
|
40
|
-
* }
|
|
41
|
-
*
|
|
42
|
-
* // app/screen.tsx — the hook call is what roots the graph
|
|
43
|
-
* const terminals = useEntityList<Terminal>("terminals");
|
|
44
|
-
* // → resources: airports.terminal (root) + airports.airport (transitive)
|
|
45
|
-
* ```
|
|
46
|
-
*
|
|
47
|
-
* Naming: `model` is the lower-camel interface name (a leading `I` prefix as
|
|
48
|
-
* in generated `IFlight` interfaces is stripped, so hook usage of generated
|
|
49
|
-
* types round-trips onto the same resource); `app` defaults to the defining
|
|
50
|
-
* file's basename. Both are overridable with `@rastackApp` / `@rastackModel` tags.
|
|
51
|
-
*/
|
|
52
3
|
export interface EntityDiagnostic {
|
|
53
4
|
severity: "error" | "warning";
|
|
54
5
|
message: string;
|
package/dist/compile/entities.js
CHANGED
|
@@ -43,17 +43,20 @@ function analyzeEntities(program, fileNames) {
|
|
|
43
43
|
const diagnostics = [];
|
|
44
44
|
const sourceFiles = (0, program_1.resourceSourceFiles)(program, fileNames);
|
|
45
45
|
const scanned = new Set(sourceFiles.map((sf) => path.resolve(sf.fileName)));
|
|
46
|
-
// Every interface declared in the scanned files is a *candidate* entity.
|
|
46
|
+
// Every class/interface declared in the scanned files is a *candidate* entity.
|
|
47
47
|
const candidates = new Set();
|
|
48
48
|
for (const sf of sourceFiles) {
|
|
49
49
|
ts.forEachChild(sf, function walk(node) {
|
|
50
50
|
if (ts.isInterfaceDeclaration(node))
|
|
51
51
|
candidates.add(node);
|
|
52
|
+
if (ts.isClassDeclaration(node) && node.name)
|
|
53
|
+
candidates.add(node);
|
|
52
54
|
ts.forEachChild(node, walk);
|
|
53
55
|
});
|
|
54
56
|
}
|
|
55
|
-
// 1. Roots:
|
|
56
|
-
// `@rastackResource` opt-ins
|
|
57
|
+
// 1. Roots: declarations used as type arguments of hook calls, explicit
|
|
58
|
+
// `@rastackResource` opt-ins, and plain data classes (declaring one *is*
|
|
59
|
+
// the opt-in).
|
|
57
60
|
const roots = [];
|
|
58
61
|
const rooted = new Set();
|
|
59
62
|
const addRoot = (decl) => {
|
|
@@ -66,7 +69,7 @@ function analyzeEntities(program, fileNames) {
|
|
|
66
69
|
ts.forEachChild(sf, function walk(node) {
|
|
67
70
|
if (ts.isCallExpression(node) && isHookCall(node)) {
|
|
68
71
|
for (const typeArg of node.typeArguments ?? []) {
|
|
69
|
-
const decl =
|
|
72
|
+
const decl = entityBehindTypeNode(typeArg, checker, scanned);
|
|
70
73
|
if (decl && candidates.has(decl))
|
|
71
74
|
addRoot(decl);
|
|
72
75
|
}
|
|
@@ -77,9 +80,11 @@ function analyzeEntities(program, fileNames) {
|
|
|
77
80
|
for (const decl of candidates) {
|
|
78
81
|
if (jsdocTags(decl).has("rastackresource"))
|
|
79
82
|
addRoot(decl);
|
|
83
|
+
else if (ts.isClassDeclaration(decl) && isDataClass(decl))
|
|
84
|
+
addRoot(decl);
|
|
80
85
|
}
|
|
81
|
-
// 2. Transitive closure over FK references: an entity's
|
|
82
|
-
// property pulls the referenced
|
|
86
|
+
// 2. Transitive closure over FK references: an entity's entity-typed
|
|
87
|
+
// property pulls the referenced declaration in as an entity too.
|
|
83
88
|
const propsByDecl = new Map();
|
|
84
89
|
const analyzed = (decl) => {
|
|
85
90
|
let props = propsByDecl.get(decl);
|
|
@@ -166,36 +171,93 @@ function isHookCall(call) {
|
|
|
166
171
|
: undefined;
|
|
167
172
|
return !!name && /^use[A-Z0-9_]/.test(name);
|
|
168
173
|
}
|
|
169
|
-
/** Resolve a type node to
|
|
170
|
-
function
|
|
171
|
-
return
|
|
174
|
+
/** Resolve a type node to a class/interface declared in a scanned file. */
|
|
175
|
+
function entityBehindTypeNode(node, checker, scanned) {
|
|
176
|
+
return entityBehindType(checker.getTypeFromTypeNode(node), scanned);
|
|
172
177
|
}
|
|
173
|
-
function
|
|
178
|
+
function entityBehindType(type, scanned) {
|
|
174
179
|
const symbol = type.aliasSymbol ?? type.getSymbol();
|
|
175
180
|
for (const decl of symbol?.declarations ?? []) {
|
|
176
|
-
if (ts.isInterfaceDeclaration(decl)
|
|
181
|
+
if ((ts.isInterfaceDeclaration(decl) ||
|
|
182
|
+
(ts.isClassDeclaration(decl) && decl.name)) &&
|
|
177
183
|
scanned.has(path.resolve(decl.getSourceFile().fileName))) {
|
|
178
184
|
return decl;
|
|
179
185
|
}
|
|
180
186
|
}
|
|
181
187
|
return undefined;
|
|
182
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* A *plain data class* — the class-authored equivalent of a `resource()` call,
|
|
191
|
+
* so it roots itself. It must carry no heritage clause (a subclass of
|
|
192
|
+
* `React.Component` or a service base is app code, not a table), not be
|
|
193
|
+
* abstract, contain nothing but property declarations, and have at least one
|
|
194
|
+
* instance property. Anything richer can still opt in via a hook or the
|
|
195
|
+
* `@rastackResource` tag.
|
|
196
|
+
*/
|
|
197
|
+
function isDataClass(decl) {
|
|
198
|
+
if (decl.heritageClauses?.length)
|
|
199
|
+
return false;
|
|
200
|
+
if (ts.getCombinedModifierFlags(decl) &
|
|
201
|
+
(ts.ModifierFlags.Abstract | ts.ModifierFlags.Ambient)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
let properties = 0;
|
|
205
|
+
for (const member of decl.members) {
|
|
206
|
+
if (!ts.isPropertyDeclaration(member))
|
|
207
|
+
return false; // method/ctor/accessor
|
|
208
|
+
if (ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static)
|
|
209
|
+
continue;
|
|
210
|
+
properties += 1;
|
|
211
|
+
}
|
|
212
|
+
return properties > 0;
|
|
213
|
+
}
|
|
183
214
|
function analyzeProperties(decl, checker, scanned) {
|
|
184
215
|
const props = [];
|
|
185
216
|
for (const member of decl.members) {
|
|
186
|
-
|
|
217
|
+
let prop;
|
|
218
|
+
if (ts.isPropertySignature(member) && member.type) {
|
|
219
|
+
prop = member;
|
|
220
|
+
}
|
|
221
|
+
else if (ts.isPropertyDeclaration(member) &&
|
|
222
|
+
(member.type || member.initializer) &&
|
|
223
|
+
!ts.isPrivateIdentifier(member.name) &&
|
|
224
|
+
!(ts.getCombinedModifierFlags(member) &
|
|
225
|
+
(ts.ModifierFlags.Static |
|
|
226
|
+
ts.ModifierFlags.Private |
|
|
227
|
+
ts.ModifierFlags.Protected))) {
|
|
228
|
+
prop = member;
|
|
229
|
+
}
|
|
230
|
+
if (!prop)
|
|
187
231
|
continue;
|
|
188
|
-
const name = ts.isIdentifier(
|
|
189
|
-
?
|
|
232
|
+
const name = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name)
|
|
233
|
+
? prop.name.text
|
|
190
234
|
: undefined;
|
|
191
235
|
if (!name)
|
|
192
236
|
continue;
|
|
193
|
-
props.push(analyzeProperty(name,
|
|
237
|
+
props.push(analyzeProperty(name, prop, checker, scanned));
|
|
194
238
|
}
|
|
195
239
|
return props;
|
|
196
240
|
}
|
|
197
241
|
function analyzeProperty(name, member, checker, scanned) {
|
|
198
242
|
let nullable = !!member.questionToken;
|
|
243
|
+
// A class field with no annotation infers its (widened) type from the
|
|
244
|
+
// initializer — `region = "EU"` is a string column defaulting to "EU".
|
|
245
|
+
if (!member.type) {
|
|
246
|
+
const type = checker.getTypeAtLocation(member);
|
|
247
|
+
const base = classifyType(type, scanned);
|
|
248
|
+
if (base === undefined) {
|
|
249
|
+
return { name, nullable, skip: "unsupported type — skipped" };
|
|
250
|
+
}
|
|
251
|
+
if (typeof base !== "string")
|
|
252
|
+
return { name, nullable, target: base };
|
|
253
|
+
const config = configFromTags(jsdocTags(member));
|
|
254
|
+
if (config.default === undefined) {
|
|
255
|
+
const dflt = initializerDefault(member);
|
|
256
|
+
if (dflt !== undefined)
|
|
257
|
+
config.default = dflt;
|
|
258
|
+
}
|
|
259
|
+
return scalarProp(name, base, config, nullable);
|
|
260
|
+
}
|
|
199
261
|
// Unwrap parens and unions: strip null/undefined (→ nullable), and require a
|
|
200
262
|
// single remaining arm (unions of string literals collapse to `string`).
|
|
201
263
|
let typeNode = unwrapParens(member.type);
|
|
@@ -219,7 +281,7 @@ function analyzeProperty(name, member, checker, scanned) {
|
|
|
219
281
|
// but is intentionally not an error. Arrays of scalars have no manifest shape.
|
|
220
282
|
const element = arrayElement(typeNode);
|
|
221
283
|
if (element) {
|
|
222
|
-
if (
|
|
284
|
+
if (entityBehindTypeNode(element, checker, scanned)) {
|
|
223
285
|
return { name, nullable, inverse: true };
|
|
224
286
|
}
|
|
225
287
|
return { name, nullable, skip: "array fields are not supported — skipped" };
|
|
@@ -230,19 +292,44 @@ function analyzeProperty(name, member, checker, scanned) {
|
|
|
230
292
|
return { name, nullable, skip: "unsupported type — skipped" };
|
|
231
293
|
}
|
|
232
294
|
if (typeof base !== "string") {
|
|
233
|
-
return { name, nullable, target: base }; // entity interface → FK
|
|
295
|
+
return { name, nullable, target: base }; // entity class/interface → FK
|
|
234
296
|
}
|
|
235
297
|
const config = {
|
|
236
298
|
...configFromTags(jsdocTags(member)),
|
|
237
299
|
...configFromType(type, checker, member),
|
|
238
300
|
};
|
|
301
|
+
if (config.default === undefined) {
|
|
302
|
+
const dflt = initializerDefault(member);
|
|
303
|
+
if (dflt !== undefined)
|
|
304
|
+
config.default = dflt;
|
|
305
|
+
}
|
|
239
306
|
return scalarProp(name, base, config, nullable);
|
|
240
307
|
}
|
|
308
|
+
/** The literal value of a class field's initializer, used as the default. */
|
|
309
|
+
function initializerDefault(member) {
|
|
310
|
+
if (!ts.isPropertyDeclaration(member) || !member.initializer)
|
|
311
|
+
return undefined;
|
|
312
|
+
const init = member.initializer;
|
|
313
|
+
if (ts.isStringLiteralLike(init))
|
|
314
|
+
return init.text;
|
|
315
|
+
if (ts.isNumericLiteral(init))
|
|
316
|
+
return Number(init.text);
|
|
317
|
+
if (ts.isPrefixUnaryExpression(init) &&
|
|
318
|
+
init.operator === ts.SyntaxKind.MinusToken &&
|
|
319
|
+
ts.isNumericLiteral(init.operand)) {
|
|
320
|
+
return -Number(init.operand.text);
|
|
321
|
+
}
|
|
322
|
+
if (init.kind === ts.SyntaxKind.TrueKeyword)
|
|
323
|
+
return true;
|
|
324
|
+
if (init.kind === ts.SyntaxKind.FalseKeyword)
|
|
325
|
+
return false;
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
241
328
|
/**
|
|
242
|
-
* What a property type *is*: a manifest scalar, an entity interface
|
|
243
|
-
* unsupported. Intersections classify by their first classifiable
|
|
244
|
-
* which is what lets `DbConfig` brands ride along — `string &
|
|
245
|
-
* string; the brand carries configuration, not a datatype.
|
|
329
|
+
* What a property type *is*: a manifest scalar, an entity class/interface
|
|
330
|
+
* (→ FK), or unsupported. Intersections classify by their first classifiable
|
|
331
|
+
* constituent, which is what lets `DbConfig` brands ride along — `string &
|
|
332
|
+
* Unique` is a string; the brand carries configuration, not a datatype.
|
|
246
333
|
*/
|
|
247
334
|
function classifyType(type, scanned) {
|
|
248
335
|
if (type.flags & ts.TypeFlags.StringLike)
|
|
@@ -261,7 +348,7 @@ function classifyType(type, scanned) {
|
|
|
261
348
|
}
|
|
262
349
|
if (type.getProperty("__rastackDb"))
|
|
263
350
|
return undefined; // a bare config brand
|
|
264
|
-
const decl =
|
|
351
|
+
const decl = entityBehindType(type, scanned);
|
|
265
352
|
if (decl)
|
|
266
353
|
return decl;
|
|
267
354
|
if (type.getSymbol()?.getName() === "Date")
|
|
@@ -390,6 +477,7 @@ function arrayElement(node) {
|
|
|
390
477
|
}
|
|
391
478
|
function entityNaming(decl) {
|
|
392
479
|
const tags = jsdocTags(decl);
|
|
480
|
+
// Class candidates are only ever collected with a name (see analyzeEntities).
|
|
393
481
|
const raw = decl.name.text;
|
|
394
482
|
// Generated entity interfaces are `I`-prefixed (`IFlight`); strip the prefix
|
|
395
483
|
// so hook usage of generated types resolves to the same resource.
|
package/dist/compile/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface CompileResult {
|
|
|
19
19
|
* beside `app/airports/index.tsx` — instead of in one top-level `resources/`
|
|
20
20
|
* folder. `.tsx` files carry no `resource(...)` definitions but are scanned
|
|
21
21
|
* for the typed hook calls (`useEntityList<Airport>(…)`) that root the
|
|
22
|
-
* interface
|
|
22
|
+
* class/interface entity graph; co-located helpers are harmless.
|
|
23
23
|
*/
|
|
24
24
|
export declare function resolveResourceFiles(input: string): string[];
|
|
25
25
|
export interface ProjectAnalysis {
|
package/dist/compile/index.js
CHANGED
|
@@ -77,7 +77,7 @@ const IGNORE_DIRS = new Set([
|
|
|
77
77
|
* beside `app/airports/index.tsx` — instead of in one top-level `resources/`
|
|
78
78
|
* folder. `.tsx` files carry no `resource(...)` definitions but are scanned
|
|
79
79
|
* for the typed hook calls (`useEntityList<Airport>(…)`) that root the
|
|
80
|
-
* interface
|
|
80
|
+
* class/interface entity graph; co-located helpers are harmless.
|
|
81
81
|
*/
|
|
82
82
|
function resolveResourceFiles(input) {
|
|
83
83
|
const stat = fs.existsSync(input) ? fs.statSync(input) : undefined;
|
package/dist/compile/program.js
CHANGED
|
@@ -55,7 +55,7 @@ function createProgram(fileNames) {
|
|
|
55
55
|
esModuleInterop: true,
|
|
56
56
|
noEmit: true,
|
|
57
57
|
// `.tsx` app files are scanned for the typed hook calls that root the
|
|
58
|
-
// interface
|
|
58
|
+
// class/interface entity graph.
|
|
59
59
|
jsx: ts.JsxEmit.ReactJSX,
|
|
60
60
|
baseUrl: process.cwd(),
|
|
61
61
|
paths: {
|
package/dist/define/db.d.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
|
package/dist/define/db.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Type-level column configuration for interface-driven entities.
|
|
3
|
+
* Type-level column configuration for class- and interface-driven entities.
|
|
4
4
|
*
|
|
5
5
|
* Interface entities are plain TypeScript, so their column constraints are
|
|
6
6
|
* expressed *in the type system*: intersect a property type with one or more
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
import type { TabularData } from "./tabular";
|
|
10
|
+
export * from "./tabular";
|
|
11
|
+
export { parseXlsx } from "./xlsx";
|
|
12
|
+
export { parsePdfTable } from "./pdf";
|
|
13
|
+
/** The source formats `rastack import` understands. */
|
|
14
|
+
export type TabularFormat = "csv" | "xlsx" | "pdf";
|
|
15
|
+
/** Infer the source format from a filename, or `null` if unsupported. */
|
|
16
|
+
export declare function formatOf(fileName: string): TabularFormat | null;
|
|
17
|
+
/** Parse a source file's bytes into headers + rows, by format. */
|
|
18
|
+
export declare function parseTabular(format: TabularFormat, data: Buffer): TabularData;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `rastack import` — load external data (CSV, Excel, PDF) and validate it
|
|
4
|
+
* against a resource's constraint state machine.
|
|
5
|
+
*
|
|
6
|
+
* `parseTabular` dispatches on the file extension to the format parsers, all
|
|
7
|
+
* of which produce the same {@link TabularData}; `validateDataset` (in
|
|
8
|
+
* `tabular.ts`) is the shared, pure validation core.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
22
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.parsePdfTable = exports.parseXlsx = void 0;
|
|
26
|
+
exports.formatOf = formatOf;
|
|
27
|
+
exports.parseTabular = parseTabular;
|
|
28
|
+
const csv_schema_1 = require("../csv-schema");
|
|
29
|
+
const xlsx_1 = require("./xlsx");
|
|
30
|
+
const pdf_1 = require("./pdf");
|
|
31
|
+
__exportStar(require("./tabular"), exports);
|
|
32
|
+
var xlsx_2 = require("./xlsx");
|
|
33
|
+
Object.defineProperty(exports, "parseXlsx", { enumerable: true, get: function () { return xlsx_2.parseXlsx; } });
|
|
34
|
+
var pdf_2 = require("./pdf");
|
|
35
|
+
Object.defineProperty(exports, "parsePdfTable", { enumerable: true, get: function () { return pdf_2.parsePdfTable; } });
|
|
36
|
+
/** Infer the source format from a filename, or `null` if unsupported. */
|
|
37
|
+
function formatOf(fileName) {
|
|
38
|
+
const ext = fileName.toLowerCase().split(".").pop();
|
|
39
|
+
if (ext === "csv")
|
|
40
|
+
return "csv";
|
|
41
|
+
if (ext === "xlsx" || ext === "xlsm")
|
|
42
|
+
return "xlsx";
|
|
43
|
+
if (ext === "pdf")
|
|
44
|
+
return "pdf";
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** Parse a source file's bytes into headers + rows, by format. */
|
|
48
|
+
function parseTabular(format, data) {
|
|
49
|
+
switch (format) {
|
|
50
|
+
case "csv":
|
|
51
|
+
return (0, csv_schema_1.parseCsv)(data.toString("utf-8"));
|
|
52
|
+
case "xlsx":
|
|
53
|
+
return (0, xlsx_1.parseXlsx)(data);
|
|
54
|
+
case "pdf":
|
|
55
|
+
return (0, pdf_1.parsePdfTable)(data);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, dependency-free PDF *table* extractor for `rastack import`.
|
|
3
|
+
*
|
|
4
|
+
* PDFs carry no table structure — only positioned text — so this is
|
|
5
|
+
* deliberately a best-effort extractor for the common case of a machine-
|
|
6
|
+
* generated tabular report: it decodes every content stream (FlateDecode via
|
|
7
|
+
* node's zlib, plus raw streams), replays the text-positioning operators
|
|
8
|
+
* (`Tm`/`Td`/`TD`/`T*`) to give each shown string (`Tj`/`TJ`/`'`) an (x, y),
|
|
9
|
+
* clusters strings that share a baseline into rows, and orders each row's
|
|
10
|
+
* cells left-to-right. The first row becomes the header.
|
|
11
|
+
*
|
|
12
|
+
* Anything a text-line model can't represent — merged cells, wrapped cell
|
|
13
|
+
* text, scanned images — won't survive; for those, export the source data to
|
|
14
|
+
* CSV/XLSX instead. Every extracted value still goes through the constraint
|
|
15
|
+
* state machine, so a misread cell is *rejected with a located error*, never
|
|
16
|
+
* silently imported.
|
|
17
|
+
*/
|
|
18
|
+
import type { TabularData } from "./tabular";
|
|
19
|
+
/**
|
|
20
|
+
* Parse a PDF buffer into headers + rows: text chunks sharing a baseline form
|
|
21
|
+
* a row (top of the page first), ordered left-to-right; the first row is the
|
|
22
|
+
* header. Rows with a different cell count than the header are padded or
|
|
23
|
+
* truncated — the state machine will flag what doesn't validate.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parsePdfTable(buf: Buffer): TabularData;
|