firebase-contract 0.1.0-rc.11 → 0.1.0-rc.13

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 (47) hide show
  1. package/README.md +34 -6
  2. package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -1
  3. package/dist/lib/_internal/compiler/compiler.js +42 -4
  4. package/dist/lib/_internal/generators/support/split.d.ts +1 -1
  5. package/dist/lib/_internal/generators/support/split.js +1 -1
  6. package/dist/lib/_internal/generators/typescript/render.d.ts +15 -0
  7. package/dist/lib/_internal/generators/typescript/render.d.ts.map +1 -0
  8. package/dist/lib/_internal/generators/typescript/render.js +92 -0
  9. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts +2 -1
  10. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -1
  11. package/dist/lib/_internal/generators/typescript/typescript-generator.js +5 -75
  12. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -1
  13. package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +2 -82
  14. package/dist/lib/_internal/generators/zod/render.d.ts +12 -0
  15. package/dist/lib/_internal/generators/zod/render.d.ts.map +1 -0
  16. package/dist/lib/_internal/generators/zod/render.js +59 -0
  17. package/dist/lib/_internal/generators/zod/zod-generator.d.ts +2 -1
  18. package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -1
  19. package/dist/lib/_internal/generators/zod/zod-generator.js +5 -52
  20. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -1
  21. package/dist/lib/_internal/generators/zod/zod-split-generator.js +2 -54
  22. package/dist/lib/_internal/ir/build-ir.d.ts.map +1 -1
  23. package/dist/lib/_internal/ir/build-ir.js +0 -2
  24. package/dist/lib/_internal/ir/ir.d.ts +0 -1
  25. package/dist/lib/_internal/ir/ir.d.ts.map +1 -1
  26. package/dist/lib/_internal/parser/parse.d.ts +5 -3
  27. package/dist/lib/_internal/parser/parse.d.ts.map +1 -1
  28. package/dist/lib/_internal/parser/parse.js +69 -12
  29. package/dist/lib/_internal/parser/raw-document.d.ts +0 -1
  30. package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -1
  31. package/dist/lib/_internal/resolver/import-resolver.js +1 -1
  32. package/dist/lib/_internal/validation/rules.d.ts +32 -0
  33. package/dist/lib/_internal/validation/rules.d.ts.map +1 -1
  34. package/dist/lib/_internal/validation/rules.js +119 -0
  35. package/dist/lib/cli/commands/generate.d.ts.map +1 -1
  36. package/dist/lib/cli/commands/generate.js +6 -4
  37. package/dist/lib/cli/commands/index.d.ts.map +1 -1
  38. package/dist/lib/cli/commands/index.js +2 -0
  39. package/dist/lib/cli/commands/init.d.ts.map +1 -1
  40. package/dist/lib/cli/commands/init.js +2 -18
  41. package/dist/lib/cli/commands/where.d.ts +3 -0
  42. package/dist/lib/cli/commands/where.d.ts.map +1 -0
  43. package/dist/lib/cli/commands/where.js +57 -0
  44. package/dist/lib/cli/config.d.ts +3 -2
  45. package/dist/lib/cli/config.d.ts.map +1 -1
  46. package/dist/lib/cli/config.js +3 -0
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -34,6 +34,7 @@ fbc init # scaffold contract.yml + firebase-contract.json
34
34
  fbc validate # parse, resolve imports, semantically validate
35
35
  fbc generate # run every generator declared across the contract graph
36
36
  fbc inspect # print the normalized IR (debugging)
37
+ fbc where <Name> # locate the yml defining a type (logical / gqlName / fsName / table)
37
38
  ```
38
39
 
39
40
  ### One-command generation
@@ -103,7 +104,13 @@ generators:
103
104
  and errors for generators without one.
104
105
  - **`options`** — free-form string map passed through to the generator.
105
106
  Currently: `typesImport` (api-types, task-payloads, data-connect-operations)
106
- overrides the `./types` import path.
107
+ overrides the types import path. For api-types and task-payloads this is
108
+ normally unnecessary: when the option is absent and a `typescript` generator
109
+ is declared (nearest-first: same yml → root), the import specifier is derived
110
+ automatically as the relative path from the output file to the declared
111
+ barrel — moving or renaming the barrel (`out` / `file`) re-derives it. An
112
+ explicit `typesImport` always wins; without a typescript declaration the
113
+ generator's built-in default (`./types`) applies.
107
114
  - **`header`** — per-generator banner override: `default` for the built-in
108
115
  AUTO-GENERATED banner, any text for a custom comment, `""` to suppress the
109
116
  banner for this generator. Wins over the contract-level `header:` and the
@@ -116,8 +123,9 @@ generators:
116
123
 
117
124
  `fbc generate contract.yml` materializes **all** declared outputs across the
118
125
  whole import graph in one run. Passing `-o`/`-g` switches to single-target mode
119
- (`fbc generate <entry> -o <dir> -g typescript,zod`), and
120
- `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.
121
129
 
122
130
 
123
131
  ### Generated-file headers
@@ -174,8 +182,25 @@ in other contracts (relative paths or npm packages). Imports are resolved
174
182
  transitively with cycle/duplicate detection and diamond dedup, so you can split
175
183
  a large contract across files that mirror your repo layout.
176
184
 
185
+ **Type names are globally unique.** The namespace is flat across the whole
186
+ import graph: defining the same name twice — same kind (`DUPLICATE_DEFINITION`)
187
+ or across kinds/derived identifiers, e.g. an enum and a model both named
188
+ `Status`, or two enums whose `CONSTANT_CASE` consts collide (`NAME_COLLISION`)
189
+ — aborts generation. Uniqueness is enforced per output namespace with its
190
+ *effective* names, so `fsName`/`gqlName` renames legitimately reuse a name in a
191
+ different output: GraphQL names are checked per `data-connect-graphql` scope
192
+ (services may map distinct logical names onto the same gqlName), and the
193
+ firestore module is checked under `fsName ?? name` alongside doc names. A bare
194
+ type reference therefore always resolves to exactly one definition — use
195
+ `fbc where <Name>` to find it.
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
+
177
203
  ```yaml
178
- version: 1
179
204
  imports:
180
205
  - ./apps/shop/data-connect/schema.yml
181
206
  - some-shared-package/contract.yml
@@ -468,8 +493,11 @@ firestore:
468
493
  ```
469
494
 
470
495
  Emits Zod schemas + inferred types, mirroring a hand-written Firestore schema
471
- library. (The simpler `firestore-types` generator just re-types *every* model
472
- with the Firestore `Timestamp` and does not denormalize.)
496
+ library. (`firestore-types` is the zero-config entry point for projects that
497
+ have not written projections yet: it re-types *every* model with the Firestore
498
+ `Timestamp`, no `firestore:` section needed. Once you declare projections,
499
+ prefer the `firestore` generator — it is the one that understands
500
+ pick/omit/fsName/denormalized fields.)
473
501
 
474
502
  With `firestore` + `split: true`, fields whose Zod chain is **identical** to the Data
475
503
  Connect schema are reused via `.pick()` — only representation changes are
@@ -1 +1 @@
1
- {"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/compiler/compiler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAoB,MAAM,mBAAmB,CAAA;AAEhE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAU,WAAW,EAAqC,MAAM,2BAA2B,CAAA;AAElG,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAG3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAI5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAEL,cAAc,EAGf,MAAM,kBAAkB,CAAA;AAEzB,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,SAAS,EAAE,WAAW,EAAE,CAAA;IACxB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,MAAM,EAAE,UAAS,cAAmB,KAAG,aAUzE,CAAA;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,oDAAoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,oFAAoF;IACpF,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,mFAAmF;IACnF,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;AAED,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,aAAa,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,oBAAoB,EAAE,CAAA;IAC/B,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,EAAE,EAAE,OAAO,CAAA;CACZ;AA6MD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,UAAS;IACP,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,GAAG,CAAC,EAAE,IAAI,CAAA;CACN,KACL,iBA0FF,CAAA;AAED,MAAM,WAAW,SAAS;IACxB,sEAAsE;IACtE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,aAAa,EAAE,KAAG,SAUlD,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,oFAAoF;IACpF,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,0DAA0D;IAC1D,EAAE,EAAE,OAAO,CAAA;CACZ;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,EAAE,SAAS,eAAe,KAAG,cAgEtE,CAAA"}
1
+ {"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/compiler/compiler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAoB,MAAM,mBAAmB,CAAA;AAEhE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAU,WAAW,EAAqC,MAAM,2BAA2B,CAAA;AAElG,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAI3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAI5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAEL,cAAc,EAGf,MAAM,kBAAkB,CAAA;AAEzB,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,SAAS,EAAE,WAAW,EAAE,CAAA;IACxB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,MAAM,EAAE,UAAS,cAAmB,KAAG,aAkBzE,CAAA;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,oDAAoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,oFAAoF;IACpF,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,mFAAmF;IACnF,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;AAED,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,aAAa,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,oBAAoB,EAAE,CAAA;IAC/B,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,EAAE,EAAE,OAAO,CAAA;CACZ;AAyOD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,UAAS;IACP,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,GAAG,CAAC,EAAE,IAAI,CAAA;CACN,KACL,iBA0FF,CAAA;AAED,MAAM,WAAW,SAAS;IACxB,sEAAsE;IACtE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,aAAa,EAAE,KAAG,SAUlD,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,oFAAoF;IACpF,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,0DAA0D;IAC1D,EAAE,EAAE,OAAO,CAAA;CACZ;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,EAAE,SAAS,eAAe,KAAG,cAgEtE,CAAA"}
@@ -4,6 +4,7 @@ import { error, hasErrors } from '../diagnostics.js';
4
4
  import { buildIr } from '../ir/build-ir.js';
5
5
  import { resolveImports } from '../resolver/import-resolver.js';
6
6
  import { createFsLoader } from '../resolver/module-loader.js';
7
+ import { graphqlNameCollisions } from '../validation/rules.js';
7
8
  import { validateIr } from '../validation/validate.js';
8
9
  import { createDefaultRegistry } from '../generators/index.js';
9
10
  import { toHeaderComment } from '../generators/support/header.js';
@@ -20,10 +21,18 @@ export const compile = (entryPath, options = {}) => {
20
21
  const { documents, diagnostics: importDiagnostics } = resolveImports(entryPath, loader);
21
22
  const { ir, diagnostics: buildDiagnostics } = buildIr(documents);
22
23
  const validationDiagnostics = validateIr(ir);
24
+ // A GraphQL schema is generated per data-connect-graphql declaration from the
25
+ // declaring yml's subtree, so its name uniqueness only holds at that scope —
26
+ // distinct services may reuse a gqlName. generateAll re-compiles each target
27
+ // with the declaring yml as entry, which is exactly when this check applies.
28
+ const entryDoc = documents[documents.length - 1];
29
+ const graphqlDiagnostics = entryDoc?.generatorDecls?.some(decl => decl.generator === 'data-connect-graphql')
30
+ ? graphqlNameCollisions(ir)
31
+ : [];
23
32
  return {
24
33
  ir,
25
34
  documents,
26
- diagnostics: [...importDiagnostics, ...buildDiagnostics, ...validationDiagnostics],
35
+ diagnostics: [...importDiagnostics, ...buildDiagnostics, ...validationDiagnostics, ...graphqlDiagnostics],
27
36
  };
28
37
  };
29
38
  /** `apis:/tasks:/events:` section a raw api belongs to (drives the section defaults lookup). */
@@ -74,6 +83,30 @@ const resolveOutDir = (out, declaringDoc, rootDoc, diagnostics) => {
74
83
  /** Find a generator declaration by name — nearest first: the declaring yml, then the root yml. */
75
84
  const findGeneratorDecl = (name, doc, rootDoc) => doc.generatorDecls?.find(decl => decl.generator === name) ??
76
85
  rootDoc.generatorDecls?.find(decl => decl.generator === name);
86
+ /** Generators whose output imports the shared types barrel via `options.typesImport`. */
87
+ const TYPES_IMPORT_GENERATORS = new Set(['api-types', 'task-payloads']);
88
+ /**
89
+ * Derive the default `typesImport` specifier from the typescript declaration
90
+ * itself: the relative path from the job's output directory to the declared
91
+ * barrel (`out` + `file`, default `types.ts`). The declaration resolves
92
+ * nearest-first (same yml → root), mirroring how entries resolve declarations,
93
+ * so both outputs stay in sync when the barrel is moved or renamed. An explicit
94
+ * `options.typesImport` always wins; without a typescript declaration in scope
95
+ * the generator's built-in default applies.
96
+ */
97
+ const deriveTypesImport = (outDir, doc, rootDoc) => {
98
+ const decl = findGeneratorDecl('typescript', doc, rootDoc);
99
+ if (!decl)
100
+ return undefined;
101
+ const declaringDoc = doc.generatorDecls?.includes(decl) ? doc : rootDoc;
102
+ // Alias problems are reported when the typescript job itself is built.
103
+ const barrelDir = resolveOutDir(decl.out, declaringDoc, rootDoc, []);
104
+ if (barrelDir === undefined)
105
+ return undefined;
106
+ const barrel = join(barrelDir, (decl.file ?? 'types.ts').replace(/\.ts$/, ''));
107
+ const specifier = relative(outDir, barrel);
108
+ return specifier.startsWith('.') ? specifier : `./${specifier}`;
109
+ };
77
110
  /**
78
111
  * Build generator jobs from the declaration/application DSL:
79
112
  * - api-scoped generators run for entries that opt in (section `defaults` →
@@ -98,14 +131,12 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
98
131
  const output = {};
99
132
  const file = use.file ?? decl?.file;
100
133
  const split = use.split ?? decl?.split ?? generator?.defaultOutput?.split;
101
- const options = use.options ?? decl?.options;
134
+ let options = use.options ?? decl?.options;
102
135
  const header = use.header ?? decl?.header;
103
136
  if (file !== undefined)
104
137
  output.file = file;
105
138
  if (split !== undefined)
106
139
  output.split = split;
107
- if (options !== undefined)
108
- output.options = options;
109
140
  // Split mode needs a per-api file name; bundled mode cannot expand one.
110
141
  const effectiveFile = file ?? generator?.defaultOutput?.file;
111
142
  if (effectiveFile !== undefined && split === false && API_PLACEHOLDER_RE.test(effectiveFile)) {
@@ -123,6 +154,13 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
123
154
  const outDir = resolveOutDir(expanded, doc, rootDoc, diagnostics);
124
155
  if (outDir === undefined)
125
156
  continue;
157
+ if (TYPES_IMPORT_GENERATORS.has(use.generator) && options?.['typesImport'] === undefined) {
158
+ const derived = deriveTypesImport(outDir, doc, rootDoc);
159
+ if (derived !== undefined)
160
+ options = { ...(options ?? {}), typesImport: derived };
161
+ }
162
+ if (options !== undefined)
163
+ output.options = options;
126
164
  const key = [use.generator, outDir, file ?? '', String(split ?? ''), JSON.stringify(options ?? null), header ?? '\u0001'].join('\u0000');
127
165
  const existing = grouped.get(key);
128
166
  if (existing) {
@@ -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"}