firebase-contract 0.1.0-rc.9 → 0.1.0

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 (78) hide show
  1. package/README.md +140 -63
  2. package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -1
  3. package/dist/lib/_internal/compiler/compiler.js +50 -21
  4. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts.map +1 -1
  5. package/dist/lib/_internal/generators/api/api-dto-generator.js +1 -8
  6. package/dist/lib/_internal/generators/api/api-types-generator.d.ts.map +1 -1
  7. package/dist/lib/_internal/generators/api/api-types-generator.js +11 -5
  8. package/dist/lib/_internal/generators/api/task-payload-generator.js +3 -3
  9. package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts +0 -7
  10. package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts.map +1 -1
  11. package/dist/lib/_internal/generators/data-connect/graphql-generator.js +5 -4
  12. package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts +0 -7
  13. package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts.map +1 -1
  14. package/dist/lib/_internal/generators/data-connect/operations-generator.js +8 -7
  15. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts.map +1 -1
  16. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.js +7 -22
  17. package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts +1 -1
  18. package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts.map +1 -1
  19. package/dist/lib/_internal/generators/firestore/firestore-split-generator.js +69 -47
  20. package/dist/lib/_internal/generators/generator.d.ts +2 -0
  21. package/dist/lib/_internal/generators/generator.d.ts.map +1 -1
  22. package/dist/lib/_internal/generators/index.d.ts +2 -5
  23. package/dist/lib/_internal/generators/index.d.ts.map +1 -1
  24. package/dist/lib/_internal/generators/index.js +4 -15
  25. package/dist/lib/_internal/generators/support/jsdoc.d.ts +3 -0
  26. package/dist/lib/_internal/generators/support/jsdoc.d.ts.map +1 -0
  27. package/dist/lib/_internal/generators/support/jsdoc.js +9 -0
  28. package/dist/lib/_internal/generators/support/split-variant.d.ts +9 -0
  29. package/dist/lib/_internal/generators/support/split-variant.d.ts.map +1 -0
  30. package/dist/lib/_internal/generators/support/split-variant.js +13 -0
  31. package/dist/lib/_internal/generators/support/split.d.ts +1 -1
  32. package/dist/lib/_internal/generators/support/split.d.ts.map +1 -1
  33. package/dist/lib/_internal/generators/support/split.js +3 -2
  34. package/dist/lib/_internal/generators/typescript/render.d.ts +15 -0
  35. package/dist/lib/_internal/generators/typescript/render.d.ts.map +1 -0
  36. package/dist/lib/_internal/generators/typescript/render.js +92 -0
  37. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts +2 -1
  38. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -1
  39. package/dist/lib/_internal/generators/typescript/typescript-generator.js +12 -78
  40. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts +1 -1
  41. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -1
  42. package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +4 -84
  43. package/dist/lib/_internal/generators/zod/render.d.ts +12 -0
  44. package/dist/lib/_internal/generators/zod/render.d.ts.map +1 -0
  45. package/dist/lib/_internal/generators/zod/render.js +59 -0
  46. package/dist/lib/_internal/generators/zod/zod-generator.d.ts +2 -1
  47. package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -1
  48. package/dist/lib/_internal/generators/zod/zod-generator.js +11 -55
  49. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts +1 -1
  50. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -1
  51. package/dist/lib/_internal/generators/zod/zod-split-generator.js +4 -56
  52. package/dist/lib/_internal/ir/build-ir.d.ts.map +1 -1
  53. package/dist/lib/_internal/ir/build-ir.js +93 -8
  54. package/dist/lib/_internal/ir/ir.d.ts +18 -5
  55. package/dist/lib/_internal/ir/ir.d.ts.map +1 -1
  56. package/dist/lib/_internal/ir/ir.js +1 -0
  57. package/dist/lib/_internal/parser/parse.d.ts +5 -3
  58. package/dist/lib/_internal/parser/parse.d.ts.map +1 -1
  59. package/dist/lib/_internal/parser/parse.js +103 -14
  60. package/dist/lib/_internal/parser/raw-document.d.ts +18 -2
  61. package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -1
  62. package/dist/lib/_internal/resolver/import-resolver.js +1 -1
  63. package/dist/lib/_internal/validation/rules.d.ts +39 -0
  64. package/dist/lib/_internal/validation/rules.d.ts.map +1 -1
  65. package/dist/lib/_internal/validation/rules.js +133 -1
  66. package/dist/lib/cli/commands/generate.d.ts.map +1 -1
  67. package/dist/lib/cli/commands/generate.js +6 -4
  68. package/dist/lib/cli/commands/index.d.ts.map +1 -1
  69. package/dist/lib/cli/commands/index.js +2 -0
  70. package/dist/lib/cli/commands/init.d.ts.map +1 -1
  71. package/dist/lib/cli/commands/init.js +2 -18
  72. package/dist/lib/cli/commands/where.d.ts +3 -0
  73. package/dist/lib/cli/commands/where.d.ts.map +1 -0
  74. package/dist/lib/cli/commands/where.js +57 -0
  75. package/dist/lib/cli/config.d.ts +3 -2
  76. package/dist/lib/cli/config.d.ts.map +1 -1
  77. package/dist/lib/cli/config.js +3 -0
  78. package/package.json +1 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@ From one contract, generate:
10
10
  - **Zod** validation schemas
11
11
  - **Data Connect** GraphQL schema (`@table`, keys, directives), query/mutation
12
12
  operations (routed per connector), and `Any`↔logical adapters
13
- - **Firestore** projection schemas (denormalized read model with `_meta_`)
13
+ - **Firestore** projection schemas (denormalized read model; shared field groups via fragments)
14
14
  - **API** request/response types, request-validation Zod, and class-validator DTOs
15
15
  - **Cloud Task / Pub/Sub** payload envelopes + delivery constants
16
16
  - **SQL migrations** for constraints Data Connect can't express (composite FKs,
@@ -34,13 +34,14 @@ 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
40
41
 
41
42
  Each contract file declares the generators it uses in a top-level
42
43
  `generators:` block — a name plus an output template. Declaring a
43
- **document-scoped** generator (typescript-split, zod-split, …) runs it once for
44
+ **document-scoped** generator (typescript, zod, …) runs it once for
44
45
  that yml; **api-scoped** generators (api-types, api-validation, api-dto,
45
46
  task-payloads) run for the entries that opt in (see below):
46
47
 
@@ -50,7 +51,7 @@ project:
50
51
  aliases:
51
52
  "#contracts/*": libs/contracts/src/* # out templates may target aliases
52
53
  generators:
53
- - { generator: typescript-split, out: "#contracts" }
54
+ - { generator: typescript, out: "#contracts", split: true }
54
55
  - { generator: api-types, out: "#contracts/api-types/{api-name}" }
55
56
  imports:
56
57
  - ./apps/shop/data-connect/schema.yml
@@ -59,7 +60,7 @@ imports:
59
60
  ```yaml
60
61
  # apps/shop/data-connect/schema.yml
61
62
  generators:
62
- - { generator: data-connect-graphql-split, out: src }
63
+ - { generator: data-connect-graphql, out: src, split: true }
63
64
  - { generator: sql-migrations, out: . }
64
65
  ```
65
66
 
@@ -90,19 +91,26 @@ generators:
90
91
  - generator: typescript
91
92
  out: "#contracts"
92
93
  file: models.ts # rename a single-file/barrel output
93
- split: true # document scope: switches to typescript-split
94
+ split: true # document scope: per-table split layout
94
95
  ```
95
96
 
96
97
  - **`file`** — output file name. Api scope: a template (`{api-name}`/`{path}`
97
98
  allowed when `split: true`). Document scope: renames the single output file,
98
- or the barrel for `-split` variants.
99
+ or the barrel of a split layout.
99
100
  - **`split`** — api scope: `true` emits one file per api (the `file` template
100
101
  must contain a placeholder), `false` bundles everything into one `file`.
101
- Document scope: `true` swaps to the generator's `-split` variant
102
- (`typescript` → `typescript-split`, …) and errors when none exists.
102
+ Document scope: `true` selects the generator's per-item split layout
103
+ (typescript, zod, firestore, data-connect-graphql, data-connect-operations)
104
+ and errors for generators without one.
103
105
  - **`options`** — free-form string map passed through to the generator.
104
106
  Currently: `typesImport` (api-types, task-payloads, data-connect-operations)
105
- 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.
106
114
  - **`header`** — per-generator banner override: `default` for the built-in
107
115
  AUTO-GENERATED banner, any text for a custom comment, `""` to suppress the
108
116
  banner for this generator. Wins over the contract-level `header:` and the
@@ -115,8 +123,9 @@ generators:
115
123
 
116
124
  `fbc generate contract.yml` materializes **all** declared outputs across the
117
125
  whole import graph in one run. Passing `-o`/`-g` switches to single-target mode
118
- (`fbc generate <entry> -o <dir> -g typescript,zod`), and
119
- `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.
120
129
 
121
130
 
122
131
  ### Generated-file headers
@@ -173,8 +182,33 @@ in other contracts (relative paths or npm packages). Imports are resolved
173
182
  transitively with cycle/duplicate detection and diamond dedup, so you can split
174
183
  a large contract across files that mirror your repo layout.
175
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
+
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
+
176
211
  ```yaml
177
- version: 1
178
212
  imports:
179
213
  - ./apps/shop/data-connect/schema.yml
180
214
  - some-shared-package/contract.yml
@@ -211,8 +245,10 @@ export type ProductStatusKey = keyof typeof PRODUCT_STATUS
211
245
  export type ProductStatus = (typeof PRODUCT_STATUS)[ProductStatusKey]
212
246
  ```
213
247
 
214
- Pass `enumStyle: 'union'` to the TypeScript generator for a plain
215
- `'OPEN' | 'DONE' | 'DISABLED'` union instead.
248
+ For a plain `'OPEN' | 'DONE' | 'DISABLED'` union instead, construct the
249
+ TypeScript generator with `enumStyle: 'union'` via the programmatic API
250
+ (`createTypeScriptGenerator({ enumStyle: 'union' })`). This is a build-time
251
+ generator option, not a YAML contract field.
216
252
 
217
253
  ## Models
218
254
 
@@ -247,24 +283,24 @@ models:
247
283
 
248
284
  | option | meaning |
249
285
  | ----------------------------------- | -------------------------------------------------------- |
250
- | `type` | document | scalar name, or an enum/model name |
251
- | `optional` | document | value may be absent (`field?`) |
252
- | `list` | document | value is an array of `type` |
253
- | `id` | document | marks the (single) primary identifier |
254
- | `unique` | document | field-level unique → `@unique` |
255
- | `relation` | document | model-typed field is a foreign-key relation (see below) |
256
- | `col` | document | Data Connect column dataType (Int64 PKs default to `bigserial`) |
257
- | `default` | document | `@default(expr: …)` on the DC column |
258
- | `literal` | document | pin to one literal value (union discriminant tags) |
259
- | `nullable` | document | value may be `null` (`.nullable()` / `\| null`) |
260
- | `description` | document | doc comment carried into generated output |
261
- | `jsdoc` | document | render the description as a JSDoc block (Firestore fields) |
286
+ | `type` | scalar name, or an enum/model name |
287
+ | `optional` | value may be absent (`field?`) |
288
+ | `list` | value is an array of `type` |
289
+ | `id` | marks the (single) primary identifier |
290
+ | `unique` | field-level unique → `@unique` |
291
+ | `relation` | model-typed field is a foreign-key relation (see below) |
292
+ | `col` | Data Connect column dataType (Int64 PKs default to `bigserial`) |
293
+ | `default` | `@default(expr: …)` on the DC column |
294
+ | `literal` | pin to one literal value (union discriminant tags) |
295
+ | `nullable` | value may be `null` (`.nullable()` / `\| null`) |
296
+ | `description` | doc comment carried into generated output |
297
+ | `jsdoc` | render the description as a JSDoc block (Firestore fields) |
262
298
  | **constraints** → | |
263
- | `min` / `max` | document | numeric bounds |
264
- | `minLength` / `maxLength` | document | string/array length bounds |
265
- | `nonempty` | document | non-empty string/array |
266
- | `pattern` | document | regex the string must match |
267
- | `email` / `url` | document | string format |
299
+ | `min` / `max` | numeric bounds |
300
+ | `minLength` / `maxLength` | string/array length bounds |
301
+ | `nonempty` | non-empty string/array |
302
+ | `pattern` | regex the string must match |
303
+ | `email` / `url` | string format |
268
304
 
269
305
  Constraints flow into the Zod schemas, the API request validation, and the
270
306
  class-validator DTOs.
@@ -273,14 +309,14 @@ class-validator DTOs.
273
309
 
274
310
  | option | meaning |
275
311
  | ------------ | ------------------------------------------------------------------ |
276
- | `key` | document | composite primary key field names (else the `id: true` field) |
277
- | `table` | document | table name override (default: snake_case pluralization) |
278
- | `gqlName` | document | GraphQL type name override |
279
- | `fsName` | document | Firestore-side rename (avoid collisions with table models) |
280
- | `directives` | document | `multi` = each type-level directive on its own line |
281
- | `footer` | document | trailing comment block after the closing `}` in the schema file |
282
- | `indexes` | document | composite `@index`/`@unique` (`{ fields, name?, unique?, expand? }` — `expand` renders args one per line) |
283
- | `sql` | document | raw SQL constraints (see [SQL migrations](#sql-migrations)) |
312
+ | `key` | composite primary key field names (else the `id: true` field) |
313
+ | `table` | table name override (default: snake_case pluralization) |
314
+ | `gqlName` | GraphQL type name override |
315
+ | `fsName` | Firestore-side rename (avoid collisions with table models) |
316
+ | `directives` | `multi` = each type-level directive on its own line |
317
+ | `footer` | trailing comment block after the closing `}` in the schema file |
318
+ | `indexes` | composite `@index`/`@unique` (`{ fields, name?, unique?, expand? }` — `expand` renders args one per line) |
319
+ | `sql` | raw SQL constraints (see [SQL migrations](#sql-migrations)) |
284
320
 
285
321
  ### Embedded vs relation
286
322
 
@@ -358,14 +394,14 @@ Everything below exists to reproduce real hand-written `.gql` files byte-for-byt
358
394
 
359
395
  | option | meaning |
360
396
  | --- | --- |
361
- | `gqlName` | document | rendered operation name (the yml key stays unique across services) |
362
- | `footer` | document | trailing `# …` comment block after the operation |
363
- | `raw` | document | emit the operation body verbatim (multi-model queries, `_or` keyset cursors — field checks are skipped) |
364
- | `single: id \| document | key` | single-row lookup (`product(id: $id)` / `product(key: { … })`) |
365
- | `keyArg` / `keyVars` | document | update/delete key argument form and variable renames |
366
- | `whereAnd: true` | document | render conditions as an `_and: [ … ]` array |
367
- | `entityDir` | document | override the `operations/<entity>/` output directory |
368
- | `style` | document | formatting hints: `signature`, `args`, `data`, `orderBy`, `auth`, `key`, `and`, `where` (`inline`/`multi`/`compact`/`bare`) |
397
+ | `gqlName` | rendered operation name (the yml key stays unique across services) |
398
+ | `footer` | trailing `# …` comment block after the operation |
399
+ | `raw` | emit the operation body verbatim (multi-model queries, `_or` keyset cursors — field checks are skipped) |
400
+ | `single: id \| key` | single-row lookup (`product(id: $id)` / `product(key: { … })`) |
401
+ | `keyArg` / `keyVars` | update/delete key argument form and variable renames |
402
+ | `whereAnd: true` | render conditions as an `_and: [ … ]` array |
403
+ | `entityDir` | override the `operations/<entity>/` output directory |
404
+ | `style` | formatting hints: `signature`, `args`, `data`, `orderBy`, `auth`, `key`, `and`, `where` (`inline`/`multi`/`compact`/`bare`) |
369
405
 
370
406
  Inputs support `var` (variable rename), `required` (override optionality),
371
407
  `literal` (fixed value, no variable), `flat` (write the FK column directly),
@@ -447,9 +483,9 @@ reality automatically:
447
483
 
448
484
  - **relations → resolved string ids** (`ownerUser` → `ownerUserId: z.string()`)
449
485
  - **timestamps → `z.date()`**, optional fields → `.nullable()`
450
- - a **`_meta_`** consistency envelope is attached (opt out with `meta: false`)
451
486
  - `pick` / `omit` select which base fields survive
452
487
  - inline `fields` add denormalized data (flattened DAG edges, stringified JSON)
488
+ - `extends: [<fragment>…]` splices shared fields in (see **Fragments** below)
453
489
 
454
490
  ```yaml
455
491
  firestore:
@@ -467,10 +503,13 @@ firestore:
467
503
  ```
468
504
 
469
505
  Emits Zod schemas + inferred types, mirroring a hand-written Firestore schema
470
- library. (The simpler `firestore-types` generator just re-types *every* model
471
- with the Firestore `Timestamp` and does not denormalize.)
506
+ library. (`firestore-types` is the zero-config entry point for projects that
507
+ have not written projections yet: it re-types *every* model with the Firestore
508
+ `Timestamp`, no `firestore:` section needed. Once you declare projections,
509
+ prefer the `firestore` generator — it is the one that understands
510
+ pick/omit/fsName/denormalized fields.)
472
511
 
473
- With `firestore-split`, fields whose Zod chain is **identical** to the Data
512
+ With `firestore` + `split: true`, fields whose Zod chain is **identical** to the Data
474
513
  Connect schema are reused via `.pick()` — only representation changes are
475
514
  restated:
476
515
 
@@ -480,7 +519,6 @@ export const ShopSchema = DcShopSchema.pick({
480
519
  }).extend({
481
520
  createdAt: z.date(),
482
521
  updatedAt: z.date().nullable(),
483
- _meta_: _Meta_Schema,
484
522
  })
485
523
  ```
486
524
 
@@ -497,6 +535,50 @@ Additional projection options:
497
535
  - FS-only enums render as frozen consts (`REVIEW_STATE` + `ReviewStateSchema`),
498
536
  co-located with the first projection that references them
499
537
 
538
+ ### Fragments (`fragments:` + `extends:`)
539
+
540
+ A **fragment** is a named group of fields declared once and spliced into
541
+ consumers via `extends: [<name>…]` — the way to share a common field across
542
+ many projections (a consistency `_meta_` envelope, audit columns, …) without a
543
+ tool-imposed convention. The fragment field's name, type, and which docs carry
544
+ it are all yours.
545
+
546
+ The value objects a fragment references can be **pinned** to a fixed split
547
+ output via `out` (+ optional `file`) on the model declaration. A pinned value
548
+ object is *placement-scoped*: emitted only at that location (dependency-ordered,
549
+ with an `index.ts` barrel), imported by consumers via that directory, and
550
+ excluded from the default `typescript`/`zod` barrels — so it lives purely in the
551
+ firestore output. This is what lets a shared envelope sit under `firestore/_/`.
552
+
553
+ ```yaml
554
+ models:
555
+ MetaOp:
556
+ out: firestore/_ # → firestore/_/_meta_.ts (+ firestore/_/index.ts)
557
+ file: _meta_.ts
558
+ fields:
559
+ id: { type: string }
560
+ Meta:
561
+ out: firestore/_
562
+ file: _meta_.ts
563
+ fields:
564
+ scope: { type: float, nullable: true }
565
+ op: { type: MetaOp, nullable: true }
566
+
567
+ fragments:
568
+ meta:
569
+ fields:
570
+ _meta_: { type: Meta }
571
+
572
+ firestore:
573
+ Shop:
574
+ from: Shop
575
+ extends: [meta] # appends `_meta_: MetaSchema` (imported from './_')
576
+ ```
577
+
578
+ `extends` referencing a fragment that is not declared is an `UNKNOWN_FRAGMENT`
579
+ error, not a silent no-op — a typo like `extends: [metaa]` fails generation
580
+ rather than quietly dropping the shared fields.
581
+
500
582
  ## Discriminated unions
501
583
 
502
584
  ```yaml
@@ -553,7 +635,7 @@ project:
553
635
  ```
554
636
 
555
637
  - `services` (`name`/`database`) feed the `FIRESTORE_DATABASES` constants in the
556
- `firestore-split` barrel.
638
+ split `firestore` barrel.
557
639
  - `idCodec` pins the Sqids settings as a contract constant; when present, the
558
640
  `id-codecs` generator also emits a self-contained `id-core.ts` (the
559
641
  encode/decode primitives), so nothing about id encoding lives outside the
@@ -574,18 +656,13 @@ project:
574
656
 
575
657
  | name | scope | output | notes |
576
658
  | ------------------------ | -------- | ----------------------------- | ------------------------------------------------------ |
577
- | `typescript` | document | `types.ts` | interfaces + const/union enums |
578
- | `typescript-split` | document | `types/<table>.ts` + `types.ts` barrel | one file per table; enums/embedded objects co-located |
579
- | `zod` | document | `schemas.ts` | `z.object` schemas (constraints, `z.lazy` model refs) |
580
- | `zod-split` | document | `schemas/<table>.ts` + `schemas.ts` barrel | split variant of `zod` |
581
- | `data-connect-graphql` | document | `schema.gql` | `type … @table(name, key)`; `@col`/`@unique`/`@index`/`@default` |
582
- | `data-connect-graphql-split` | document | `schema/<table>.gql` | same schema, one file per table (enums co-located) |
583
- | `data-connect-operations`| document | `<connector>/operations.gql` + `.ts` | queries/mutations (where ops, orderBy, limit, aggregate, exprs, inc), per connector |
584
- | `data-connect-operations-split` | document | `<connector>/operations/<entity>/{queries,mutations}.gql` | real Data Connect repo layout; shared types stay in `typescript`/`zod` outputs |
659
+ | `typescript` | document | `types.ts`; `split: true` → `types/<table>.ts` + barrel | interfaces + const/union enums; split co-locates enums/embedded objects per table |
660
+ | `zod` | document | `schemas.ts`; `split: true` → `schemas/<table>.ts` + barrel | `z.object` schemas (constraints, `z.lazy` model refs) |
661
+ | `data-connect-graphql` | document | `schema.gql`; `split: true` → `schema/<table>.gql` | `type … @table(name, key)`; `@col`/`@unique`/`@index`/`@default` |
662
+ | `data-connect-operations`| document | `<connector>/operations.gql` + `.ts`; `split: true` → `<connector>/operations/<entity>/{queries,mutations}.gql` | queries/mutations (where ops, orderBy, limit, aggregate, exprs, inc), per connector; split matches the real Data Connect repo layout |
585
663
  | `data-connect-adapter` | document | `data-connect-adapters.ts` | convert `Any` rows ⇄ logical types |
586
664
  | `firestore-types` | document | `firestore-types.ts` | naive TS types with Firestore `Timestamp` (all models) |
587
- | `firestore` | document | `firestore.ts` | Firestore projection Zod schemas (derived, denormalized) |
588
- | `firestore-split` | document | `firestore/<collection>.ts` + `firestore.ts` barrel | one file per projection; `_meta_` under `firestore/_/`; DC-schema `.pick()` reuse; `FIRESTORE_DATABASES` constants in the barrel |
665
+ | `firestore` | document | `firestore.ts`; `split: true` → `firestore/<collection>.ts` + barrel | Firestore projection Zod schemas (derived, denormalized); `extends` fragments, pinned value objects under their `out` (e.g. `firestore/_/`), DC-schema `.pick()` reuse, `FIRESTORE_DATABASES` constants in the barrel |
589
666
  | `api-types` | api | `api-types.ts` | endpoint request/response types |
590
667
  | `api-validation` | api | `api-validation.ts` | endpoint request-validation Zod |
591
668
  | `api-dto` | api | `<operation>.dto.ts` per api | class-validator DTO classes (NestJS `dto/` convention) |
@@ -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;AAmND;;;;;;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) {
@@ -145,25 +183,16 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
145
183
  }
146
184
  if ((generator.scope ?? 'document') === 'api')
147
185
  continue; // applied via entries, not by declaration
148
- // Document-scoped `split` toggles between a generator and its `-split`
149
- // variant (e.g. typescript ⇄ typescript-split) instead of reshaping output.
150
- let generatorName = decl.generator;
151
- if (decl.split === true && !generatorName.endsWith('-split')) {
152
- const splitName = `${generatorName}-split`;
153
- if (registry.get(splitName)) {
154
- generatorName = splitName;
155
- }
156
- else {
157
- diagnostics.push(error('UNSUPPORTED_SPLIT', `Generator "${decl.generator}" has no split variant (${doc.filePath})`));
186
+ const output = {};
187
+ // Document scope: `split: true` selects the generator's per-item layout
188
+ // (dispatched inside the generator); reject it when none exists.
189
+ if (decl.split !== undefined) {
190
+ if (decl.split === true && generator.splittable !== true) {
191
+ diagnostics.push(error('UNSUPPORTED_SPLIT', `Generator "${decl.generator}" has no split layout (${doc.filePath})`));
158
192
  continue;
159
193
  }
194
+ output.split = decl.split;
160
195
  }
161
- else if (decl.split === false && generatorName.endsWith('-split')) {
162
- const baseName = generatorName.slice(0, -'-split'.length);
163
- if (registry.get(baseName))
164
- generatorName = baseName;
165
- }
166
- const output = {};
167
196
  if (decl.file !== undefined) {
168
197
  if (API_PLACEHOLDER_RE.test(decl.file)) {
169
198
  diagnostics.push(error('INVALID_OUT_TEMPLATE', `file "${decl.file}" uses api placeholders but generator "${decl.generator}" is document-scoped (${doc.filePath})`));
@@ -181,7 +210,7 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
181
210
  continue;
182
211
  jobs.push({
183
212
  source: doc.filePath,
184
- generatorName,
213
+ generatorName: decl.generator,
185
214
  outDir,
186
215
  ...(Object.keys(output).length > 0 ? { output } : {}),
187
216
  ...(decl.header !== undefined ? { header: decl.header } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"api-dto-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/api/api-dto-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAgC,MAAM,iBAAiB,CAAA;AAoHxF,eAAO,MAAM,qBAAqB,QAAO,SAuBvC,CAAA"}
1
+ {"version":3,"file":"api-dto-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/api/api-dto-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAgC,MAAM,iBAAiB,CAAA;AA8GxF,eAAO,MAAM,qBAAqB,QAAO,SAuBvC,CAAA"}
@@ -3,6 +3,7 @@ import { pascalCase, singleQuote } from '../support/naming.js';
3
3
  import { isRelation, relationFkName } from '../support/relations.js';
4
4
  import { selectApis } from '../generator.js';
5
5
  import { headerBlocks } from '../support/header.js';
6
+ import { jsdoc } from '../support/jsdoc.js';
6
7
  import { emitApiFiles, resolveOutput } from '../support/templates.js';
7
8
  const SCALAR_TS = {
8
9
  string: 'string',
@@ -96,14 +97,6 @@ const decoratorsFor = (ir, field, used) => {
96
97
  }
97
98
  return out;
98
99
  };
99
- const jsdoc = (description, indent) => {
100
- if (!description)
101
- return '';
102
- const lines = description.split('\n');
103
- if (lines.length === 1)
104
- return `${indent}/** ${lines[0]} */\n`;
105
- return `${indent}/**\n${lines.map(line => `${indent} * ${line}`.trimEnd()).join('\n')}\n${indent} */\n`;
106
- };
107
100
  const renderDto = (ir, api, used) => {
108
101
  const fields = payloadFields(ir, api.request);
109
102
  const className = `${pascalCase(api.name)}Dto`;
@@ -1 +1 @@
1
- {"version":3,"file":"api-types-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/api/api-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAgC,MAAM,iBAAiB,CAAA;AAsFxF,eAAO,MAAM,uBAAuB,QAAO,SA+BzC,CAAA"}
1
+ {"version":3,"file":"api-types-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/api/api-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAgC,MAAM,iBAAiB,CAAA;AA4FxF,eAAO,MAAM,uBAAuB,QAAO,SA+BzC,CAAA"}
@@ -2,6 +2,7 @@ import { pascalCase } from '../support/naming.js';
2
2
  import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
3
3
  import { selectApis } from '../generator.js';
4
4
  import { headerBlocks } from '../support/header.js';
5
+ import { jsdoc } from '../support/jsdoc.js';
5
6
  import { emitApiFiles, resolveOutput } from '../support/templates.js';
6
7
  const SCALAR_TS = {
7
8
  string: 'string',
@@ -34,8 +35,7 @@ const renderField = (ir, field) => {
34
35
  const name = isRelation(field) ? relationFkName(field) : field.name;
35
36
  const optional = field.optional ? '?' : '';
36
37
  const nullable = field.nullable ? ' | null' : '';
37
- const doc = field.description ? ` /** ${field.description} */\n` : '';
38
- return `${doc} ${name}${optional}: ${renderFieldTsType(ir, field)}${nullable}`;
38
+ return `${jsdoc(field.description, ' ')} ${name}${optional}: ${renderFieldTsType(ir, field)}${nullable}`;
39
39
  };
40
40
  const renderPayloadType = (ir, payload, typeName) => {
41
41
  if (payload.isVoid) {
@@ -74,9 +74,15 @@ const apiComment = (api) => {
74
74
  const bits = [`kind: ${api.kind}`];
75
75
  if (api.method)
76
76
  bits.push(`method: ${api.method}`);
77
- if (api.description)
78
- bits.unshift(api.description);
79
- return `// ${api.name} — ${bits.join(' | ')}`;
77
+ const description = api.description?.split('\n') ?? [];
78
+ // Single-line descriptions stay inline; multi-line ones get their own
79
+ // comment lines so literal-block yml descriptions survive verbatim.
80
+ if (description.length <= 1) {
81
+ if (description[0])
82
+ bits.unshift(description[0]);
83
+ return `// ${api.name} — ${bits.join(' | ')}`;
84
+ }
85
+ return [`// ${api.name} — ${bits.join(' | ')}`, ...description.map(line => `// ${line}`.trimEnd())].join('\n');
80
86
  };
81
87
  /**
82
88
  * Generates request/response TypeScript types for each API endpoint. A payload
@@ -3,6 +3,7 @@ import { constantCase, pascalCase, singleQuote } from '../support/naming.js';
3
3
  import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
4
4
  import { selectApis } from '../generator.js';
5
5
  import { headerBlocks } from '../support/header.js';
6
+ import { jsdoc } from '../support/jsdoc.js';
6
7
  import { emitApiFiles, resolveOutput } from '../support/templates.js';
7
8
  const SCALAR_TS = {
8
9
  string: 'string',
@@ -30,13 +31,12 @@ const fieldTs = (ir, field) => {
30
31
  };
31
32
  const renderMember = (ir, field) => {
32
33
  const name = isRelation(field) ? relationFkName(field) : field.name;
33
- const doc = field.description ? ` /** ${field.description} */\n` : '';
34
34
  const nullable = field.nullable ? ' | null' : '';
35
- return `${doc} ${name}${field.optional ? '?' : ''}: ${fieldTs(ir, field)}${nullable}`;
35
+ return `${jsdoc(field.description, ' ')} ${name}${field.optional ? '?' : ''}: ${fieldTs(ir, field)}${nullable}`;
36
36
  };
37
37
  const payloadFields = (ir, payload) => payload.model ? (findModel(ir, payload.model)?.fields ?? []) : payload.fields;
38
38
  const renderEnvelope = (env) => {
39
- const doc = env.description ? `/** ${env.description} */\n` : '';
39
+ const doc = jsdoc(env.description, '');
40
40
  const members = env.fields.map(field => ` ${field.name}${field.optional ? '?' : ''}: ${SCALAR_TS[field.type.kind === 'scalar' ? field.type.name : 'string']}`).join('\n');
41
41
  return `${doc}export type ${env.name}<T> = T & {\n${members}\n}`;
42
42
  };
@@ -5,11 +5,4 @@ import { Generator } from '../generator.js';
5
5
  * comment, matching the "JSON is Any, logical type preserved" rule.
6
6
  */
7
7
  export declare const createDataConnectGraphqlGenerator: () => Generator;
8
- /**
9
- * Split variant matching the real Data Connect layout: one file per table at
10
- * `schema/<table_name>.gql`, with each enum co-located in the file of the first
11
- * model that references it (leftover enums go to `schema/_enums.gql`). The `Any`
12
- * scalar is not declared — Data Connect provides it built-in.
13
- */
14
- export declare const createDataConnectGraphqlSplitGenerator: () => Generator;
15
8
  //# sourceMappingURL=graphql-generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphql-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/data-connect/graphql-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AA+I5E;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,QAAO,SA+BnD,CAAA;AAaF;;;;;GAKG;AACH,eAAO,MAAM,sCAAsC,QAAO,SAmCxD,CAAA"}
1
+ {"version":3,"file":"graphql-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/data-connect/graphql-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAgJ5E;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,QAAO,SAmClD,CAAA"}
@@ -2,6 +2,7 @@ import { headerBlocks } from '../support/header.js';
2
2
  import { constantCase, pluralize, snakeCase } from '../support/naming.js';
3
3
  import { isRelation } from '../support/relations.js';
4
4
  import { outputFile } from '../support/templates.js';
5
+ import { withSplitVariant } from '../support/split-variant.js';
5
6
  // Data Connect built-in scalars. `json` and embedded model objects are stored
6
7
  // as `Any` — the logical type is preserved in a comment and restored by the
7
8
  // Data Connect adapter generator.
@@ -131,7 +132,7 @@ const commentBanner = (header) => header
131
132
  * model fields become the `Any` scalar while retaining their logical type as a
132
133
  * comment, matching the "JSON is Any, logical type preserved" rule.
133
134
  */
134
- export const createDataConnectGraphqlGenerator = () => ({
135
+ export const createDataConnectGraphqlGenerator = () => withSplitVariant({
135
136
  name: 'data-connect-graphql',
136
137
  description: 'Data Connect GraphQL schema',
137
138
  generate(ir, context) {
@@ -154,7 +155,7 @@ export const createDataConnectGraphqlGenerator = () => ({
154
155
  }
155
156
  return [{ path: outputFile(context, 'schema.gql'), content: `${blocks.join('\n\n')}\n` }];
156
157
  },
157
- });
158
+ }, createDataConnectGraphqlSplitLayout());
158
159
  const renderEnumGql = (irEnum, rename) => {
159
160
  const lines = [...commentLines(irEnum.description, '')];
160
161
  lines.push(`enum ${gqlOf(rename, irEnum.name)} {`);
@@ -171,8 +172,8 @@ const renderEnumGql = (irEnum, rename) => {
171
172
  * model that references it (leftover enums go to `schema/_enums.gql`). The `Any`
172
173
  * scalar is not declared — Data Connect provides it built-in.
173
174
  */
174
- export const createDataConnectGraphqlSplitGenerator = () => ({
175
- name: 'data-connect-graphql-split',
175
+ const createDataConnectGraphqlSplitLayout = () => ({
176
+ name: 'data-connect-graphql',
176
177
  description: 'Data Connect GraphQL schema, one file per table (schema/<table>.gql)',
177
178
  generate(ir, context) {
178
179
  const banner = headerBlocks(context, commentBanner);