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
@@ -0,0 +1,682 @@
1
+ import { parse as parseYamlText } from 'yaml';
2
+ import { ContractError, error } from '../diagnostics.js';
3
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ const fail = (message, filePath, path) => {
5
+ throw new ContractError(message, [error('PARSE_ERROR', message, { file: filePath, path })]);
6
+ };
7
+ const normalizeField = (raw, filePath, path) => {
8
+ // Shorthand: `title: string` — the value is the type name.
9
+ if (typeof raw === 'string') {
10
+ return { type: raw };
11
+ }
12
+ if (!isObject(raw)) {
13
+ return fail(`Field "${path}" must be a type name or an object`, filePath, path);
14
+ }
15
+ if (typeof raw.type !== 'string') {
16
+ return fail(`Field "${path}" is missing a string "type"`, filePath, path);
17
+ }
18
+ const field = { type: raw.type };
19
+ if (raw.optional !== undefined)
20
+ field.optional = Boolean(raw.optional);
21
+ if (raw.nullable !== undefined)
22
+ field.nullable = Boolean(raw.nullable);
23
+ if (raw.jsdoc !== undefined)
24
+ field.jsdoc = Boolean(raw.jsdoc);
25
+ if (raw.list !== undefined)
26
+ field.list = Boolean(raw.list);
27
+ if (raw.id !== undefined)
28
+ field.id = Boolean(raw.id);
29
+ if (raw.unique !== undefined)
30
+ field.unique = Boolean(raw.unique);
31
+ if (raw.relation !== undefined)
32
+ field.relation = Boolean(raw.relation);
33
+ if (raw.default !== undefined)
34
+ field.default = raw.default;
35
+ if (raw.description !== undefined)
36
+ field.description = String(raw.description);
37
+ if (raw.min !== undefined)
38
+ field.min = Number(raw.min);
39
+ if (raw.max !== undefined)
40
+ field.max = Number(raw.max);
41
+ if (raw.minLength !== undefined)
42
+ field.minLength = Number(raw.minLength);
43
+ if (raw.maxLength !== undefined)
44
+ field.maxLength = Number(raw.maxLength);
45
+ if (raw.pattern !== undefined)
46
+ field.pattern = String(raw.pattern);
47
+ if (raw.nonempty !== undefined)
48
+ field.nonempty = Boolean(raw.nonempty);
49
+ if (raw.email !== undefined)
50
+ field.email = Boolean(raw.email);
51
+ if (raw.url !== undefined)
52
+ field.url = Boolean(raw.url);
53
+ if (raw.col !== undefined)
54
+ field.col = String(raw.col);
55
+ if (raw.literal !== undefined)
56
+ field.literal = String(raw.literal);
57
+ return field;
58
+ };
59
+ const normalizeModel = (raw, filePath, name) => {
60
+ if (!isObject(raw)) {
61
+ return fail(`Model "${name}" must be an object`, filePath, `models.${name}`);
62
+ }
63
+ const fieldsRaw = raw.fields;
64
+ if (!isObject(fieldsRaw)) {
65
+ return fail(`Model "${name}" is missing a "fields" object`, filePath, `models.${name}.fields`);
66
+ }
67
+ const fields = {};
68
+ for (const [fieldName, fieldRaw] of Object.entries(fieldsRaw)) {
69
+ fields[fieldName] = normalizeField(fieldRaw, filePath, `models.${name}.fields.${fieldName}`);
70
+ }
71
+ const model = { fields };
72
+ if (raw.description !== undefined)
73
+ model.description = String(raw.description);
74
+ if (raw.key !== undefined)
75
+ model.key = asStringArray(raw.key, filePath, `models.${name}.key`);
76
+ if (raw.table !== undefined)
77
+ model.table = String(raw.table);
78
+ if (raw.gqlName !== undefined)
79
+ model.gqlName = String(raw.gqlName);
80
+ if (raw.directives !== undefined)
81
+ model.directives = String(raw.directives);
82
+ if (raw.footer !== undefined)
83
+ model.footer = String(raw.footer);
84
+ if (raw.fsName !== undefined)
85
+ model.fsName = String(raw.fsName);
86
+ if (raw.indexes !== undefined) {
87
+ if (!Array.isArray(raw.indexes)) {
88
+ return fail(`Model "${name}" indexes must be an array`, filePath, `models.${name}.indexes`);
89
+ }
90
+ model.indexes = raw.indexes.map((entry, i) => {
91
+ if (!isObject(entry)) {
92
+ return fail(`Model "${name}" index #${i} must be an object`, filePath, `models.${name}.indexes`);
93
+ }
94
+ const index = {
95
+ fields: asStringArray(entry.fields, filePath, `models.${name}.indexes[${i}].fields`),
96
+ };
97
+ if (entry.name !== undefined)
98
+ index.name = String(entry.name);
99
+ if (entry.unique !== undefined)
100
+ index.unique = Boolean(entry.unique);
101
+ if (entry.expand !== undefined)
102
+ index.expand = Boolean(entry.expand);
103
+ return index;
104
+ });
105
+ }
106
+ if (raw.sql !== undefined) {
107
+ if (!isObject(raw.sql)) {
108
+ return fail(`Model "${name}" sql must be an object`, filePath, `models.${name}.sql`);
109
+ }
110
+ const sqlRaw = raw.sql;
111
+ const sql = {};
112
+ if (sqlRaw.checks !== undefined)
113
+ sql.checks = asStringArray(sqlRaw.checks, filePath, `models.${name}.sql.checks`);
114
+ if (sqlRaw.foreignKeys !== undefined) {
115
+ if (!Array.isArray(sqlRaw.foreignKeys)) {
116
+ return fail(`Model "${name}" sql.foreignKeys must be an array`, filePath, `models.${name}.sql.foreignKeys`);
117
+ }
118
+ sql.foreignKeys = sqlRaw.foreignKeys.map((entry, i) => {
119
+ if (!isObject(entry) || typeof entry.references !== 'string') {
120
+ return fail(`Model "${name}" sql.foreignKeys[${i}] needs columns + references`, filePath, `models.${name}.sql.foreignKeys`);
121
+ }
122
+ const fk = {
123
+ columns: asStringArray(entry.columns, filePath, `models.${name}.sql.foreignKeys[${i}].columns`),
124
+ references: entry.references,
125
+ };
126
+ if (entry.name !== undefined)
127
+ fk.name = String(entry.name);
128
+ return fk;
129
+ });
130
+ }
131
+ if (sqlRaw.indexes !== undefined) {
132
+ if (!Array.isArray(sqlRaw.indexes)) {
133
+ return fail(`Model "${name}" sql.indexes must be an array`, filePath, `models.${name}.sql.indexes`);
134
+ }
135
+ sql.indexes = sqlRaw.indexes.map((entry, i) => {
136
+ if (!isObject(entry)) {
137
+ return fail(`Model "${name}" sql.indexes[${i}] must be an object`, filePath, `models.${name}.sql.indexes`);
138
+ }
139
+ const idx = {
140
+ columns: asStringArray(entry.columns, filePath, `models.${name}.sql.indexes[${i}].columns`),
141
+ };
142
+ if (entry.name !== undefined)
143
+ idx.name = String(entry.name);
144
+ if (entry.unique !== undefined)
145
+ idx.unique = Boolean(entry.unique);
146
+ return idx;
147
+ });
148
+ }
149
+ model.sql = sql;
150
+ }
151
+ return model;
152
+ };
153
+ const normalizeEnum = (raw, filePath, name) => {
154
+ if (!isObject(raw)) {
155
+ return fail(`Enum "${name}" must be an object`, filePath, `enums.${name}`);
156
+ }
157
+ if (!Array.isArray(raw.values)) {
158
+ return fail(`Enum "${name}" is missing a "values" array`, filePath, `enums.${name}.values`);
159
+ }
160
+ const values = [];
161
+ const valueComments = {};
162
+ const valueKeys = {};
163
+ for (const entry of raw.values) {
164
+ if (isObject(entry) && typeof entry.value === 'string') {
165
+ values.push(entry.value);
166
+ if (entry.description !== undefined)
167
+ valueComments[entry.value] = String(entry.description);
168
+ if (entry.key !== undefined)
169
+ valueKeys[entry.value] = String(entry.key);
170
+ }
171
+ else {
172
+ values.push(String(entry));
173
+ }
174
+ }
175
+ const result = { values };
176
+ if (Object.keys(valueComments).length > 0)
177
+ result.valueComments = valueComments;
178
+ if (Object.keys(valueKeys).length > 0)
179
+ result.valueKeys = valueKeys;
180
+ if (raw.gqlName !== undefined)
181
+ result.gqlName = String(raw.gqlName);
182
+ if (raw.fsName !== undefined)
183
+ result.fsName = String(raw.fsName);
184
+ if (raw.fsDescription !== undefined)
185
+ result.fsDescription = String(raw.fsDescription);
186
+ if (raw.description !== undefined)
187
+ result.description = String(raw.description);
188
+ return result;
189
+ };
190
+ const asStringArray = (value, filePath, path) => {
191
+ if (value === undefined)
192
+ return [];
193
+ if (!Array.isArray(value)) {
194
+ return fail(`"${path}" must be an array of strings`, filePath, path);
195
+ }
196
+ return value.map(entry => String(entry));
197
+ };
198
+ const normalizeOperation = (raw, filePath, name) => {
199
+ if (!isObject(raw)) {
200
+ return fail(`Operation "${name}" must be an object`, filePath, `operations.${name}`);
201
+ }
202
+ if (typeof raw.type !== 'string') {
203
+ return fail(`Operation "${name}" is missing a string "type" (query|mutation)`, filePath, `operations.${name}.type`);
204
+ }
205
+ if (typeof raw.model !== 'string') {
206
+ return fail(`Operation "${name}" is missing a string "model"`, filePath, `operations.${name}.model`);
207
+ }
208
+ const op = { type: raw.type, model: raw.model };
209
+ if (raw.action !== undefined)
210
+ op.action = String(raw.action);
211
+ if (raw.auth !== undefined)
212
+ op.auth = String(raw.auth);
213
+ if (raw.authReason !== undefined)
214
+ op.authReason = String(raw.authReason);
215
+ if (raw.inputs !== undefined) {
216
+ if (!Array.isArray(raw.inputs)) {
217
+ return fail(`Operation "${name}" inputs must be an array`, filePath, `operations.${name}.inputs`);
218
+ }
219
+ op.inputs = raw.inputs.map(entry => {
220
+ if (typeof entry === 'string')
221
+ return { field: entry };
222
+ if (isObject(entry) && typeof entry.field === 'string') {
223
+ return {
224
+ field: entry.field,
225
+ ...(entry.required !== undefined ? { required: Boolean(entry.required) } : {}),
226
+ ...(entry.description !== undefined ? { description: String(entry.description) } : {}),
227
+ ...(entry.var !== undefined ? { var: String(entry.var) } : {}),
228
+ ...(entry.flat !== undefined ? { flat: Boolean(entry.flat) } : {}),
229
+ ...(entry.literal !== undefined ? { literal: String(entry.literal) } : {}),
230
+ ...(entry.quote !== undefined ? { quote: Boolean(entry.quote) } : {}),
231
+ ...(entry.inc !== undefined ? { inc: Boolean(entry.inc) } : {}),
232
+ ...(entry.asKey !== undefined ? { asKey: Boolean(entry.asKey) } : {}),
233
+ };
234
+ }
235
+ return fail(`Operation "${name}" inputs entry must be a field or { field, ... }`, filePath, `operations.${name}.inputs`);
236
+ });
237
+ }
238
+ if (raw.select !== undefined) {
239
+ const normalizeSelect = (entries, path) => {
240
+ if (!Array.isArray(entries)) {
241
+ return fail(`"${path}" must be an array`, filePath, path);
242
+ }
243
+ return entries.map(entry => {
244
+ if (typeof entry === 'string')
245
+ return { field: entry };
246
+ if (isObject(entry) && typeof entry.field === 'string') {
247
+ return {
248
+ field: entry.field,
249
+ ...(entry.select !== undefined ? { select: normalizeSelect(entry.select, `${path}.${entry.field}`) } : {}),
250
+ ...(entry.description !== undefined ? { description: String(entry.description) } : {}),
251
+ ...(entry.alias !== undefined ? { alias: String(entry.alias) } : {}),
252
+ ...(entry.args !== undefined ? { args: String(entry.args) } : {}),
253
+ };
254
+ }
255
+ return fail(`"${path}" entry must be a field or { field, select }`, filePath, path);
256
+ });
257
+ };
258
+ op.select = normalizeSelect(raw.select, `operations.${name}.select`);
259
+ }
260
+ // where: array of "field" (eq) or { field, op }.
261
+ if (raw.where !== undefined) {
262
+ if (!Array.isArray(raw.where)) {
263
+ return fail(`Operation "${name}" where must be an array`, filePath, `operations.${name}.where`);
264
+ }
265
+ op.where = raw.where.map(entry => {
266
+ if (typeof entry === 'string')
267
+ return { field: entry, op: 'eq' };
268
+ if (isObject(entry) && typeof entry.field === 'string') {
269
+ return {
270
+ field: entry.field,
271
+ op: entry.op !== undefined ? String(entry.op) : 'eq',
272
+ ...(entry.required !== undefined ? { required: Boolean(entry.required) } : {}),
273
+ ...(entry.description !== undefined ? { description: String(entry.description) } : {}),
274
+ ...(entry.var !== undefined ? { var: String(entry.var) } : {}),
275
+ ...(entry.literal !== undefined ? { literal: String(entry.literal) } : {}),
276
+ ...(entry.flat !== undefined ? { flat: Boolean(entry.flat) } : {}),
277
+ };
278
+ }
279
+ return fail(`Operation "${name}" where entry must be a field or { field, op }`, filePath, `operations.${name}.where`);
280
+ });
281
+ }
282
+ // orderBy: array of { field, dir } (dir defaults to ASC).
283
+ if (raw.orderBy !== undefined) {
284
+ if (!Array.isArray(raw.orderBy)) {
285
+ return fail(`Operation "${name}" orderBy must be an array`, filePath, `operations.${name}.orderBy`);
286
+ }
287
+ op.orderBy = raw.orderBy.map(entry => {
288
+ if (!isObject(entry) || typeof entry.field !== 'string') {
289
+ return fail(`Operation "${name}" orderBy entry needs a field`, filePath, `operations.${name}.orderBy`);
290
+ }
291
+ return { field: entry.field, dir: entry.dir !== undefined ? String(entry.dir).toUpperCase() : 'ASC' };
292
+ });
293
+ }
294
+ if (raw.limit !== undefined) {
295
+ if (isObject(raw.limit) && typeof raw.limit.var === 'string') {
296
+ op.limit = {
297
+ var: raw.limit.var,
298
+ ...(raw.limit.default !== undefined ? { default: Number(raw.limit.default) } : {}),
299
+ ...(raw.limit.required !== undefined ? { required: Boolean(raw.limit.required) } : {}),
300
+ };
301
+ }
302
+ else {
303
+ op.limit = Number(raw.limit);
304
+ }
305
+ }
306
+ if (raw.single !== undefined)
307
+ op.single = String(raw.single);
308
+ if (raw.keyArg !== undefined)
309
+ op.keyArg = String(raw.keyArg);
310
+ if (raw.entityDir !== undefined)
311
+ op.entityDir = String(raw.entityDir);
312
+ if (raw.whereAnd !== undefined)
313
+ op.whereAnd = Boolean(raw.whereAnd);
314
+ if (raw.gqlName !== undefined)
315
+ op.gqlName = String(raw.gqlName);
316
+ if (raw.footer !== undefined)
317
+ op.footer = String(raw.footer);
318
+ if (raw.raw !== undefined)
319
+ op.raw = String(raw.raw);
320
+ if (raw.style !== undefined) {
321
+ if (!isObject(raw.style)) {
322
+ return fail(`Operation "${name}" style must be a mapping`, filePath, `operations.${name}.style`);
323
+ }
324
+ const style = {};
325
+ for (const k of ['signature', 'args', 'data', 'orderBy', 'auth', 'key', 'and', 'where']) {
326
+ if (raw.style[k] !== undefined)
327
+ style[k] = String(raw.style[k]);
328
+ }
329
+ op.style = style;
330
+ }
331
+ if (raw.keyVars !== undefined && isObject(raw.keyVars)) {
332
+ const keyVars = {};
333
+ for (const [k, v] of Object.entries(raw.keyVars))
334
+ keyVars[k] = String(v);
335
+ op.keyVars = keyVars;
336
+ }
337
+ if (raw.aggregate !== undefined) {
338
+ if (!isObject(raw.aggregate)) {
339
+ return fail(`Operation "${name}" aggregate must be an object`, filePath, `operations.${name}.aggregate`);
340
+ }
341
+ op.aggregate = {
342
+ count: raw.aggregate.count !== undefined ? Boolean(raw.aggregate.count) : false,
343
+ sum: asStringArray(raw.aggregate.sum, filePath, `operations.${name}.aggregate.sum`),
344
+ };
345
+ }
346
+ if (raw.exprs !== undefined) {
347
+ if (!isObject(raw.exprs)) {
348
+ return fail(`Operation "${name}" exprs must be a mapping`, filePath, `operations.${name}.exprs`);
349
+ }
350
+ op.exprs = Object.entries(raw.exprs).map(([field, expr]) => ({ field, expr: String(expr) }));
351
+ }
352
+ if (raw.inc !== undefined) {
353
+ if (!Array.isArray(raw.inc)) {
354
+ return fail(`Operation "${name}" inc must be an array`, filePath, `operations.${name}.inc`);
355
+ }
356
+ op.inc = raw.inc.map(entry => {
357
+ if (typeof entry === 'string')
358
+ return { field: entry };
359
+ if (isObject(entry) && typeof entry.field === 'string') {
360
+ return { field: entry.field, ...(entry.var !== undefined ? { var: String(entry.var) } : {}) };
361
+ }
362
+ return fail(`Operation "${name}" inc entry must be a field or { field, var }`, filePath, `operations.${name}.inc`);
363
+ });
364
+ }
365
+ // `connector: app` (single) or `connectors: [app, api]` (multiple).
366
+ if (raw.connector !== undefined) {
367
+ op.connectors = [String(raw.connector)];
368
+ }
369
+ else if (raw.connectors !== undefined) {
370
+ op.connectors = asStringArray(raw.connectors, filePath, `operations.${name}.connectors`);
371
+ }
372
+ if (raw.description !== undefined)
373
+ op.description = String(raw.description);
374
+ return op;
375
+ };
376
+ const normalizeApiPayload = (raw, filePath, path) => {
377
+ if (raw === undefined)
378
+ return {};
379
+ if (!isObject(raw)) {
380
+ return fail(`"${path}" must be an object`, filePath, path);
381
+ }
382
+ const payload = {};
383
+ if (raw.model !== undefined)
384
+ payload.model = String(raw.model);
385
+ if (raw.void !== undefined)
386
+ payload.void = Boolean(raw.void);
387
+ if (raw.fields !== undefined) {
388
+ if (!isObject(raw.fields)) {
389
+ return fail(`"${path}.fields" must be a mapping`, filePath, `${path}.fields`);
390
+ }
391
+ const fields = {};
392
+ for (const [fieldName, fieldRaw] of Object.entries(raw.fields)) {
393
+ fields[fieldName] = normalizeField(fieldRaw, filePath, `${path}.fields.${fieldName}`);
394
+ }
395
+ payload.fields = fields;
396
+ }
397
+ return payload;
398
+ };
399
+ const normalizeApi = (raw, filePath, name) => {
400
+ if (!isObject(raw)) {
401
+ return fail(`Api "${name}" must be an object`, filePath, `apis.${name}`);
402
+ }
403
+ if (typeof raw.kind !== 'string') {
404
+ return fail(`Api "${name}" is missing a string "kind" (callable|https|task|pubsub)`, filePath, `apis.${name}.kind`);
405
+ }
406
+ const api = { kind: raw.kind };
407
+ if (raw.method !== undefined)
408
+ api.method = String(raw.method);
409
+ if (raw.description !== undefined)
410
+ api.description = String(raw.description);
411
+ if (raw.envelope !== undefined)
412
+ api.envelope = String(raw.envelope);
413
+ if (raw.maxAttempts !== undefined)
414
+ api.maxAttempts = Number(raw.maxAttempts);
415
+ if (raw.timeoutSeconds !== undefined)
416
+ api.timeoutSeconds = Number(raw.timeoutSeconds);
417
+ if (raw.topic !== undefined)
418
+ api.topic = String(raw.topic);
419
+ api.request = normalizeApiPayload(raw.request, filePath, `apis.${name}.request`);
420
+ api.response = normalizeApiPayload(raw.response, filePath, `apis.${name}.response`);
421
+ return api;
422
+ };
423
+ const normalizeEnvelope = (raw, filePath, name) => {
424
+ if (!isObject(raw)) {
425
+ return fail(`Envelope "${name}" must be an object`, filePath, `envelopes.${name}`);
426
+ }
427
+ if (!isObject(raw.fields)) {
428
+ return fail(`Envelope "${name}" is missing a "fields" object`, filePath, `envelopes.${name}.fields`);
429
+ }
430
+ const fields = {};
431
+ for (const [fieldName, fieldRaw] of Object.entries(raw.fields)) {
432
+ fields[fieldName] = normalizeField(fieldRaw, filePath, `envelopes.${name}.fields.${fieldName}`);
433
+ }
434
+ const envelope = { fields };
435
+ if (raw.description !== undefined)
436
+ envelope.description = String(raw.description);
437
+ return envelope;
438
+ };
439
+ const normalizeFirestoreDoc = (raw, filePath, name) => {
440
+ if (!isObject(raw)) {
441
+ return fail(`Firestore doc "${name}" must be an object`, filePath, `firestore.${name}`);
442
+ }
443
+ const doc = {};
444
+ if (raw.from !== undefined)
445
+ doc.from = String(raw.from);
446
+ if (raw.collection !== undefined)
447
+ doc.collection = String(raw.collection);
448
+ if (raw.description !== undefined)
449
+ doc.description = String(raw.description);
450
+ if (raw.meta !== undefined)
451
+ doc.meta = Boolean(raw.meta);
452
+ if (raw.helpers !== undefined)
453
+ doc.helpers = String(raw.helpers);
454
+ if (raw.include !== undefined)
455
+ doc.include = asStringArray(raw.include, filePath, `firestore.${name}.include`);
456
+ doc.pick = asStringArray(raw.pick, filePath, `firestore.${name}.pick`);
457
+ doc.omit = asStringArray(raw.omit, filePath, `firestore.${name}.omit`);
458
+ if (raw.fields !== undefined) {
459
+ if (!isObject(raw.fields)) {
460
+ return fail(`Firestore doc "${name}" fields must be a mapping`, filePath, `firestore.${name}.fields`);
461
+ }
462
+ const fields = {};
463
+ for (const [fieldName, fieldRaw] of Object.entries(raw.fields)) {
464
+ fields[fieldName] = normalizeField(fieldRaw, filePath, `firestore.${name}.fields.${fieldName}`);
465
+ }
466
+ doc.fields = fields;
467
+ }
468
+ return doc;
469
+ };
470
+ const normalizeUnion = (raw, filePath, name) => {
471
+ if (!isObject(raw)) {
472
+ return fail(`Union "${name}" must be an object`, filePath, `unions.${name}`);
473
+ }
474
+ if (typeof raw.discriminant !== 'string') {
475
+ return fail(`Union "${name}" is missing a string "discriminant"`, filePath, `unions.${name}.discriminant`);
476
+ }
477
+ const union = {
478
+ discriminant: raw.discriminant,
479
+ variants: asStringArray(raw.variants, filePath, `unions.${name}.variants`),
480
+ };
481
+ if (raw.description !== undefined)
482
+ union.description = String(raw.description);
483
+ return union;
484
+ };
485
+ const normalizeProject = (raw, filePath) => {
486
+ if (!isObject(raw)) {
487
+ return fail('"project" must be an object', filePath, 'project');
488
+ }
489
+ const project = {};
490
+ if (raw.services !== undefined) {
491
+ if (!Array.isArray(raw.services)) {
492
+ return fail('"project.services" must be an array', filePath, 'project.services');
493
+ }
494
+ project.services = raw.services.map((entry, i) => {
495
+ if (!isObject(entry) || typeof entry.name !== 'string') {
496
+ return fail(`project.services[${i}] needs a name`, filePath, 'project.services');
497
+ }
498
+ const service = { name: entry.name };
499
+ if (entry.database !== undefined)
500
+ service.database = String(entry.database);
501
+ if (entry.location !== undefined)
502
+ service.location = String(entry.location);
503
+ if (entry.connectors !== undefined)
504
+ service.connectors = asStringArray(entry.connectors, filePath, `project.services[${i}].connectors`);
505
+ return service;
506
+ });
507
+ }
508
+ if (raw.codebases !== undefined) {
509
+ if (!isObject(raw.codebases)) {
510
+ return fail('"project.codebases" must be a mapping', filePath, 'project.codebases');
511
+ }
512
+ const codebases = {};
513
+ for (const [codebase, service] of Object.entries(raw.codebases)) {
514
+ codebases[codebase] = String(service);
515
+ }
516
+ project.codebases = codebases;
517
+ }
518
+ if (isObject(raw.idCodec)) {
519
+ const idCodec = {};
520
+ if (raw.idCodec.minLength !== undefined)
521
+ idCodec.minLength = Number(raw.idCodec.minLength);
522
+ if (raw.idCodec.alphabet !== undefined)
523
+ idCodec.alphabet = String(raw.idCodec.alphabet);
524
+ project.idCodec = idCodec;
525
+ }
526
+ return project;
527
+ };
528
+ /**
529
+ * Parse a single YAML contract file into a {@link RawContract}. Shorthand forms
530
+ * are normalized here so downstream code sees one canonical shape. Syntax and
531
+ * structural problems throw a {@link ContractError}; semantic problems are left
532
+ * for validation.
533
+ */
534
+ export const parseContract = (content, filePath) => {
535
+ let doc;
536
+ try {
537
+ doc = parseYamlText(content);
538
+ }
539
+ catch (cause) {
540
+ const message = cause instanceof Error ? cause.message : String(cause);
541
+ return fail(`Failed to parse YAML: ${message}`, filePath);
542
+ }
543
+ if (doc === null || doc === undefined) {
544
+ // An empty file is a valid, contribution-free contract.
545
+ return {
546
+ filePath,
547
+ version: 1,
548
+ imports: [],
549
+ defaultConnectors: [],
550
+ enums: {},
551
+ models: {},
552
+ operations: {},
553
+ apis: {},
554
+ firestore: {},
555
+ unions: {},
556
+ envelopes: {},
557
+ };
558
+ }
559
+ if (!isObject(doc)) {
560
+ return fail('Contract root must be a mapping', filePath);
561
+ }
562
+ const imports = [];
563
+ if (doc.imports !== undefined) {
564
+ if (!Array.isArray(doc.imports)) {
565
+ return fail('"imports" must be an array of paths', filePath, 'imports');
566
+ }
567
+ for (const entry of doc.imports) {
568
+ if (typeof entry !== 'string') {
569
+ return fail('Every "imports" entry must be a string', filePath, 'imports');
570
+ }
571
+ imports.push(entry);
572
+ }
573
+ }
574
+ const enums = {};
575
+ if (doc.enums !== undefined) {
576
+ if (!isObject(doc.enums)) {
577
+ return fail('"enums" must be a mapping', filePath, 'enums');
578
+ }
579
+ for (const [name, raw] of Object.entries(doc.enums)) {
580
+ enums[name] = normalizeEnum(raw, filePath, name);
581
+ }
582
+ }
583
+ const models = {};
584
+ if (doc.models !== undefined) {
585
+ if (!isObject(doc.models)) {
586
+ return fail('"models" must be a mapping', filePath, 'models');
587
+ }
588
+ for (const [name, raw] of Object.entries(doc.models)) {
589
+ models[name] = normalizeModel(raw, filePath, name);
590
+ }
591
+ }
592
+ // File-level defaults, e.g. `defaults: { connectors: [app, api] }`.
593
+ let defaultConnectors = [];
594
+ if (doc.defaults !== undefined) {
595
+ if (!isObject(doc.defaults)) {
596
+ return fail('"defaults" must be a mapping', filePath, 'defaults');
597
+ }
598
+ defaultConnectors = asStringArray(doc.defaults.connectors, filePath, 'defaults.connectors');
599
+ }
600
+ const operations = {};
601
+ if (doc.operations !== undefined) {
602
+ if (!isObject(doc.operations)) {
603
+ return fail('"operations" must be a mapping', filePath, 'operations');
604
+ }
605
+ for (const [name, raw] of Object.entries(doc.operations)) {
606
+ const op = normalizeOperation(raw, filePath, name);
607
+ // Inherit the file default when the operation declares no connector.
608
+ if ((!op.connectors || op.connectors.length === 0) && defaultConnectors.length > 0) {
609
+ op.connectors = [...defaultConnectors];
610
+ }
611
+ operations[name] = op;
612
+ }
613
+ }
614
+ const apis = {};
615
+ if (doc.apis !== undefined) {
616
+ if (!isObject(doc.apis)) {
617
+ return fail('"apis" must be a mapping', filePath, 'apis');
618
+ }
619
+ for (const [name, raw] of Object.entries(doc.apis)) {
620
+ apis[name] = normalizeApi(raw, filePath, name);
621
+ }
622
+ }
623
+ const firestore = {};
624
+ if (doc.firestore !== undefined) {
625
+ if (!isObject(doc.firestore)) {
626
+ return fail('"firestore" must be a mapping', filePath, 'firestore');
627
+ }
628
+ for (const [name, raw] of Object.entries(doc.firestore)) {
629
+ firestore[name] = normalizeFirestoreDoc(raw, filePath, name);
630
+ }
631
+ }
632
+ const unions = {};
633
+ if (doc.unions !== undefined) {
634
+ if (!isObject(doc.unions)) {
635
+ return fail('"unions" must be a mapping', filePath, 'unions');
636
+ }
637
+ for (const [name, raw] of Object.entries(doc.unions)) {
638
+ unions[name] = normalizeUnion(raw, filePath, name);
639
+ }
640
+ }
641
+ const envelopes = {};
642
+ if (doc.envelopes !== undefined) {
643
+ if (!isObject(doc.envelopes)) {
644
+ return fail('"envelopes" must be a mapping', filePath, 'envelopes');
645
+ }
646
+ for (const [name, raw] of Object.entries(doc.envelopes)) {
647
+ envelopes[name] = normalizeEnvelope(raw, filePath, name);
648
+ }
649
+ }
650
+ const version = typeof doc.version === 'number' ? doc.version : 1;
651
+ const contract = {
652
+ filePath,
653
+ version,
654
+ imports,
655
+ defaultConnectors,
656
+ enums,
657
+ models,
658
+ operations,
659
+ apis,
660
+ firestore,
661
+ unions,
662
+ envelopes,
663
+ };
664
+ if (doc.project !== undefined) {
665
+ contract.project = normalizeProject(doc.project, filePath);
666
+ }
667
+ if (typeof doc.header === 'string') {
668
+ contract.header = doc.header;
669
+ }
670
+ if (doc.generate !== undefined) {
671
+ if (!Array.isArray(doc.generate)) {
672
+ return fail('"generate" must be an array of { out, generators } entries', filePath, 'generate');
673
+ }
674
+ contract.outputs = doc.generate.map((entry, i) => {
675
+ if (!isObject(entry) || typeof entry.out !== 'string') {
676
+ return fail(`generate[${i}] needs a string "out"`, filePath, `generate[${i}]`);
677
+ }
678
+ return { out: entry.out, generators: asStringArray(entry.generators, filePath, `generate[${i}].generators`) };
679
+ });
680
+ }
681
+ return contract;
682
+ };