firebase-contract 0.1.0-rc.12 → 0.1.0-rc.14

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 (37) hide show
  1. package/README.md +22 -5
  2. package/dist/lib/_internal/generators/support/split.d.ts +1 -1
  3. package/dist/lib/_internal/generators/support/split.js +1 -1
  4. package/dist/lib/_internal/generators/typescript/render.d.ts +15 -0
  5. package/dist/lib/_internal/generators/typescript/render.d.ts.map +1 -0
  6. package/dist/lib/_internal/generators/typescript/render.js +92 -0
  7. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts +2 -1
  8. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -1
  9. package/dist/lib/_internal/generators/typescript/typescript-generator.js +5 -75
  10. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -1
  11. package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +2 -82
  12. package/dist/lib/_internal/generators/zod/render.d.ts +12 -0
  13. package/dist/lib/_internal/generators/zod/render.d.ts.map +1 -0
  14. package/dist/lib/_internal/generators/zod/render.js +59 -0
  15. package/dist/lib/_internal/generators/zod/zod-generator.d.ts +2 -1
  16. package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -1
  17. package/dist/lib/_internal/generators/zod/zod-generator.js +5 -52
  18. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -1
  19. package/dist/lib/_internal/generators/zod/zod-split-generator.js +2 -54
  20. package/dist/lib/_internal/ir/build-ir.d.ts.map +1 -1
  21. package/dist/lib/_internal/ir/build-ir.js +62 -7
  22. package/dist/lib/_internal/ir/ir.d.ts +0 -1
  23. package/dist/lib/_internal/ir/ir.d.ts.map +1 -1
  24. package/dist/lib/_internal/parser/parse.d.ts +5 -3
  25. package/dist/lib/_internal/parser/parse.d.ts.map +1 -1
  26. package/dist/lib/_internal/parser/parse.js +69 -12
  27. package/dist/lib/_internal/parser/raw-document.d.ts +0 -1
  28. package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -1
  29. package/dist/lib/_internal/resolver/import-resolver.js +1 -1
  30. package/dist/lib/cli/commands/generate.d.ts.map +1 -1
  31. package/dist/lib/cli/commands/generate.js +6 -4
  32. package/dist/lib/cli/commands/init.d.ts.map +1 -1
  33. package/dist/lib/cli/commands/init.js +2 -18
  34. package/dist/lib/cli/config.d.ts +3 -2
  35. package/dist/lib/cli/config.d.ts.map +1 -1
  36. package/dist/lib/cli/config.js +3 -0
  37. package/package.json +1 -1
package/README.md CHANGED
@@ -123,8 +123,9 @@ generators:
123
123
 
124
124
  `fbc generate contract.yml` materializes **all** declared outputs across the
125
125
  whole import graph in one run. Passing `-o`/`-g` switches to single-target mode
126
- (`fbc generate <entry> -o <dir> -g typescript,zod`), and
127
- `firebase-contract.json` (`entry`, `outDir`, `generators`) can hold defaults.
126
+ (`fbc generate <entry> -o <dir> -g typescript,zod`). `firebase-contract.json`
127
+ holds only a default `entry` — output routing lives in the yml `generators:`
128
+ declarations, so a config file can never silently override them.
128
129
 
129
130
 
130
131
  ### Generated-file headers
@@ -193,8 +194,21 @@ firestore module is checked under `fsName ?? name` alongside doc names. A bare
193
194
  type reference therefore always resolves to exactly one definition — use
194
195
  `fbc where <Name>` to find it.
195
196
 
197
+ **Unknown keys are errors.** Every key the parser does not consume is an
198
+ `UNKNOWN_KEY` error (all reported in one pass), never silently dropped — a
199
+ typo like `optionnal:` must not quietly turn into a required field. There is
200
+ no `version:` field: nothing consumes one, so accepting it would only fake a
201
+ compatibility mechanism that does not exist.
202
+
203
+ **Out-of-vocabulary values are errors too.** A value outside a field's fixed
204
+ set is `INVALID_VALUE` rather than a silent coercion: `auth: PUBILC` does not
205
+ quietly become `NO_ACCESS`, `dir: Dsec` does not become `ASC`, and a mistyped
206
+ `action` / `single` / `keyArg` / `style.*` is not dropped. The one exception is
207
+ `where.op`, which is warned (`UNKNOWN_WHERE_OP`) not errored — it is emitted
208
+ verbatim into GraphQL and the recognized-operator list may lag Data Connect, so
209
+ a typo is surfaced without rejecting a valid-but-unlisted operator.
210
+
196
211
  ```yaml
197
- version: 1
198
212
  imports:
199
213
  - ./apps/shop/data-connect/schema.yml
200
214
  - some-shared-package/contract.yml
@@ -487,8 +501,11 @@ firestore:
487
501
  ```
488
502
 
489
503
  Emits Zod schemas + inferred types, mirroring a hand-written Firestore schema
490
- library. (The simpler `firestore-types` generator just re-types *every* model
491
- with the Firestore `Timestamp` and does not denormalize.)
504
+ library. (`firestore-types` is the zero-config entry point for projects that
505
+ have not written projections yet: it re-types *every* model with the Firestore
506
+ `Timestamp`, no `firestore:` section needed. Once you declare projections,
507
+ prefer the `firestore` generator — it is the one that understands
508
+ pick/omit/fsName/denormalized fields.)
492
509
 
493
510
  With `firestore` + `split: true`, fields whose Zod chain is **identical** to the Data
494
511
  Connect schema are reused via `.pick()` — only representation changes are
@@ -18,7 +18,7 @@ export declare const collectDeps: (ir: Ir, roots: IrModel[]) => HostedDeps;
18
18
  /**
19
19
  * Assign a unique module basename to every table. Two models may share a
20
20
  * physical table name across services (e.g. both declare
21
- * `reconciliation_operations`); the first keeps the table-derived name and
21
+ * `audit_logs`); the first keeps the table-derived name and
22
22
  * later ones fall back to their model name so files never overwrite each other.
23
23
  */
24
24
  export declare const assignModuleNames: (tables: IrModel[]) => Map<string, string>;
@@ -52,7 +52,7 @@ export const collectDeps = (ir, roots) => {
52
52
  /**
53
53
  * Assign a unique module basename to every table. Two models may share a
54
54
  * physical table name across services (e.g. both declare
55
- * `reconciliation_operations`); the first keeps the table-derived name and
55
+ * `audit_logs`); the first keeps the table-derived name and
56
56
  * later ones fall back to their model name so files never overwrite each other.
57
57
  */
58
58
  export const assignModuleNames = (tables) => {
@@ -0,0 +1,15 @@
1
+ import { Ir, IrEnum, IrField, IrModel, ScalarType } from '../../ir/ir.js';
2
+ /**
3
+ * Rendering shared by the bundled and split TypeScript layouts. Both layouts
4
+ * MUST go through these helpers so a contract setting (valueKeys, nullable,
5
+ * multi-line descriptions, …) means the same thing regardless of layout.
6
+ */
7
+ export declare const SCALAR_TS: Record<ScalarType, string>;
8
+ export declare const JSON_TYPE: string;
9
+ export declare const docLines: (description: string | undefined, indent: string) => string[];
10
+ export declare const renderField: (field: IrField, ir: Ir) => string;
11
+ export declare const renderEnumConst: (irEnum: IrEnum) => string;
12
+ export declare const renderEnumUnion: (irEnum: IrEnum) => string;
13
+ export declare const renderModel: (model: IrModel, ir: Ir) => string;
14
+ export declare const usesJson: (models: IrModel[]) => boolean;
15
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/typescript/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAa,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAIpF;;;;GAIG;AAEH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAUhD,CAAA;AAED,eAAO,MAAM,SAAS,QAQV,CAAA;AAcZ,eAAO,MAAM,QAAQ,GAAI,aAAa,MAAM,GAAG,SAAS,EAAE,QAAQ,MAAM,KAAG,MAAM,EAKhF,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,EAAE,IAAI,EAAE,KAAG,MAiBpD,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,KAAG,MAchD,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,KAAG,MAIhD,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,EAAE,IAAI,EAAE,KAAG,MAIpD,CAAA;AAED,eAAO,MAAM,QAAQ,GAAI,QAAQ,OAAO,EAAE,KAAG,OACiE,CAAA"}
@@ -0,0 +1,92 @@
1
+ import { constantCase, singleQuote } from '../support/naming.js';
2
+ import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
3
+ /**
4
+ * Rendering shared by the bundled and split TypeScript layouts. Both layouts
5
+ * MUST go through these helpers so a contract setting (valueKeys, nullable,
6
+ * multi-line descriptions, …) means the same thing regardless of layout.
7
+ */
8
+ export const SCALAR_TS = {
9
+ string: 'string',
10
+ int: 'number',
11
+ int64: 'number',
12
+ float: 'number',
13
+ boolean: 'boolean',
14
+ timestamp: 'string',
15
+ date: 'string',
16
+ json: 'Json',
17
+ id: 'string',
18
+ };
19
+ export const JSON_TYPE = [
20
+ 'export type Json =',
21
+ ' | string',
22
+ ' | number',
23
+ ' | boolean',
24
+ ' | null',
25
+ ' | Json[]',
26
+ ' | { [key: string]: Json }',
27
+ ].join('\n');
28
+ const renderRef = (ref) => {
29
+ switch (ref.kind) {
30
+ case 'scalar':
31
+ return SCALAR_TS[ref.name];
32
+ case 'enum':
33
+ case 'model':
34
+ return ref.name;
35
+ default:
36
+ return 'unknown';
37
+ }
38
+ };
39
+ export const docLines = (description, indent) => {
40
+ if (!description)
41
+ return [];
42
+ const lines = description.split('\n');
43
+ if (lines.length === 1)
44
+ return [`${indent}/** ${lines[0]} */`];
45
+ return [`${indent}/**`, ...lines.map(line => `${indent} * ${line}`.trimEnd()), `${indent} */`];
46
+ };
47
+ export const renderField = (field, ir) => {
48
+ const optional = field.optional ? '?' : '';
49
+ const doc = docLines(field.description, ' ');
50
+ let entry;
51
+ if (field.literal !== undefined) {
52
+ const type = field.list ? `${singleQuote(field.literal)}[]` : singleQuote(field.literal);
53
+ entry = ` ${field.name}${optional}: ${type}`;
54
+ }
55
+ else if (isRelation(field)) {
56
+ const base = SCALAR_TS[relationFkType(ir, field)];
57
+ entry = ` ${relationFkName(field)}${optional}: ${field.list ? `${base}[]` : base}`;
58
+ }
59
+ else {
60
+ const base = renderRef(field.type);
61
+ const type = field.list ? `${base}[]` : base;
62
+ const nullable = field.nullable ? ' | null' : '';
63
+ entry = ` ${field.name}${optional}: ${type}${nullable}`;
64
+ }
65
+ return [...doc, entry].join('\n');
66
+ };
67
+ export const renderEnumConst = (irEnum) => {
68
+ const doc = docLines(irEnum.description, '');
69
+ const constName = constantCase(irEnum.name);
70
+ const entries = irEnum.values
71
+ .map(value => ` ${irEnum.valueKeys?.[value] ?? constantCase(value)}: ${singleQuote(value)},`)
72
+ .join('\n');
73
+ return [
74
+ ...doc,
75
+ `export const ${constName} = Object.freeze({`,
76
+ entries,
77
+ `} as const)`,
78
+ `export type ${irEnum.name}Key = keyof typeof ${constName}`,
79
+ `export type ${irEnum.name} = (typeof ${constName})[${irEnum.name}Key]`,
80
+ ].join('\n');
81
+ };
82
+ export const renderEnumUnion = (irEnum) => {
83
+ const doc = docLines(irEnum.description, '');
84
+ const union = irEnum.values.map(value => singleQuote(value)).join(' | ') || 'never';
85
+ return [...doc, `export type ${irEnum.name} = ${union}`].join('\n');
86
+ };
87
+ export const renderModel = (model, ir) => {
88
+ const doc = docLines(model.description, '');
89
+ const fields = model.fields.map(field => renderField(field, ir)).join('\n');
90
+ return [...doc, `export interface ${model.name} {`, fields, '}'].join('\n');
91
+ };
92
+ export const usesJson = (models) => models.some(model => model.fields.some(field => field.type.kind === 'scalar' && field.type.name === 'json'));
@@ -12,7 +12,8 @@ export interface TypeScriptGeneratorOptions {
12
12
  * Generates TypeScript types: an interface per model and, per enum, either a
13
13
  * frozen const object (default — runtime values + `Key`/value types) or a
14
14
  * plain string-literal union. This is the highest-priority
15
- * generator and the shape other TS-facing generators reference.
15
+ * generator and the shape other TS-facing generators reference. Rendering is
16
+ * shared with the split layout (./render.js) so both emit identical members.
16
17
  */
17
18
  export declare const createTypeScriptGenerator: (options?: TypeScriptGeneratorOptions) => Generator;
18
19
  //# sourceMappingURL=typescript-generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"typescript-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/typescript/typescript-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAQ5E,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;CAC9B;AA+ED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,GAAI,UAAS,0BAA+B,KAAG,SA4BpF,CAAA"}
1
+ {"version":3,"file":"typescript-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/typescript/typescript-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAO5E,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;CAC9B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GAAI,UAAS,0BAA+B,KAAG,SA0BpF,CAAA"}
@@ -1,82 +1,14 @@
1
1
  import { headerBlocks } from '../support/header.js';
2
2
  import { withSplitVariant } from '../support/split-variant.js';
3
3
  import { createTypeScriptSplitLayout } from './typescript-split-generator.js';
4
- import { constantCase, singleQuote } from '../support/naming.js';
5
- import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
6
4
  import { outputFile } from '../support/templates.js';
7
- const SCALAR_TS = {
8
- string: 'string',
9
- int: 'number',
10
- int64: 'number',
11
- float: 'number',
12
- boolean: 'boolean',
13
- timestamp: 'string',
14
- date: 'string',
15
- json: 'Json',
16
- id: 'string',
17
- };
18
- const JSON_TYPE = [
19
- 'export type Json =',
20
- ' | string',
21
- ' | number',
22
- ' | boolean',
23
- ' | null',
24
- ' | Json[]',
25
- ' | { [key: string]: Json }',
26
- ].join('\n');
27
- const renderRef = (ref) => {
28
- switch (ref.kind) {
29
- case 'scalar':
30
- return SCALAR_TS[ref.name];
31
- case 'enum':
32
- case 'model':
33
- return ref.name;
34
- default:
35
- return 'unknown';
36
- }
37
- };
38
- const renderFieldType = (field) => {
39
- const base = renderRef(field.type);
40
- return field.list ? `${base}[]` : base;
41
- };
42
- const renderField = (field, ir) => {
43
- const optional = field.optional ? '?' : '';
44
- const doc = field.description ? ` /** ${field.description} */\n` : '';
45
- if (field.literal !== undefined) {
46
- const type = field.list ? `${singleQuote(field.literal)}[]` : singleQuote(field.literal);
47
- return `${doc} ${field.name}${optional}: ${type}`;
48
- }
49
- // Relation: expose the foreign-key id (matching the persisted/projected shape).
50
- if (isRelation(field)) {
51
- const base = SCALAR_TS[relationFkType(ir, field)];
52
- const type = field.list ? `${base}[]` : base;
53
- return `${doc} ${relationFkName(field)}${optional}: ${type}`;
54
- }
55
- return `${doc} ${field.name}${optional}: ${renderFieldType(field)}`;
56
- };
57
- const usesJson = (ir) => ir.models.some(model => model.fields.some(field => field.type.kind === 'scalar' && field.type.name === 'json'));
58
- const renderEnumUnion = (irEnum) => {
59
- const doc = irEnum.description ? `/** ${irEnum.description} */\n` : '';
60
- const union = irEnum.values.map(value => singleQuote(value)).join(' | ') || 'never';
61
- return `${doc}export type ${irEnum.name} = ${union}`;
62
- };
63
- const renderEnumConst = (irEnum) => {
64
- const doc = irEnum.description ? `/** ${irEnum.description} */\n` : '';
65
- const constName = constantCase(irEnum.name);
66
- const entries = irEnum.values.map(value => ` ${constantCase(value)}: ${singleQuote(value)},`).join('\n');
67
- return [
68
- `${doc}export const ${constName} = Object.freeze({`,
69
- entries,
70
- `} as const)`,
71
- `export type ${irEnum.name}Key = keyof typeof ${constName}`,
72
- `export type ${irEnum.name} = (typeof ${constName})[${irEnum.name}Key]`,
73
- ].join('\n');
74
- };
5
+ import { JSON_TYPE, renderEnumConst, renderEnumUnion, renderModel, usesJson } from './render.js';
75
6
  /**
76
7
  * Generates TypeScript types: an interface per model and, per enum, either a
77
8
  * frozen const object (default — runtime values + `Key`/value types) or a
78
9
  * plain string-literal union. This is the highest-priority
79
- * generator and the shape other TS-facing generators reference.
10
+ * generator and the shape other TS-facing generators reference. Rendering is
11
+ * shared with the split layout (./render.js) so both emit identical members.
80
12
  */
81
13
  export const createTypeScriptGenerator = (options = {}) => {
82
14
  const enumStyle = options.enumStyle ?? 'const';
@@ -85,16 +17,14 @@ export const createTypeScriptGenerator = (options = {}) => {
85
17
  description: 'TypeScript interfaces and enum const/union types',
86
18
  generate(ir, context) {
87
19
  const blocks = [...headerBlocks(context)];
88
- if (usesJson(ir)) {
20
+ if (usesJson(ir.models)) {
89
21
  blocks.push(JSON_TYPE);
90
22
  }
91
23
  for (const irEnum of ir.enums) {
92
24
  blocks.push(enumStyle === 'const' ? renderEnumConst(irEnum) : renderEnumUnion(irEnum));
93
25
  }
94
26
  for (const model of ir.models) {
95
- const doc = model.description ? `/** ${model.description} */\n` : '';
96
- const fields = model.fields.map(field => renderField(field, ir)).join('\n');
97
- blocks.push(`${doc}export interface ${model.name} {\n${fields}\n}`);
27
+ blocks.push(renderModel(model, ir));
98
28
  }
99
29
  return [{ path: outputFile(context, 'types.ts'), content: `${blocks.join('\n\n')}\n` }];
100
30
  },
@@ -1 +1 @@
1
- {"version":3,"file":"typescript-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/typescript/typescript-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AA4F5E;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,QAAO,SA0E7C,CAAA"}
1
+ {"version":3,"file":"typescript-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/typescript/typescript-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAO5E;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,QAAO,SA0E7C,CAAA"}
@@ -1,89 +1,9 @@
1
1
  import { findEnum, findModel } from '../../ir/ir.js';
2
2
  import { headerBlocks } from '../support/header.js';
3
- import { constantCase, singleQuote } from '../support/naming.js';
4
- import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
3
+ import { isRelation } from '../support/relations.js';
5
4
  import { assignModuleNames, collectDeps, splitGroups } from '../support/split.js';
6
5
  import { outputFile } from '../support/templates.js';
7
- const SCALAR_TS = {
8
- string: 'string',
9
- int: 'number',
10
- int64: 'number',
11
- float: 'number',
12
- boolean: 'boolean',
13
- timestamp: 'string',
14
- date: 'string',
15
- json: 'Json',
16
- id: 'string',
17
- };
18
- const JSON_TYPE = [
19
- 'export type Json =',
20
- ' | string',
21
- ' | number',
22
- ' | boolean',
23
- ' | null',
24
- ' | Json[]',
25
- ' | { [key: string]: Json }',
26
- ].join('\n');
27
- const renderRef = (ref) => {
28
- switch (ref.kind) {
29
- case 'scalar':
30
- return SCALAR_TS[ref.name];
31
- case 'enum':
32
- case 'model':
33
- return ref.name;
34
- default:
35
- return 'unknown';
36
- }
37
- };
38
- const docLines = (description, indent) => {
39
- if (!description)
40
- return [];
41
- const lines = description.split('\n');
42
- if (lines.length === 1)
43
- return [`${indent}/** ${lines[0]} */`];
44
- return [`${indent}/**`, ...lines.map(line => `${indent} * ${line}`.trimEnd()), `${indent} */`];
45
- };
46
- const renderField = (field, ir) => {
47
- const optional = field.optional ? '?' : '';
48
- const doc = docLines(field.description, ' ');
49
- let entry;
50
- if (field.literal !== undefined) {
51
- const type = field.list ? `${singleQuote(field.literal)}[]` : singleQuote(field.literal);
52
- entry = ` ${field.name}${optional}: ${type}`;
53
- }
54
- else if (isRelation(field)) {
55
- const base = SCALAR_TS[relationFkType(ir, field)];
56
- entry = ` ${relationFkName(field)}${optional}: ${field.list ? `${base}[]` : base}`;
57
- }
58
- else {
59
- const base = renderRef(field.type);
60
- const type = field.list ? `${base}[]` : base;
61
- const nullable = field.nullable ? ' | null' : '';
62
- entry = ` ${field.name}${optional}: ${type}${nullable}`;
63
- }
64
- return [...doc, entry].join('\n');
65
- };
66
- const renderEnumConst = (irEnum) => {
67
- const doc = docLines(irEnum.description, '');
68
- const constName = constantCase(irEnum.name);
69
- const entries = irEnum.values
70
- .map(value => ` ${irEnum.valueKeys?.[value] ?? constantCase(value)}: ${singleQuote(value)},`)
71
- .join('\n');
72
- return [
73
- ...doc,
74
- `export const ${constName} = Object.freeze({`,
75
- entries,
76
- `} as const)`,
77
- `export type ${irEnum.name}Key = keyof typeof ${constName}`,
78
- `export type ${irEnum.name} = (typeof ${constName})[${irEnum.name}Key]`,
79
- ].join('\n');
80
- };
81
- const renderModel = (model, ir) => {
82
- const doc = docLines(model.description, '');
83
- const fields = model.fields.map(field => renderField(field, ir)).join('\n');
84
- return [...doc, `export interface ${model.name} {`, fields, '}'].join('\n');
85
- };
86
- const usesJson = (models) => models.some(model => model.fields.some(field => field.type.kind === 'scalar' && field.type.name === 'json'));
6
+ import { JSON_TYPE, renderEnumConst, renderModel, usesJson } from './render.js';
87
7
  /**
88
8
  * Split variant of the TypeScript generator: one file per table at
89
9
  * `types/<table-name>.ts` (kebab-case), with enums and embedded value objects
@@ -0,0 +1,12 @@
1
+ import { Ir, IrEnum, IrField, IrModel, ScalarType } from '../../ir/ir.js';
2
+ /**
3
+ * Rendering shared by the bundled and split Zod layouts. Both layouts MUST go
4
+ * through these helpers so a contract setting (nullable, constraints, …) means
5
+ * the same thing regardless of layout.
6
+ */
7
+ export declare const SCALAR_ZOD: Record<ScalarType, string>;
8
+ export declare const schemaName: (name: string) => string;
9
+ export declare const renderFieldSchema: (field: IrField, ir: Ir) => string;
10
+ export declare const renderEnum: (irEnum: IrEnum) => string;
11
+ export declare const renderModel: (model: IrModel, ir: Ir) => string;
12
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/zod/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAa,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAKpF;;;;GAIG;AAEH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAUjD,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,MAAyB,CAAA;AAgBnE,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,EAAE,IAAI,EAAE,KAAG,MAa1D,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,KAAG,MAM3C,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,EAAE,IAAI,EAAE,KAAG,MAMpD,CAAA"}
@@ -0,0 +1,59 @@
1
+ import { zodConstraints } from '../support/constraints.js';
2
+ import { singleQuote } from '../support/naming.js';
3
+ import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
4
+ /**
5
+ * Rendering shared by the bundled and split Zod layouts. Both layouts MUST go
6
+ * through these helpers so a contract setting (nullable, constraints, …) means
7
+ * the same thing regardless of layout.
8
+ */
9
+ export const SCALAR_ZOD = {
10
+ string: 'z.string()',
11
+ int: 'z.number().int()',
12
+ int64: 'z.number().int()',
13
+ float: 'z.number()',
14
+ boolean: 'z.boolean()',
15
+ timestamp: 'z.string().datetime()',
16
+ date: 'z.string()',
17
+ json: 'z.unknown()',
18
+ id: 'z.string()',
19
+ };
20
+ export const schemaName = (name) => `${name}Schema`;
21
+ const renderRef = (ref) => {
22
+ switch (ref.kind) {
23
+ case 'scalar':
24
+ return SCALAR_ZOD[ref.name];
25
+ case 'enum':
26
+ return schemaName(ref.name);
27
+ case 'model':
28
+ // Wrapped in z.lazy so declaration order and (cross-file) cycles never matter.
29
+ return `z.lazy(() => ${schemaName(ref.name)})`;
30
+ default:
31
+ return 'z.unknown()';
32
+ }
33
+ };
34
+ export const renderFieldSchema = (field, ir) => {
35
+ const isRel = isRelation(field);
36
+ const name = isRel ? relationFkName(field) : field.name;
37
+ const base = field.literal !== undefined
38
+ ? `z.literal(${singleQuote(field.literal)})`
39
+ : isRel
40
+ ? SCALAR_ZOD[relationFkType(ir, field)]
41
+ : renderRef(field.type);
42
+ const cons = zodConstraints(field);
43
+ let schema = field.list ? `z.array(${base})${cons}` : `${base}${cons}`;
44
+ if (field.nullable)
45
+ schema = `${schema}.nullable()`;
46
+ if (field.optional)
47
+ schema = `${schema}.optional()`;
48
+ return ` ${name}: ${schema},`;
49
+ };
50
+ export const renderEnum = (irEnum) => {
51
+ const values = irEnum.values.map(singleQuote).join(', ');
52
+ return (`export const ${schemaName(irEnum.name)} = z.enum([${values}])\n` +
53
+ `export type ${irEnum.name} = z.infer<typeof ${schemaName(irEnum.name)}>`);
54
+ };
55
+ export const renderModel = (model, ir) => {
56
+ const fields = model.fields.map(field => renderFieldSchema(field, ir)).join('\n');
57
+ return (`export const ${schemaName(model.name)} = z.object({\n${fields}\n})\n` +
58
+ `export type ${model.name} = z.infer<typeof ${schemaName(model.name)}>`);
59
+ };
@@ -1,7 +1,8 @@
1
1
  import { Generator } from '../generator.js';
2
2
  /**
3
3
  * Generates Zod schemas and the types inferred from them. Model references use
4
- * `z.lazy` so the output is order- and cycle-independent.
4
+ * `z.lazy` so the output is order- and cycle-independent. Rendering is shared
5
+ * with the split layout (./render.js) so both emit identical members.
5
6
  */
6
7
  export declare const createZodGenerator: () => Generator;
7
8
  //# sourceMappingURL=zod-generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"zod-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/zod/zod-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAuD5E;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAAO,SA4BnC,CAAA"}
1
+ {"version":3,"file":"zod-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/zod/zod-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAO5E;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAO,SAoBnC,CAAA"}
@@ -1,55 +1,12 @@
1
1
  import { headerBlocks } from '../support/header.js';
2
2
  import { withSplitVariant } from '../support/split-variant.js';
3
3
  import { createZodSplitLayout } from './zod-split-generator.js';
4
- import { singleQuote } from '../support/naming.js';
5
- import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
6
- import { zodConstraints } from '../support/constraints.js';
7
4
  import { outputFile } from '../support/templates.js';
8
- const SCALAR_ZOD = {
9
- string: 'z.string()',
10
- int: 'z.number().int()',
11
- int64: 'z.number().int()',
12
- float: 'z.number()',
13
- boolean: 'z.boolean()',
14
- timestamp: 'z.string().datetime()',
15
- date: 'z.string()',
16
- json: 'z.unknown()',
17
- id: 'z.string()',
18
- };
19
- const schemaName = (name) => `${name}Schema`;
20
- const renderRef = (ref) => {
21
- switch (ref.kind) {
22
- case 'scalar':
23
- return SCALAR_ZOD[ref.name];
24
- case 'enum':
25
- // Enums are declared before models, so a direct reference is safe.
26
- return schemaName(ref.name);
27
- case 'model':
28
- // Model references are wrapped in z.lazy so declaration order and cycles
29
- // never matter.
30
- return `z.lazy(() => ${schemaName(ref.name)})`;
31
- default:
32
- return 'z.unknown()';
33
- }
34
- };
35
- const renderFieldSchema = (field, ir) => {
36
- const isRel = isRelation(field);
37
- const name = isRel ? relationFkName(field) : field.name;
38
- const base = field.literal !== undefined
39
- ? `z.literal(${singleQuote(field.literal)})`
40
- : isRel
41
- ? SCALAR_ZOD[relationFkType(ir, field)]
42
- : renderRef(field.type);
43
- const cons = zodConstraints(field);
44
- let schema = field.list ? `z.array(${base})${cons}` : `${base}${cons}`;
45
- if (field.optional) {
46
- schema = `${schema}.optional()`;
47
- }
48
- return ` ${name}: ${schema},`;
49
- };
5
+ import { renderEnum, renderModel } from './render.js';
50
6
  /**
51
7
  * Generates Zod schemas and the types inferred from them. Model references use
52
- * `z.lazy` so the output is order- and cycle-independent.
8
+ * `z.lazy` so the output is order- and cycle-independent. Rendering is shared
9
+ * with the split layout (./render.js) so both emit identical members.
53
10
  */
54
11
  export const createZodGenerator = () => withSplitVariant({
55
12
  name: 'zod',
@@ -57,14 +14,10 @@ export const createZodGenerator = () => withSplitVariant({
57
14
  generate(ir, context) {
58
15
  const blocks = [...headerBlocks(context), "import { z } from 'zod'"];
59
16
  for (const irEnum of ir.enums) {
60
- const values = irEnum.values.map(singleQuote).join(', ');
61
- blocks.push(`export const ${schemaName(irEnum.name)} = z.enum([${values}])\n` +
62
- `export type ${irEnum.name} = z.infer<typeof ${schemaName(irEnum.name)}>`);
17
+ blocks.push(renderEnum(irEnum));
63
18
  }
64
19
  for (const model of ir.models) {
65
- const fields = model.fields.map(field => renderFieldSchema(field, ir)).join('\n');
66
- blocks.push(`export const ${schemaName(model.name)} = z.object({\n${fields}\n})\n` +
67
- `export type ${model.name} = z.infer<typeof ${schemaName(model.name)}>`);
20
+ blocks.push(renderModel(model, ir));
68
21
  }
69
22
  return [{ path: outputFile(context, 'schemas.ts'), content: `${blocks.join('\n\n')}\n` }];
70
23
  },
@@ -1 +1 @@
1
- {"version":3,"file":"zod-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/zod/zod-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAmE5E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAO,SAmEtC,CAAA"}
1
+ {"version":3,"file":"zod-split-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/zod/zod-split-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAO5E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAO,SAmEtC,CAAA"}
@@ -1,61 +1,9 @@
1
1
  import { findEnum, findModel } from '../../ir/ir.js';
2
- import { zodConstraints } from '../support/constraints.js';
3
2
  import { headerBlocks } from '../support/header.js';
4
- import { singleQuote } from '../support/naming.js';
5
- import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
3
+ import { isRelation } from '../support/relations.js';
6
4
  import { assignModuleNames, collectDeps, splitGroups } from '../support/split.js';
7
5
  import { outputFile } from '../support/templates.js';
8
- const SCALAR_ZOD = {
9
- string: 'z.string()',
10
- int: 'z.number().int()',
11
- int64: 'z.number().int()',
12
- float: 'z.number()',
13
- boolean: 'z.boolean()',
14
- timestamp: 'z.string().datetime()',
15
- date: 'z.string()',
16
- json: 'z.unknown()',
17
- id: 'z.string()',
18
- };
19
- const schemaName = (name) => `${name}Schema`;
20
- const renderRef = (ref) => {
21
- switch (ref.kind) {
22
- case 'scalar':
23
- return SCALAR_ZOD[ref.name];
24
- case 'enum':
25
- return schemaName(ref.name);
26
- case 'model':
27
- // Wrapped in z.lazy so declaration order and (cross-file) cycles never matter.
28
- return `z.lazy(() => ${schemaName(ref.name)})`;
29
- default:
30
- return 'z.unknown()';
31
- }
32
- };
33
- const renderFieldSchema = (field, ir) => {
34
- const isRel = isRelation(field);
35
- const name = isRel ? relationFkName(field) : field.name;
36
- const base = field.literal !== undefined
37
- ? `z.literal(${singleQuote(field.literal)})`
38
- : isRel
39
- ? SCALAR_ZOD[relationFkType(ir, field)]
40
- : renderRef(field.type);
41
- const cons = zodConstraints(field);
42
- let schema = field.list ? `z.array(${base})${cons}` : `${base}${cons}`;
43
- if (field.nullable)
44
- schema = `${schema}.nullable()`;
45
- if (field.optional)
46
- schema = `${schema}.optional()`;
47
- return ` ${name}: ${schema},`;
48
- };
49
- const renderEnum = (irEnum) => {
50
- const values = irEnum.values.map(singleQuote).join(', ');
51
- return (`export const ${schemaName(irEnum.name)} = z.enum([${values}])\n` +
52
- `export type ${irEnum.name} = z.infer<typeof ${schemaName(irEnum.name)}>`);
53
- };
54
- const renderModel = (model, ir) => {
55
- const fields = model.fields.map(field => renderFieldSchema(field, ir)).join('\n');
56
- return (`export const ${schemaName(model.name)} = z.object({\n${fields}\n})\n` +
57
- `export type ${model.name} = z.infer<typeof ${schemaName(model.name)}>`);
58
- };
6
+ import { renderEnum, renderModel, schemaName } from './render.js';
59
7
  /**
60
8
  * Split variant of the Zod generator: one file per table at
61
9
  * `schemas/<table-name>.ts` (kebab-case) with enums and embedded value objects
@@ -1 +1 @@
1
- {"version":3,"file":"build-ir.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/ir/build-ir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAS,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAGL,WAAW,EAIZ,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAGL,EAAE,EAgBH,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAqLD;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,WAAW,EAAE,KAAG,aAqQlD,CAAA"}
1
+ {"version":3,"file":"build-ir.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/ir/build-ir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAGL,WAAW,EAIZ,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAGL,EAAE,EAgBH,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAyPD;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,WAAW,EAAE,KAAG,aAkQlD,CAAA"}