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,
|
|
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) {
|