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.
Files changed (49) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/compile/entities.d.ts +0 -49
  3. package/dist/compile/entities.js +110 -22
  4. package/dist/compile/index.d.ts +1 -1
  5. package/dist/compile/index.js +1 -1
  6. package/dist/compile/program.js +1 -1
  7. package/dist/define/db.d.ts +1 -1
  8. package/dist/define/db.js +1 -1
  9. package/dist/import/index.d.ts +18 -0
  10. package/dist/import/index.js +57 -0
  11. package/dist/import/pdf.d.ts +25 -0
  12. package/dist/import/pdf.js +326 -0
  13. package/dist/import/tabular.d.ts +76 -0
  14. package/dist/import/tabular.js +98 -0
  15. package/dist/import/xlsx.d.ts +18 -0
  16. package/dist/import/xlsx.js +160 -0
  17. package/dist/rastack-import.d.ts +22 -0
  18. package/dist/rastack-import.js +165 -0
  19. package/dist/rastack.d.ts +1 -0
  20. package/dist/rastack.js +7 -0
  21. package/dist/validate/adapters.d.ts +42 -0
  22. package/dist/validate/adapters.js +109 -0
  23. package/dist/validate/index.d.ts +2 -0
  24. package/dist/validate/index.js +18 -0
  25. package/dist/validate/machine.d.ts +175 -0
  26. package/dist/validate/machine.js +347 -0
  27. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  28. package/hooks/form/form.ts +16 -5
  29. package/hooks/form/interfaces.ts +15 -0
  30. package/hooks/form/structure.ts +28 -0
  31. package/package.json +1 -1
  32. package/src/compile/entities.ts +161 -59
  33. package/src/compile/index.ts +1 -1
  34. package/src/compile/program.ts +1 -1
  35. package/src/define/db.ts +1 -1
  36. package/src/import/index.ts +41 -0
  37. package/src/import/pdf.ts +304 -0
  38. package/src/import/tabular.ts +159 -0
  39. package/src/import/xlsx.ts +186 -0
  40. package/src/rastack-import.ts +203 -0
  41. package/src/rastack.ts +7 -0
  42. package/src/validate/adapters.ts +118 -0
  43. package/src/validate/index.ts +2 -0
  44. package/src/validate/machine.ts +525 -0
  45. package/test/entities.spec.ts +99 -0
  46. package/test/import.spec.ts +241 -0
  47. package/test/validate.spec.ts +319 -0
  48. package/validate.ts +10 -0
  49. package/wasm/rastack_wasm_bg.wasm +0 -0
@@ -4,55 +4,67 @@ import { FieldModel, RelationModel, ResourceModel, ScalarType } from "./model";
4
4
  import { resourceSourceFiles } from "./program";
5
5
 
6
6
  /**
7
- * Interface-driven resource inference — "the entities in the app are a list of
8
- * interfaces".
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 hooks. The
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. An interface can
17
- * also opt in explicitly with a `@rastackResource` JSDoc tag.
18
- * 2. **Datatypes come from the interfaces.** Each property's TypeScript type
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 interface
27
- * is pulled into the resource graph transitively — the closure of the
28
- * hook-rooted interfaces is the app's resource set.
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 interface Airport {
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 interface Terminal {
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` prefix as
51
- * in generated `IFlight` interfaces is stripped, so hook usage of generated
52
- * types round-trips onto the same resource); `app` defaults to the defining
53
- * file's basename. Both are overridable with `@rastackApp` / `@rastackModel` tags.
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<ts.InterfaceDeclaration>();
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: interfaces used as type arguments of hook calls, plus explicit
85
- // `@rastackResource` opt-ins.
86
- const roots: ts.InterfaceDeclaration[] = [];
87
- const rooted = new Set<ts.InterfaceDeclaration>();
88
- const addRoot = (decl: ts.InterfaceDeclaration) => {
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 = interfaceBehindTypeNode(typeArg, checker, scanned);
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 interface-typed
110
- // property pulls the referenced interface in as an entity too.
111
- const propsByDecl = new Map<ts.InterfaceDeclaration, PropInfo[]>();
112
- const analyzed = (decl: ts.InterfaceDeclaration): PropInfo[] => {
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: ts.InterfaceDeclaration[] = [];
122
- const seen = new Set<ts.InterfaceDeclaration>();
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 an interface declared in one of the scanned files. */
204
- function interfaceBehindTypeNode(
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
- ): ts.InterfaceDeclaration | undefined {
209
- return interfaceBehindType(checker.getTypeFromTypeNode(node), scanned);
223
+ ): EntityDecl | undefined {
224
+ return entityBehindType(checker.getTypeFromTypeNode(node), scanned);
210
225
  }
211
226
 
212
- function interfaceBehindType(
227
+ function entityBehindType(
213
228
  type: ts.Type,
214
229
  scanned: Set<string>,
215
- ): ts.InterfaceDeclaration | undefined {
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 interface (relation resolved after naming). */
236
- target?: ts.InterfaceDeclaration;
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: ts.InterfaceDeclaration,
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
- if (!ts.isPropertySignature(member) || !member.type) continue;
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(member.name) || ts.isStringLiteralLike(member.name)
253
- ? member.name.text
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, member, checker, scanned));
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: ts.PropertySignature,
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 (interfaceBehindTypeNode(element, checker, scanned)) {
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 (→ FK), or
317
- * unsupported. Intersections classify by their first classifiable constituent,
318
- * which is what lets `DbConfig` brands ride along — `string & Unique` is a
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 | ts.InterfaceDeclaration | undefined {
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 = interfaceBehindType(type, scanned);
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: ts.InterfaceDeclaration): EntityNaming {
580
+ function entityNaming(decl: EntityDecl): EntityNaming {
480
581
  const tags = jsdocTags(decl);
481
- const raw = decl.name.text;
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;
@@ -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-driven entity graph; co-located helpers are harmless.
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;
@@ -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-driven entity graph.
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
+ }