firebase-contract 0.1.0-rc.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 (152) hide show
  1. package/README.md +560 -0
  2. package/dist/index.d.ts +15 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +12 -0
  5. package/dist/lib/_internal/compiler/compiler.d.ts +71 -0
  6. package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -0
  7. package/dist/lib/_internal/compiler/compiler.js +104 -0
  8. package/dist/lib/_internal/compiler/index.d.ts +3 -0
  9. package/dist/lib/_internal/compiler/index.d.ts.map +1 -0
  10. package/dist/lib/_internal/compiler/index.js +1 -0
  11. package/dist/lib/_internal/diagnostics.d.ts +29 -0
  12. package/dist/lib/_internal/diagnostics.d.ts.map +1 -0
  13. package/dist/lib/_internal/diagnostics.js +26 -0
  14. package/dist/lib/_internal/format-diagnostics.d.ts +8 -0
  15. package/dist/lib/_internal/format-diagnostics.d.ts.map +1 -0
  16. package/dist/lib/_internal/format-diagnostics.js +10 -0
  17. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts +8 -0
  18. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts.map +1 -0
  19. package/dist/lib/_internal/generators/api/api-dto-generator.js +134 -0
  20. package/dist/lib/_internal/generators/api/api-types-generator.d.ts +8 -0
  21. package/dist/lib/_internal/generators/api/api-types-generator.d.ts.map +1 -0
  22. package/dist/lib/_internal/generators/api/api-types-generator.js +105 -0
  23. package/dist/lib/_internal/generators/api/api-validation-generator.d.ts +7 -0
  24. package/dist/lib/_internal/generators/api/api-validation-generator.d.ts.map +1 -0
  25. package/dist/lib/_internal/generators/api/api-validation-generator.js +73 -0
  26. package/dist/lib/_internal/generators/api/task-payload-generator.d.ts +9 -0
  27. package/dist/lib/_internal/generators/api/task-payload-generator.d.ts.map +1 -0
  28. package/dist/lib/_internal/generators/api/task-payload-generator.js +96 -0
  29. package/dist/lib/_internal/generators/config/config-generator.d.ts +9 -0
  30. package/dist/lib/_internal/generators/config/config-generator.d.ts.map +1 -0
  31. package/dist/lib/_internal/generators/config/config-generator.js +71 -0
  32. package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts +8 -0
  33. package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts.map +1 -0
  34. package/dist/lib/_internal/generators/data-connect/adapter-generator.js +142 -0
  35. package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts +15 -0
  36. package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts.map +1 -0
  37. package/dist/lib/_internal/generators/data-connect/graphql-generator.js +209 -0
  38. package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts +18 -0
  39. package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts.map +1 -0
  40. package/dist/lib/_internal/generators/data-connect/operations-generator.js +539 -0
  41. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts +11 -0
  42. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts.map +1 -0
  43. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.js +112 -0
  44. package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts +11 -0
  45. package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts.map +1 -0
  46. package/dist/lib/_internal/generators/firestore/firestore-split-generator.js +375 -0
  47. package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts +12 -0
  48. package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts.map +1 -0
  49. package/dist/lib/_internal/generators/firestore/firestore-type-generator.js +81 -0
  50. package/dist/lib/_internal/generators/generator.d.ts +23 -0
  51. package/dist/lib/_internal/generators/generator.d.ts.map +1 -0
  52. package/dist/lib/_internal/generators/generator.js +1 -0
  53. package/dist/lib/_internal/generators/id/id-codec-generator.d.ts +16 -0
  54. package/dist/lib/_internal/generators/id/id-codec-generator.d.ts.map +1 -0
  55. package/dist/lib/_internal/generators/id/id-codec-generator.js +140 -0
  56. package/dist/lib/_internal/generators/index.d.ts +31 -0
  57. package/dist/lib/_internal/generators/index.d.ts.map +1 -0
  58. package/dist/lib/_internal/generators/index.js +64 -0
  59. package/dist/lib/_internal/generators/registry.d.ts +15 -0
  60. package/dist/lib/_internal/generators/registry.d.ts.map +1 -0
  61. package/dist/lib/_internal/generators/registry.js +24 -0
  62. package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts +9 -0
  63. package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts.map +1 -0
  64. package/dist/lib/_internal/generators/sql/sql-migration-generator.js +44 -0
  65. package/dist/lib/_internal/generators/support/constraints.d.ts +8 -0
  66. package/dist/lib/_internal/generators/support/constraints.d.ts.map +1 -0
  67. package/dist/lib/_internal/generators/support/constraints.js +39 -0
  68. package/dist/lib/_internal/generators/support/header.d.ts +13 -0
  69. package/dist/lib/_internal/generators/support/header.d.ts.map +1 -0
  70. package/dist/lib/_internal/generators/support/header.js +17 -0
  71. package/dist/lib/_internal/generators/support/naming.d.ts +10 -0
  72. package/dist/lib/_internal/generators/support/naming.d.ts.map +1 -0
  73. package/dist/lib/_internal/generators/support/naming.js +30 -0
  74. package/dist/lib/_internal/generators/support/relations.d.ts +12 -0
  75. package/dist/lib/_internal/generators/support/relations.d.ts.map +1 -0
  76. package/dist/lib/_internal/generators/support/relations.js +11 -0
  77. package/dist/lib/_internal/generators/support/split.d.ts +34 -0
  78. package/dist/lib/_internal/generators/support/split.d.ts.map +1 -0
  79. package/dist/lib/_internal/generators/support/split.js +75 -0
  80. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts +18 -0
  81. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -0
  82. package/dist/lib/_internal/generators/typescript/typescript-generator.js +99 -0
  83. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts +10 -0
  84. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -0
  85. package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +175 -0
  86. package/dist/lib/_internal/generators/union/union-generator.d.ts +8 -0
  87. package/dist/lib/_internal/generators/union/union-generator.d.ts.map +1 -0
  88. package/dist/lib/_internal/generators/union/union-generator.js +37 -0
  89. package/dist/lib/_internal/generators/zod/zod-generator.d.ts +7 -0
  90. package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -0
  91. package/dist/lib/_internal/generators/zod/zod-generator.js +68 -0
  92. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts +9 -0
  93. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -0
  94. package/dist/lib/_internal/generators/zod/zod-split-generator.js +136 -0
  95. package/dist/lib/_internal/ir/build-ir.d.ts +18 -0
  96. package/dist/lib/_internal/ir/build-ir.d.ts.map +1 -0
  97. package/dist/lib/_internal/ir/build-ir.js +430 -0
  98. package/dist/lib/_internal/ir/ir.d.ts +393 -0
  99. package/dist/lib/_internal/ir/ir.d.ts.map +1 -0
  100. package/dist/lib/_internal/ir/ir.js +22 -0
  101. package/dist/lib/_internal/parser/parse.d.ts +9 -0
  102. package/dist/lib/_internal/parser/parse.d.ts.map +1 -0
  103. package/dist/lib/_internal/parser/parse.js +682 -0
  104. package/dist/lib/_internal/parser/raw-document.d.ts +230 -0
  105. package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -0
  106. package/dist/lib/_internal/parser/raw-document.js +6 -0
  107. package/dist/lib/_internal/resolver/import-resolver.d.ts +25 -0
  108. package/dist/lib/_internal/resolver/import-resolver.d.ts.map +1 -0
  109. package/dist/lib/_internal/resolver/import-resolver.js +73 -0
  110. package/dist/lib/_internal/resolver/module-loader.d.ts +25 -0
  111. package/dist/lib/_internal/resolver/module-loader.d.ts.map +1 -0
  112. package/dist/lib/_internal/resolver/module-loader.js +62 -0
  113. package/dist/lib/_internal/runtime/data-connect-adapter.d.ts +25 -0
  114. package/dist/lib/_internal/runtime/data-connect-adapter.d.ts.map +1 -0
  115. package/dist/lib/_internal/runtime/data-connect-adapter.js +24 -0
  116. package/dist/lib/_internal/runtime/index.d.ts +3 -0
  117. package/dist/lib/_internal/runtime/index.d.ts.map +1 -0
  118. package/dist/lib/_internal/runtime/index.js +1 -0
  119. package/dist/lib/_internal/validation/rules.d.ts +32 -0
  120. package/dist/lib/_internal/validation/rules.d.ts.map +1 -0
  121. package/dist/lib/_internal/validation/rules.js +282 -0
  122. package/dist/lib/_internal/validation/validate.d.ts +9 -0
  123. package/dist/lib/_internal/validation/validate.d.ts.map +1 -0
  124. package/dist/lib/_internal/validation/validate.js +6 -0
  125. package/dist/lib/cli/commands/generate.d.ts +3 -0
  126. package/dist/lib/cli/commands/generate.d.ts.map +1 -0
  127. package/dist/lib/cli/commands/generate.js +66 -0
  128. package/dist/lib/cli/commands/index.d.ts +4 -0
  129. package/dist/lib/cli/commands/index.d.ts.map +1 -0
  130. package/dist/lib/cli/commands/index.js +11 -0
  131. package/dist/lib/cli/commands/init.d.ts +3 -0
  132. package/dist/lib/cli/commands/init.d.ts.map +1 -0
  133. package/dist/lib/cli/commands/init.js +72 -0
  134. package/dist/lib/cli/commands/inspect.d.ts +3 -0
  135. package/dist/lib/cli/commands/inspect.d.ts.map +1 -0
  136. package/dist/lib/cli/commands/inspect.js +23 -0
  137. package/dist/lib/cli/commands/validate.d.ts +3 -0
  138. package/dist/lib/cli/commands/validate.d.ts.map +1 -0
  139. package/dist/lib/cli/commands/validate.js +23 -0
  140. package/dist/lib/cli/config.d.ts +12 -0
  141. package/dist/lib/cli/config.d.ts.map +1 -0
  142. package/dist/lib/cli/config.js +20 -0
  143. package/dist/lib/cli/index.d.ts +3 -0
  144. package/dist/lib/cli/index.d.ts.map +1 -0
  145. package/dist/lib/cli/index.js +13 -0
  146. package/dist/lib/cli/version.d.ts +3 -0
  147. package/dist/lib/cli/version.d.ts.map +1 -0
  148. package/dist/lib/cli/version.js +15 -0
  149. package/dist/lib/modules/runtime.d.ts +2 -0
  150. package/dist/lib/modules/runtime.d.ts.map +1 -0
  151. package/dist/lib/modules/runtime.js +3 -0
  152. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,560 @@
1
+ # firebase-contract
2
+
3
+ Treat a **YAML contract as the single source of truth** for a Firebase app and
4
+ generate every downstream representation from it — so shared types, enums, and
5
+ validation stop being hand-maintained in parallel (and stop drifting).
6
+
7
+ From one contract, generate:
8
+
9
+ - **TypeScript** types (interfaces + frozen-const enums)
10
+ - **Zod** validation schemas
11
+ - **Data Connect** GraphQL schema (`@table`, keys, directives), query/mutation
12
+ operations (routed per connector), and `Any`↔logical adapters
13
+ - **Firestore** projection schemas (denormalized read model with `_meta_`)
14
+ - **API** request/response types, request-validation Zod, and class-validator DTOs
15
+ - **Cloud Task / Pub/Sub** payload envelopes + delivery constants
16
+ - **SQL migrations** for constraints Data Connect can't express (composite FKs,
17
+ CHECK, indexes)
18
+ - **Per-entity id codecs** (typed encode/decode wrappers)
19
+ - **Discriminated unions**
20
+ - **Project config** (`dataconnect.yaml`, `connector.yaml`) + sync constants
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm i -D firebase-contract
26
+ ```
27
+
28
+ Provides the `firebase-contract` (alias `fbc`) CLI and a programmatic API.
29
+
30
+ ## Quick start
31
+
32
+ ```bash
33
+ fbc init # scaffold contract.yml + firebase-contract.json
34
+ fbc validate # parse, resolve imports, semantically validate
35
+ fbc generate # run every `generate:` target declared in the contract graph
36
+ fbc inspect # print the normalized IR (debugging)
37
+ ```
38
+
39
+ ### One-command generation
40
+
41
+ Each contract file can declare its own outputs in a `generate:` section —
42
+ out dirs are relative to that yml file:
43
+
44
+ ```yaml
45
+ # apps/shop/data-connect/schema.yml
46
+ generate:
47
+ - { out: src, generators: [data-connect-graphql-split] }
48
+ - { out: ., generators: [sql-migrations] }
49
+ ```
50
+
51
+ ```yaml
52
+ # contract.yml (root)
53
+ generate:
54
+ - out: libs/contracts/src
55
+ generators: [typescript-split, zod-split, firestore-split, id-codecs]
56
+ imports:
57
+ - ./apps/shop/data-connect/schema.yml
58
+ ```
59
+
60
+ `fbc generate contract.yml` then materializes **all** targets across the whole
61
+ import graph in one run. Passing `-o`/`-g` switches to single-target mode
62
+ (`fbc generate <entry> -o <dir> -g typescript,zod`), and
63
+ `firebase-contract.json` (`entry`, `outDir`, `generators`) can hold defaults.
64
+
65
+ ### Generated-file headers
66
+
67
+ By default no banner is emitted. Opt in per run or per contract:
68
+
69
+ ```bash
70
+ fbc generate --header # default AUTO-GENERATED banner
71
+ fbc generate --header "Managed by fbc" # custom text (multi-line ok)
72
+ ```
73
+
74
+ ```yaml
75
+ header: default # or any text; the CLI --header flag wins
76
+ ```
77
+
78
+ Comment syntax adapts per file type (`//` for TS, `#` for GraphQL/YAML-ish,
79
+ `--` for SQL).
80
+
81
+ ---
82
+
83
+ # The YAML DSL
84
+
85
+ A contract is composed of top-level sections — `enums`, `models`, `operations`,
86
+ `apis`, `firestore`, `unions`, `envelopes`, `project` — and `imports` that pull
87
+ in other contracts (relative paths or npm packages). Imports are resolved
88
+ transitively with cycle/duplicate detection and diamond dedup, so you can split
89
+ a large contract across files that mirror your repo layout.
90
+
91
+ ```yaml
92
+ version: 1
93
+ imports:
94
+ - ./apps/shop/data-connect/schema.yml
95
+ - some-shared-package/contract.yml
96
+ ```
97
+
98
+ ## Enums
99
+
100
+ ```yaml
101
+ enums:
102
+ ProductStatus:
103
+ description: Lifecycle state of a product
104
+ values: [DRAFT, PUBLISHED, ARCHIVED]
105
+
106
+ ShippingSpeed:
107
+ description: |-
108
+ Multi-line descriptions become multi-line comments.
109
+ values:
110
+ - { value: S, key: STANDARD } # const-object key override → STANDARD: 'S'
111
+ - { value: X, key: EXPRESS, description: per-value comment }
112
+ ```
113
+
114
+ Enum options: `description`, `gqlName` (GraphQL rendering name),
115
+ `fsName` / `fsDescription` (Firestore-side rename/comment — e.g. render DC's
116
+ `Status` as `ReviewStatus` in projections), and per-value `key` /
117
+ `description` overrides.
118
+
119
+ By default the TypeScript generator emits the **frozen-const** representation
120
+ (runtime values + `Key` and value types) — the shape you'd otherwise hand-write
121
+ and keep in sync with the DC enum and the Zod enum:
122
+
123
+ ```ts
124
+ export const PRODUCT_STATUS = Object.freeze({ DRAFT: 'DRAFT', PUBLISHED: 'PUBLISHED', ARCHIVED: 'ARCHIVED' } as const)
125
+ export type ProductStatusKey = keyof typeof PRODUCT_STATUS
126
+ export type ProductStatus = (typeof PRODUCT_STATUS)[ProductStatusKey]
127
+ ```
128
+
129
+ Pass `enumStyle: 'union'` to the TypeScript generator for a plain
130
+ `'OPEN' | 'DONE' | 'DISABLED'` union instead.
131
+
132
+ ## Models
133
+
134
+ ```yaml
135
+ models:
136
+ User:
137
+ fields:
138
+ id: { type: id, id: true }
139
+ name: string # shorthand — the value is the type
140
+ email: { type: string, email: true }
141
+
142
+ Product:
143
+ description: A unit of work
144
+ key: [catalog, productNo] # composite primary key
145
+ indexes:
146
+ - { fields: [catalog, productNo], unique: true }
147
+ - { fields: [status, createdAt] }
148
+ fields:
149
+ catalog: { type: Catalog, relation: true }
150
+ productNo: int
151
+ title: { type: string, nonempty: true, maxLength: 200 }
152
+ status: ProductStatus
153
+ metadata: { type: ProductMetadata, optional: true } # embedded → Any
154
+ createdAt: timestamp
155
+ ```
156
+
157
+ ### Scalar types
158
+
159
+ `string`, `int`, `float`, `boolean`, `timestamp`, `date`, `json`, `id`.
160
+
161
+ ### Field options
162
+
163
+ | option | meaning |
164
+ | ----------------------------------- | -------------------------------------------------------- |
165
+ | `type` | scalar name, or an enum/model name |
166
+ | `optional` | value may be absent (`field?`) |
167
+ | `list` | value is an array of `type` |
168
+ | `id` | marks the (single) primary identifier |
169
+ | `unique` | field-level unique → `@unique` |
170
+ | `relation` | model-typed field is a foreign-key relation (see below) |
171
+ | `col` | Data Connect column dataType (Int64 PKs default to `bigserial`) |
172
+ | `default` | `@default(expr: …)` on the DC column |
173
+ | `literal` | pin to one literal value (union discriminant tags) |
174
+ | `nullable` | value may be `null` (`.nullable()` / `\| null`) |
175
+ | `description` | doc comment carried into generated output |
176
+ | `jsdoc` | render the description as a JSDoc block (Firestore fields) |
177
+ | **constraints** → | |
178
+ | `min` / `max` | numeric bounds |
179
+ | `minLength` / `maxLength` | string/array length bounds |
180
+ | `nonempty` | non-empty string/array |
181
+ | `pattern` | regex the string must match |
182
+ | `email` / `url` | string format |
183
+
184
+ Constraints flow into the Zod schemas, the API request validation, and the
185
+ class-validator DTOs.
186
+
187
+ ### Model options
188
+
189
+ | option | meaning |
190
+ | ------------ | ------------------------------------------------------------------ |
191
+ | `key` | composite primary key field names (else the `id: true` field) |
192
+ | `table` | table name override (default: snake_case pluralization) |
193
+ | `gqlName` | GraphQL type name override |
194
+ | `fsName` | Firestore-side rename (avoid collisions with table models) |
195
+ | `directives` | `multi` = each type-level directive on its own line |
196
+ | `footer` | trailing comment block after the closing `}` in the schema file |
197
+ | `indexes` | composite `@index`/`@unique` (`{ fields, name?, unique?, expand? }` — `expand` renders args one per line) |
198
+ | `sql` | raw SQL constraints (see [SQL migrations](#sql-migrations)) |
199
+
200
+ ### Embedded vs relation
201
+
202
+ A field whose `type` is another model is one of two things:
203
+
204
+ - **embedded** (default) — a nested value object. Nested in TypeScript /
205
+ Firestore / Zod; stored as `Any` (jsonb) in Data Connect with the logical type
206
+ preserved and restored by the adapter.
207
+ - **relation** (`relation: true`) — a foreign-key reference to another table.
208
+ Data Connect emits a relation (`owner: User!`, auto-creating the FK column);
209
+ TypeScript / Firestore / Zod expose the foreign-key id (`ownerId`).
210
+
211
+ ```yaml
212
+ fields:
213
+ profile: { type: ProfileImage } # embedded → Any + adapter
214
+ owner: { type: User, relation: true } # relation → owner: User! / ownerId
215
+ ```
216
+
217
+ ## Data Connect operations
218
+
219
+ Query/mutation operations over a model's table. Emits `.gql` (over the
220
+ auto-generated `<table>_insert/update/upsert/delete` resolvers, with `@auth`
221
+ directives) plus TS `Variables`/`Result` types. Operations are **routed per
222
+ connector** — an operation may target several connectors and is emitted into
223
+ each, under `<connector>/operations.gql`.
224
+
225
+ ```yaml
226
+ defaults:
227
+ connectors: [app] # file-level default for operations below
228
+
229
+ operations:
230
+ CreateShop:
231
+ type: mutation
232
+ model: Shop
233
+ action: insert # insert | update | upsert | delete
234
+ auth: NO_ACCESS # NO_ACCESS | PUBLIC | USER
235
+ connectors: [app, api] # emitted into both connectors
236
+ inputs: [type, ownerUser, name, slug, status]
237
+
238
+ IncrementSeq:
239
+ type: mutation
240
+ model: Shop
241
+ action: update
242
+ inc: [projectNoSeq] # → projectNoSeq_update: { inc: 1 }
243
+ exprs: { updatedAt: request.time } # → updatedAt_expr: "request.time"
244
+
245
+ SearchProducts:
246
+ type: query
247
+ model: Product
248
+ auth: PUBLIC
249
+ authReason: gated at the app layer # required when auth is PUBLIC
250
+ where:
251
+ - { field: title, op: contains } # eq | contains | lt | le | gt | ge | ne
252
+ - status # shorthand → { field: status, op: eq }
253
+ orderBy:
254
+ - { field: createdAt, dir: DESC }
255
+ limit: 20
256
+ select: [id, title, status]
257
+
258
+ UsageTotals:
259
+ type: query
260
+ model: UsageLog
261
+ where: [{ field: shop, op: eq }]
262
+ aggregate:
263
+ count: true
264
+ sum: [weightedAmount, inputTokens]
265
+ ```
266
+
267
+ Relation inputs thread through as `owner: { id: $ownerId }`, matching Data
268
+ Connect's relation-reference form. Composite keys become `key: { a: $a, b: $b }`.
269
+
270
+ ### Fidelity options
271
+
272
+ Everything below exists to reproduce real hand-written `.gql` files byte-for-byte:
273
+
274
+ | option | meaning |
275
+ | --- | --- |
276
+ | `gqlName` | rendered operation name (the yml key stays unique across services) |
277
+ | `footer` | trailing `# …` comment block after the operation |
278
+ | `raw` | emit the operation body verbatim (multi-model queries, `_or` keyset cursors — field checks are skipped) |
279
+ | `single: id \| key` | single-row lookup (`product(id: $id)` / `product(key: { … })`) |
280
+ | `keyArg` / `keyVars` | update/delete key argument form and variable renames |
281
+ | `whereAnd: true` | render conditions as an `_and: [ … ]` array |
282
+ | `entityDir` | override the `operations/<entity>/` output directory |
283
+ | `style` | formatting hints: `signature`, `args`, `data`, `orderBy`, `auth`, `key`, `and`, `where` (`inline`/`multi`/`compact`/`bare`) |
284
+
285
+ Inputs support `var` (variable rename), `required` (override optionality),
286
+ `literal` (fixed value, no variable), `flat` (write the FK column directly),
287
+ `inc` (increment write), and `asKey` (pass a relation as a `Model_Key` object
288
+ variable — `$catalogVersion: CatalogVersion_Key`). `where` fields support dotted
289
+ paths through relations (`review.catalog.id` → nested `review: { catalog: { id: { eq: … } } }`),
290
+ `literal` comparisons, and `flat` FK reads. `select` supports nested selections,
291
+ aliases, arguments, and reverse joins (`comments: reviewComments_on_product`).
292
+ `limit` may be a number or a variable (`{ var, default?, required? }`).
293
+
294
+ Operations are namespaced **per connector**: two operations may share a rendered
295
+ name as long as their connector sets don't overlap.
296
+
297
+ ## API endpoints
298
+
299
+ Model application endpoints (Cloud Task / callable / https / pubsub). Generates
300
+ request/response types (`api-types`), request-validation Zod (`api-validation`),
301
+ class-validator DTOs (`api-dto`), and — for `task`/`pubsub` — payload contracts
302
+ (`task-payloads`). A payload references a model or declares inline fields; a void
303
+ response maps to `void`.
304
+
305
+ ```yaml
306
+ envelopes:
307
+ RetryTaskPayload: # RetryTaskPayload<T> = T & { … }
308
+ fields:
309
+ identifierId: string
310
+ opId: string
311
+ enqueuedAt: int
312
+
313
+ apis:
314
+ createCatalog:
315
+ kind: task # product | callable | https | pubsub
316
+ envelope: RetryTaskPayload # wraps the product data
317
+ maxAttempts: 3
318
+ request:
319
+ fields:
320
+ shopId: string
321
+ catalogId: string
322
+ response:
323
+ void: true
324
+
325
+ generateAiResponse:
326
+ kind: pubsub
327
+ topic: ai-review-generate-response
328
+ timeoutSeconds: 540
329
+
330
+ getShopBySlug:
331
+ kind: callable
332
+ request: { fields: { slug: { type: string, nonempty: true } } }
333
+ response: { model: Shop }
334
+ ```
335
+
336
+ For `createCatalog` this yields `CreateCatalogTaskData`, `CreateCatalogTaskPayload =
337
+ RetryTaskPayload<CreateCatalogTaskData>`, and `CREATE_CATALOG_MAX_ATTEMPTS`, plus the
338
+ request types, Zod schema, and DTO.
339
+
340
+ ## Firestore projections
341
+
342
+ Firestore is a **denormalized read model**, not the same shape as Data Connect.
343
+ A projection *derives* from a DC model (`from`) and applies the projection
344
+ reality automatically:
345
+
346
+ - **relations → resolved string ids** (`ownerUser` → `ownerUserId: z.string()`)
347
+ - **timestamps → `z.date()`**, optional fields → `.nullable()`
348
+ - a **`_meta_`** consistency envelope is attached (opt out with `meta: false`)
349
+ - `pick` / `omit` select which base fields survive
350
+ - inline `fields` add denormalized data (flattened DAG edges, stringified JSON)
351
+
352
+ ```yaml
353
+ firestore:
354
+ User:
355
+ from: User
356
+ collection: users/{userId}
357
+ pick: [displayName, photoURL, createdAt, updatedAt, lastLoginAt]
358
+ Product:
359
+ from: Product
360
+ collection: shops/{ws}/.../products/{productNo}
361
+ omit: [catalog, log]
362
+ fields:
363
+ parentProductNos: { type: int, list: true }
364
+ linkedCatalogTitle: { type: string, optional: true }
365
+ ```
366
+
367
+ Emits Zod schemas + inferred types, mirroring a hand-written Firestore schema
368
+ library. (The simpler `firestore-types` generator just re-types *every* model
369
+ with the Firestore `Timestamp` and does not denormalize.)
370
+
371
+ With `firestore-split`, fields whose Zod chain is **identical** to the Data
372
+ Connect schema are reused via `.pick()` — only representation changes are
373
+ restated:
374
+
375
+ ```ts
376
+ export const ShopSchema = DcShopSchema.pick({
377
+ type: true, name: true, slug: true, ownerUserId: true, status: true,
378
+ }).extend({
379
+ createdAt: z.date(),
380
+ updatedAt: z.date().nullable(),
381
+ _meta_: _Meta_Schema,
382
+ })
383
+ ```
384
+
385
+ Additional projection options:
386
+
387
+ - **override semantics** — fields declared under `fields:` use `nullable:` →
388
+ `.nullable()` and `optional:` → `.optional()` explicitly (base-picked fields
389
+ map DC-optional → `.nullable()` automatically); `default:` → `.default(…)`
390
+ - **`pick` order is authoritative** for the projected field order
391
+ - **`helpers:`** — verbatim TypeScript (e.g. small accessor functions) emitted
392
+ into the projection file
393
+ - **`include: [Model]`** — host a shared embedded schema in this file even when
394
+ no projected field references it
395
+ - FS-only enums render as frozen consts (`REVIEW_STATE` + `ReviewStateSchema`),
396
+ co-located with the first projection that references them
397
+
398
+ ## Discriminated unions
399
+
400
+ ```yaml
401
+ unions:
402
+ CatalogOperationDraft:
403
+ discriminant: operationType
404
+ variants: [AddLinkOperation, CutLinkOperation]
405
+ ```
406
+
407
+ Each variant is a model whose `operationType` field is pinned with `literal:` so
408
+ the output is a valid `z.discriminatedUnion('operationType', [...])` plus the TS
409
+ union type.
410
+
411
+ ## SQL migrations
412
+
413
+ Constraints Data Connect cannot express — composite foreign keys, CHECK
414
+ constraints, extra indexes — are declared per model and emitted as an executable
415
+ `migrations/constraints.sql`:
416
+
417
+ ```yaml
418
+ models:
419
+ ProductLink:
420
+ key: [catalog, parentProductNo, childProductNo]
421
+ sql:
422
+ checks:
423
+ - "parent_product_no != child_product_no"
424
+ foreignKeys:
425
+ - { columns: [catalog_id, parent_product_no], references: "products(catalog_id, product_no)" }
426
+ indexes:
427
+ - { columns: [catalog_id, child_product_no] }
428
+ ```
429
+
430
+ ## Id codecs
431
+
432
+ For each model with an id field, the `id-codecs` generator emits typed
433
+ `encode<Model>Id` / `decode<Model>Id` wrappers (numeric codec for Int64 ids,
434
+ string codec otherwise) over generic primitives from a configurable `core`
435
+ module — replacing scattered untyped `encodeNumericId` calls.
436
+
437
+ ## Project config
438
+
439
+ ```yaml
440
+ project:
441
+ services:
442
+ - { name: shop, database: shop }
443
+ - { name: warehouse, database: warehouse }
444
+ codebases:
445
+ shp: shop
446
+ idCodec:
447
+ minLength: 8
448
+ alphabet: <your-shuffled-sqids-alphabet>
449
+ ```
450
+
451
+ - `services` (`name`/`database`) feed the `FIRESTORE_DATABASES` constants in the
452
+ `firestore-split` barrel.
453
+ - `idCodec` pins the Sqids settings as a contract constant; when present, the
454
+ `id-codecs` generator also emits a self-contained `id-core.ts` (the
455
+ encode/decode primitives), so nothing about id encoding lives outside the
456
+ contract.
457
+ - `codebases` / service `location` + `connectors` are consumed by the optional
458
+ `config` generator (preview), which emits per-service `dataconnect.yaml`,
459
+ per-connector `connector.yaml`, and the `FIRESTORE_DATABASES` /
460
+ `API_CODEBASES` sync constants. It does not yet reproduce every field of a
461
+ real deployment config (e.g. `cloudSql` datasource details) — treat it as a
462
+ starting point, not a drop-in replacement.
463
+
464
+ ---
465
+
466
+ # Generators
467
+
468
+ | name | output | notes |
469
+ | ------------------------ | ----------------------------- | ------------------------------------------------------ |
470
+ | `typescript` | `types.ts` | interfaces + const/union enums |
471
+ | `typescript-split` | `types/<table>.ts` + `types.ts` barrel | one file per table; enums/embedded objects co-located |
472
+ | `zod` | `schemas.ts` | `z.object` schemas (constraints, `z.lazy` model refs) |
473
+ | `zod-split` | `schemas/<table>.ts` + `schemas.ts` barrel | split variant of `zod` |
474
+ | `data-connect-graphql` | `schema.gql` | `type … @table(name, key)`; `@col`/`@unique`/`@index`/`@default` |
475
+ | `data-connect-graphql-split` | `schema/<table>.gql` | same schema, one file per table (enums co-located) |
476
+ | `data-connect-operations`| `<connector>/operations.gql` + `.ts` | queries/mutations (where ops, orderBy, limit, aggregate, exprs, inc), per connector |
477
+ | `data-connect-operations-split` | `<connector>/operations/<entity>/{queries,mutations}.gql` | real Data Connect repo layout; shared types stay in `typescript`/`zod` outputs |
478
+ | `data-connect-adapter` | `data-connect-adapters.ts` | convert `Any` rows ⇄ logical types |
479
+ | `firestore-types` | `firestore-types.ts` | naive TS types with Firestore `Timestamp` (all models) |
480
+ | `firestore` | `firestore.ts` | Firestore projection Zod schemas (derived, denormalized) |
481
+ | `firestore-split` | `firestore/<collection>.ts` + `firestore.ts` barrel | one file per projection; `_meta_` under `firestore/_/`; DC-schema `.pick()` reuse; `FIRESTORE_DATABASES` constants in the barrel |
482
+ | `api-types` | `api-types.ts` | endpoint request/response types |
483
+ | `api-validation` | `api-validation.ts` | endpoint request-validation Zod |
484
+ | `api-dto` | `api-dtos.ts` | class-validator DTO classes |
485
+ | `task-payloads` | `task-payloads.ts` | envelopes + `*TaskData`/`*TaskPayload` + constants |
486
+ | `sql-migrations` | `migrations/constraints.sql` | composite FK / CHECK / index SQL |
487
+ | `id-codecs` | `id-codecs.ts` (+ `id-core.ts`) | typed per-entity id encode/decode wrappers; emits the Sqids primitives when `project.idCodec` is set |
488
+ | `unions` | `unions.ts` | Zod discriminated unions + TS union types |
489
+ | `config` | `dataconnect.yaml`, `connector.yaml`, `constants.ts` | DC configs + sync constants (preview — see [Project config](#project-config)) |
490
+
491
+ Each generator emits nothing when its section is absent, so a small contract
492
+ produces a small output. Select a subset with `-g`.
493
+
494
+ ### The `Any` boundary
495
+
496
+ Data Connect stores JSON and embedded objects as the `Any` scalar, erasing the
497
+ logical type. The GraphQL generator keeps the logical type in a comment
498
+ (`metadata: Any # logical: ProductMetadata`), and the adapter emits typed
499
+ converters that call `fromAny`/`toAny` from `firebase-contract/runtime`:
500
+
501
+ ```
502
+ ProductMetadata → Data Connect: Any → generated adapter → ProductMetadata
503
+ ```
504
+
505
+ ---
506
+
507
+ # Programmatic API
508
+
509
+ ```ts
510
+ import { compile, generate, generateAll, createDefaultRegistry } from 'firebase-contract'
511
+
512
+ const { ir, diagnostics } = compile('contract.yml')
513
+
514
+ // Single target:
515
+ const result = generate('contract.yml', {
516
+ outDir: 'generated',
517
+ generators: ['typescript', 'zod'], // omit for all
518
+ write: true,
519
+ })
520
+
521
+ // Every `generate:` target declared across the import graph:
522
+ const all = generateAll('contract.yml', { write: true })
523
+ ```
524
+
525
+ # Architecture
526
+
527
+ ```
528
+ YAML → Parser → Import Resolver → Normalized IR → Semantic Validation → Generators
529
+ ```
530
+
531
+ - **Generators never see YAML.** Their only input is the IR (`src/lib/_internal/ir`).
532
+ - **Generators are independent** of each other and of shared mutable state.
533
+ - **The CLI is thin** and delegates to the compiler; it never calls a generator
534
+ directly.
535
+ - Dependency direction: `cli → compiler → {parser, resolver, ir, validation, generators}`.
536
+
537
+ All implementation lives under `src/lib/_internal` (imported via the `~internal`
538
+ alias); the outer layers are thin: `src/lib/cli/**` (bin), `src/lib/modules/**`
539
+ (public subpath exports, e.g. `firebase-contract/runtime`), and `src/index.ts`
540
+ (root barrel).
541
+
542
+ ### Adding a generator
543
+
544
+ Implement the `Generator` interface and register it — no existing code changes:
545
+
546
+ ```ts
547
+ import { Generator, createDefaultRegistry } from 'firebase-contract'
548
+
549
+ const openApi: Generator = {
550
+ name: 'openapi',
551
+ generate: ir => [{ path: 'openapi.json', content: toOpenApi(ir) }],
552
+ }
553
+
554
+ const registry = createDefaultRegistry().register(openApi)
555
+ ```
556
+
557
+ ### Adding a validation rule
558
+
559
+ Rules are independent `(ir) => Diagnostic[]` functions; pass a custom set to
560
+ `validateIr(ir, rules)`.
@@ -0,0 +1,15 @@
1
+ export * from './lib/_internal/compiler/index.js';
2
+ export * from './lib/_internal/ir/ir.js';
3
+ export * from './lib/_internal/generators/index.js';
4
+ export type { Diagnostic, DiagnosticSeverity } from './lib/_internal/diagnostics.js';
5
+ export { ContractError, hasErrors } from './lib/_internal/diagnostics.js';
6
+ export type { RawContract, RawModel, RawField, RawEnum } from './lib/_internal/parser/raw-document.js';
7
+ export { parseContract } from './lib/_internal/parser/parse.js';
8
+ export type { ResolveResult } from './lib/_internal/resolver/import-resolver.js';
9
+ export { resolveImports } from './lib/_internal/resolver/import-resolver.js';
10
+ export type { ModuleLoader } from './lib/_internal/resolver/module-loader.js';
11
+ export { createFsLoader, createMemoryLoader } from './lib/_internal/resolver/module-loader.js';
12
+ export { validateIr } from './lib/_internal/validation/validate.js';
13
+ export type { ValidationRule } from './lib/_internal/validation/rules.js';
14
+ export * as runtime from './lib/_internal/runtime/index.js';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,6BAA6B,CAAA;AAC3C,cAAc,oBAAoB,CAAA;AAClC,cAAc,+BAA+B,CAAA;AAE7C,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAC9E,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAEnE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAA;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD,YAAY,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAA;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAA;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AACvE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAA;AAExF,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAC7D,YAAY,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAEnE,OAAO,KAAK,OAAO,MAAM,4BAA4B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // Public, programmatic API for firebase-contract.
2
+ // The CLI (bin: firebase-contract / fbc) is a thin wrapper over `compile`/`generate`.
3
+ // All implementation lives under lib/_internal; this barrel re-exports it.
4
+ export * from './lib/_internal/compiler/index.js';
5
+ export * from './lib/_internal/ir/ir.js';
6
+ export * from './lib/_internal/generators/index.js';
7
+ export { ContractError, hasErrors } from './lib/_internal/diagnostics.js';
8
+ export { parseContract } from './lib/_internal/parser/parse.js';
9
+ export { resolveImports } from './lib/_internal/resolver/import-resolver.js';
10
+ export { createFsLoader, createMemoryLoader } from './lib/_internal/resolver/module-loader.js';
11
+ export { validateIr } from './lib/_internal/validation/validate.js';
12
+ export * as runtime from './lib/_internal/runtime/index.js';
@@ -0,0 +1,71 @@
1
+ import { Diagnostic } from '../diagnostics.js';
2
+ import { Ir } from '../ir/ir.js';
3
+ import { RawContract } from '../parser/raw-document.js';
4
+ import { ModuleLoader } from '../resolver/module-loader.js';
5
+ import { GeneratedFile, GeneratorContext } from '../generators/generator.js';
6
+ import { GeneratorRegistry } from '../generators/registry.js';
7
+ export interface CompileOptions {
8
+ /** Override module loading (defaults to the file system). */
9
+ loader?: ModuleLoader;
10
+ }
11
+ export interface CompileResult {
12
+ ir: Ir;
13
+ documents: RawContract[];
14
+ diagnostics: Diagnostic[];
15
+ }
16
+ /**
17
+ * Run the front end of the pipeline: resolve imports, build the IR, and run
18
+ * semantic validation. Generators are intentionally not involved here — the CLI
19
+ * and {@link generate} depend on this, keeping analysis and code emission
20
+ * separable and independently testable.
21
+ */
22
+ export declare const compile: (entryPath: string, options?: CompileOptions) => CompileResult;
23
+ export interface GenerateOptions extends CompileOptions {
24
+ outDir: string;
25
+ /** Generator names to run; defaults to every generator in the registry. */
26
+ generators?: string[];
27
+ /** Registry to resolve generators from; defaults to the built-ins. */
28
+ registry?: GeneratorRegistry;
29
+ /** When true, write the generated files to disk. */
30
+ write?: boolean;
31
+ context?: GeneratorContext;
32
+ }
33
+ export interface GenerateTargetResult {
34
+ /** The contract file that declared this target. */
35
+ source: string;
36
+ outDir: string;
37
+ generators: string[];
38
+ files: GeneratedFile[];
39
+ }
40
+ export interface GenerateAllResult {
41
+ targets: GenerateTargetResult[];
42
+ diagnostics: Diagnostic[];
43
+ ok: boolean;
44
+ }
45
+ /**
46
+ * Run every `generate:` target declared across the entry contract and its
47
+ * (transitive) imports — the one-command flow: each contract file states where
48
+ * its own outputs go, and `fbc generate <root>` materializes all of them.
49
+ * Output dirs resolve relative to the declaring yml file.
50
+ */
51
+ export declare const generateAll: (entryPath: string, options?: {
52
+ registry?: GeneratorRegistry;
53
+ write?: boolean;
54
+ context?: GeneratorContext;
55
+ loader?: ModuleLoader;
56
+ }) => GenerateAllResult;
57
+ export interface GenerateResult {
58
+ ir: Ir;
59
+ diagnostics: Diagnostic[];
60
+ /** Output files with paths resolved under `outDir`. Empty when there are errors. */
61
+ files: GeneratedFile[];
62
+ /** True when no error-level diagnostics were produced. */
63
+ ok: boolean;
64
+ }
65
+ /**
66
+ * Compile then emit code. Generation is skipped when compilation produced any
67
+ * error, so generators never see an invalid IR. The CLI is a thin wrapper over
68
+ * this function.
69
+ */
70
+ export declare const generate: (entryPath: string, options: GenerateOptions) => GenerateResult;
71
+ //# sourceMappingURL=compiler.d.ts.map
@@ -0,0 +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,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAEvD,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAG3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAE5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAE7D,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;CAC3B;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;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,UAAS;IAAE,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAAC,MAAM,CAAC,EAAE,YAAY,CAAA;CAAO,KACjH,iBA8BF,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,cAuCtE,CAAA"}