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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -200,6 +200,14 @@ typo like `optionnal:` must not quietly turn into a required field. There is
200
200
  no `version:` field: nothing consumes one, so accepting it would only fake a
201
201
  compatibility mechanism that does not exist.
202
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
+
203
211
  ```yaml
204
212
  imports:
205
213
  - ./apps/shop/data-connect/schema.yml
@@ -1 +1 @@
1
- {"version":3,"file":"build-ir.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/ir/build-ir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAS,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAGL,WAAW,EAIZ,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAGL,EAAE,EAgBH,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAqLD;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,WAAW,EAAE,KAAG,aAkQlD,CAAA"}
1
+ {"version":3,"file":"build-ir.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/ir/build-ir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAGL,WAAW,EAIZ,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAGL,EAAE,EAgBH,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAyPD;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,WAAW,EAAE,KAAG,aAkQlD,CAAA"}
@@ -1,4 +1,4 @@
1
- import { error } from '../diagnostics.js';
1
+ import { error, warning } from '../diagnostics.js';
2
2
  import { isScalarType, } from './ir.js';
3
3
  const resolveTypeRef = (typeName, enumNames, modelNames) => {
4
4
  if (isScalarType(typeName)) {
@@ -58,6 +58,42 @@ const OPERATION_TYPES = new Set(['query', 'mutation']);
58
58
  const MUTATION_ACTIONS = new Set(['insert', 'update', 'upsert', 'delete']);
59
59
  const AUTH_LEVELS = new Set(['NO_ACCESS', 'PUBLIC', 'USER']);
60
60
  const API_KINDS = new Set(['callable', 'https', 'task', 'pubsub']);
61
+ const ORDER_DIRS = new Set(['ASC', 'DESC']);
62
+ const SINGLE_KINDS = new Set(['key', 'id']);
63
+ /** Per-key valid values for an operation's `style:` block. */
64
+ const STYLE_VALUES = {
65
+ signature: new Set(['inline', 'multi']),
66
+ args: new Set(['inline', 'multi']),
67
+ data: new Set(['inline', 'multi', 'compact']),
68
+ orderBy: new Set(['bare', 'array']),
69
+ auth: new Set(['inline', 'newline']),
70
+ key: new Set(['inline', 'multi']),
71
+ and: new Set(['inline', 'multi']),
72
+ where: new Set(['inline', 'multi']),
73
+ };
74
+ /**
75
+ * Data Connect scalar/list filter operators. A `where.op` outside this set is
76
+ * warned (not errored): the value is emitted verbatim into GraphQL, so a typo
77
+ * ships broken schema, but the list may lag DC additions — a warning surfaces
78
+ * typos without rejecting a valid-but-unlisted operator.
79
+ */
80
+ const WHERE_OPS = new Set([
81
+ 'eq', 'ne', 'gt', 'ge', 'lt', 'le', 'in', 'nin', 'isNull',
82
+ 'contains', 'startsWith', 'endsWith',
83
+ 'includes', 'excludes', 'includesAll', 'includesAny', 'excludesAll', 'excludesAny',
84
+ ]);
85
+ /**
86
+ * The contract is the single source of truth, so an out-of-vocabulary *value* is
87
+ * as much a defect as an unknown key: it is silently coerced to a default or
88
+ * dropped, and the generated output no longer reflects what was written. These
89
+ * checks turn that silent coercion into a diagnostic at the coercion site (the
90
+ * raw value is still in hand here; it is lost once the IR is built).
91
+ */
92
+ const checkValue = (diagnostics, file, path, key, value, valid) => {
93
+ if (value !== undefined && !valid.has(value)) {
94
+ diagnostics.push(error('INVALID_VALUE', `Invalid ${key} "${value}" — expected one of ${[...valid].join(' | ')}`, { file, path }));
95
+ }
96
+ };
61
97
  const buildSelect = (entries) => entries.map(entry => ({
62
98
  field: entry.field,
63
99
  ...(entry.select !== undefined ? { select: buildSelect(entry.select) } : {}),
@@ -65,7 +101,27 @@ const buildSelect = (entries) => entries.map(entry => ({
65
101
  ...(entry.alias !== undefined ? { alias: entry.alias } : {}),
66
102
  ...(entry.args !== undefined ? { args: entry.args } : {}),
67
103
  }));
68
- const buildOperation = (name, raw, sourceFile) => {
104
+ const buildOperation = (name, raw, sourceFile, diagnostics) => {
105
+ const path = `operations.${name}`;
106
+ checkValue(diagnostics, sourceFile, `${path}.type`, 'type', raw.type, OPERATION_TYPES);
107
+ checkValue(diagnostics, sourceFile, `${path}.auth`, 'auth', raw.auth, AUTH_LEVELS);
108
+ checkValue(diagnostics, sourceFile, `${path}.action`, 'action', raw.action, MUTATION_ACTIONS);
109
+ checkValue(diagnostics, sourceFile, `${path}.single`, 'single', raw.single, SINGLE_KINDS);
110
+ checkValue(diagnostics, sourceFile, `${path}.keyArg`, 'keyArg', raw.keyArg, SINGLE_KINDS);
111
+ for (const o of raw.orderBy ?? [])
112
+ checkValue(diagnostics, sourceFile, `${path}.orderBy`, 'orderBy dir', o.dir, ORDER_DIRS);
113
+ for (const [k, v] of Object.entries(raw.style ?? {})) {
114
+ if (STYLE_VALUES[k])
115
+ checkValue(diagnostics, sourceFile, `${path}.style.${k}`, `style.${k}`, v, STYLE_VALUES[k]);
116
+ }
117
+ for (const w of raw.where ?? []) {
118
+ if (w.op !== undefined && !WHERE_OPS.has(w.op)) {
119
+ diagnostics.push(warning('UNKNOWN_WHERE_OP', `Unknown where operator "${w.op}" on ${path} — emitted verbatim into GraphQL; verify it is a valid Data Connect operator`, {
120
+ file: sourceFile,
121
+ path: `${path}.where`,
122
+ }));
123
+ }
124
+ }
69
125
  const op = {
70
126
  name,
71
127
  operationType: (OPERATION_TYPES.has(raw.type) ? raw.type : 'query'),
@@ -129,7 +185,8 @@ const buildApiPayload = (raw, enumNames, modelNames) => {
129
185
  payload.model = raw.model;
130
186
  return payload;
131
187
  };
132
- const buildApi = (name, raw, sourceFile, enumNames, modelNames) => {
188
+ const buildApi = (name, raw, sourceFile, enumNames, modelNames, diagnostics) => {
189
+ checkValue(diagnostics, sourceFile, `apis.${name}.kind`, 'kind', raw.kind, API_KINDS);
133
190
  const api = {
134
191
  name,
135
192
  kind: (API_KINDS.has(raw.kind) ? raw.kind : 'callable'),
@@ -316,11 +373,11 @@ export const buildIr = (documents) => {
316
373
  }
317
374
  const operations = [];
318
375
  for (const { name, doc } of operationEntries) {
319
- operations.push(buildOperation(name, doc.operations[name], doc.filePath));
376
+ operations.push(buildOperation(name, doc.operations[name], doc.filePath, diagnostics));
320
377
  }
321
378
  const apis = [];
322
379
  for (const [name, doc] of apiSources) {
323
- apis.push(buildApi(name, doc.apis[name], doc.filePath, enumNames, modelNames));
380
+ apis.push(buildApi(name, doc.apis[name], doc.filePath, enumNames, modelNames, diagnostics));
324
381
  }
325
382
  const unionSources = new Map();
326
383
  for (const doc of documents) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-contract",
3
- "version": "0.1.0-rc.13",
3
+ "version": "0.1.0-rc.14",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "firebase-contract": "./dist/lib/cli/index.js",