supabase-strict-check 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,791 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Checker = void 0;
7
+ exports.flushUnhitAnyPayloads = flushUnhitAnyPayloads;
8
+ const typescript_1 = __importDefault(require("typescript"));
9
+ const ast_1 = require("../utils/ast");
10
+ const files_1 = require("../utils/files");
11
+ const clients_1 = require("../utils/clients");
12
+ const paths_1 = require("../utils/paths");
13
+ const catalog_1 = require("./catalog");
14
+ const program_1 = require("./program");
15
+ const select_1 = require("./select");
16
+ const scope_1 = require("./scope");
17
+ const types_1 = require("./types");
18
+ class Checker {
19
+ errors;
20
+ warnings;
21
+ catalog;
22
+ sf;
23
+ consts;
24
+ tsChecker;
25
+ clientNames;
26
+ subst;
27
+ instantiated;
28
+ pendingAny;
29
+ resolvingPending = false;
30
+ constructor(opts) {
31
+ this.catalog = opts.catalog;
32
+ this.sf = opts.sf;
33
+ this.consts = opts.consts;
34
+ this.tsChecker = opts.tsChecker;
35
+ this.clientNames = opts.clientNames ?? new Set();
36
+ this.subst = opts.subst ?? new Map();
37
+ this.instantiated = opts.instantiated ?? new Set();
38
+ this.pendingAny = opts.pendingAny ?? [];
39
+ this.errors = opts.errors ?? [];
40
+ this.warnings = opts.warnings ?? [];
41
+ }
42
+ evalOpts() {
43
+ return { checker: this.tsChecker, subst: this.subst };
44
+ }
45
+ str(expr) {
46
+ return (0, ast_1.evalString)(expr, this.consts, this.evalOpts());
47
+ }
48
+ fail(node, message) {
49
+ const { line, column } = (0, ast_1.loc)(this.sf, node);
50
+ this.errors.push({ file: (0, files_1.relPath)(this.sf.fileName), line, column, message });
51
+ }
52
+ warn(node, message) {
53
+ const { line, column } = (0, ast_1.loc)(this.sf, node);
54
+ this.warnings.push({ file: (0, files_1.relPath)(this.sf.fileName), line, column, message });
55
+ }
56
+ checkFile() {
57
+ this.resolvingPending = false;
58
+ this.visit(this.sf, new scope_1.Scope());
59
+ }
60
+ followAnyPayloads() {
61
+ this.resolvingPending = true;
62
+ this.visit(this.sf, new scope_1.Scope());
63
+ }
64
+ spawn(sf, subst) {
65
+ return new Checker({
66
+ catalog: this.catalog,
67
+ sf,
68
+ consts: this.consts,
69
+ tsChecker: this.tsChecker,
70
+ clientNames: this.clientNames,
71
+ subst,
72
+ instantiated: this.instantiated,
73
+ pendingAny: this.pendingAny,
74
+ errors: this.errors,
75
+ warnings: this.warnings,
76
+ });
77
+ }
78
+ visit(node, scope) {
79
+ const next = typescript_1.default.isFunctionLike(node) || typescript_1.default.isBlock(node) ? scope.child() : scope;
80
+ if (typescript_1.default.isCallExpression(node)) {
81
+ if (this.resolvingPending) {
82
+ this.applyPendingAtCall(node, next);
83
+ }
84
+ else {
85
+ this.maybeInstantiate(node);
86
+ if ((0, ast_1.isChainTail)(node))
87
+ this.checkTail(node, next);
88
+ }
89
+ }
90
+ if (typescript_1.default.isVariableDeclaration(node) && typescript_1.default.isIdentifier(node.name) && node.initializer) {
91
+ const init = (0, ast_1.unwrapExpr)(node.initializer);
92
+ if (typescript_1.default.isObjectLiteralExpression(init) || typescript_1.default.isArrayLiteralExpression(init)) {
93
+ next.setObject(node.name.text, init);
94
+ }
95
+ }
96
+ typescript_1.default.forEachChild(node, (child) => this.visit(child, next));
97
+ }
98
+ maybeInstantiate(call) {
99
+ if (!this.tsChecker)
100
+ return;
101
+ if (typescript_1.default.isPropertyAccessExpression(call.expression)) {
102
+ const method = call.expression.name.text;
103
+ if (paths_1.QUERY_METHODS.has(method) || paths_1.SKIP_CHAIN_PROPS.has(method))
104
+ return;
105
+ }
106
+ const fn = (0, program_1.calleeFunction)(call, this.tsChecker);
107
+ if (!fn)
108
+ return;
109
+ const file = fn.getSourceFile().fileName;
110
+ if (file.includes("node_modules") || file.endsWith(".d.ts"))
111
+ return;
112
+ const subst = this.substFromCall(fn, call);
113
+ if (!subst)
114
+ return;
115
+ if (sameSubst(subst, this.subst))
116
+ return;
117
+ const key = `${file}:${fn.getStart()}:${stringifySubst(subst)}`;
118
+ if (this.instantiated.has(key))
119
+ return;
120
+ this.instantiated.add(key);
121
+ const body = fn.body;
122
+ if (!body)
123
+ return;
124
+ this.spawn(fn.getSourceFile(), subst).visit(body, new scope_1.Scope());
125
+ }
126
+ applyPendingAtCall(call, scope) {
127
+ if (!this.tsChecker || this.pendingAny.length === 0)
128
+ return;
129
+ const fn = (0, program_1.calleeFunction)(call, this.tsChecker);
130
+ if (!fn)
131
+ return;
132
+ for (const pending of this.pendingAny) {
133
+ if (!sameFunction(pending.fn, fn))
134
+ continue;
135
+ const callId = `${call.getSourceFile().fileName}:${call.getStart()}`;
136
+ if (pending.seenCalls.has(callId))
137
+ continue;
138
+ pending.seenCalls.add(callId);
139
+ const arg = call.arguments[pending.paramIndex];
140
+ if (!arg) {
141
+ pending.hits++;
142
+ this.fail(call, `${pending.kind} on ${(0, types_1.relKey)(pending.relation.schema, pending.relation.name)}: missing payload argument`);
143
+ continue;
144
+ }
145
+ const ident = (0, ast_1.unwrapExpr)(arg);
146
+ if (typescript_1.default.isIdentifier(ident)) {
147
+ const wrapper = (0, ast_1.enclosingParam)(ident);
148
+ if (wrapper && !sameFunction(wrapper.fn, pending.fn)) {
149
+ this.queuePending({
150
+ fn: wrapper.fn,
151
+ paramIndex: wrapper.index,
152
+ relation: pending.relation,
153
+ kind: pending.kind,
154
+ allowed: pending.allowed,
155
+ checkRequired: pending.checkRequired,
156
+ origin: pending.origin,
157
+ hits: 0,
158
+ seenCalls: new Set(),
159
+ });
160
+ pending.hits++;
161
+ continue;
162
+ }
163
+ }
164
+ pending.hits++;
165
+ this.checkPayload(pending.relation, arg, pending.kind, pending.allowed, scope, pending.checkRequired, arg, "callsite");
166
+ }
167
+ }
168
+ queuePending(item) {
169
+ const exists = this.pendingAny.some((p) => sameFunction(p.fn, item.fn) && p.paramIndex === item.paramIndex && p.relation === item.relation && p.kind === item.kind);
170
+ if (!exists)
171
+ this.pendingAny.push(item);
172
+ }
173
+ substFromCall(fn, call) {
174
+ const subst = new Map(this.subst);
175
+ let bound = false;
176
+ fn.parameters.forEach((param, i) => {
177
+ const arg = call.arguments[i];
178
+ if (!arg)
179
+ return;
180
+ if (typescript_1.default.isIdentifier(param.name)) {
181
+ const value = this.str(arg);
182
+ if (value != null) {
183
+ subst.set(param.name.text, value);
184
+ bound = true;
185
+ }
186
+ return;
187
+ }
188
+ if (typescript_1.default.isObjectBindingPattern(param.name)) {
189
+ const obj = (0, ast_1.unwrapExpr)(arg);
190
+ if (!typescript_1.default.isObjectLiteralExpression(obj))
191
+ return;
192
+ for (const el of param.name.elements) {
193
+ if (!typescript_1.default.isBindingElement(el) || !typescript_1.default.isIdentifier(el.name))
194
+ continue;
195
+ const prop = el.propertyName && typescript_1.default.isIdentifier(el.propertyName)
196
+ ? el.propertyName.text
197
+ : el.name.text;
198
+ for (const p of obj.properties) {
199
+ if (!typescript_1.default.isPropertyAssignment(p))
200
+ continue;
201
+ if ((0, ast_1.propName)(p.name) !== prop)
202
+ continue;
203
+ const value = this.str(p.initializer);
204
+ if (value != null) {
205
+ subst.set(el.name.text, value);
206
+ bound = true;
207
+ }
208
+ }
209
+ }
210
+ }
211
+ });
212
+ return bound ? subst : null;
213
+ }
214
+ checkTail(tail, scope) {
215
+ if ((0, ast_1.chainProps)(tail.expression).some((n) => paths_1.SKIP_CHAIN_PROPS.has(n)))
216
+ return;
217
+ const methods = (0, ast_1.collectChain)(tail);
218
+ if (methods.length === 0)
219
+ return;
220
+ const fromCall = methods.find((m) => m.name === "from");
221
+ const rpcCall = methods.find((m) => m.name === "rpc");
222
+ const schemaCall = methods.find((m) => m.name === "schema");
223
+ if (fromCall?.name === "from" && (0, ast_1.isArrayFrom)(fromCall))
224
+ return;
225
+ const root = (0, ast_1.chainRoot)(tail);
226
+ const rootIdent = root && typescript_1.default.isIdentifier(root) ? root.text : null;
227
+ const continued = rootIdent ? scope.getQuery(rootIdent) : undefined;
228
+ if (!fromCall && !rpcCall && !continued)
229
+ return;
230
+ if (fromCall && !continued && !rpcCall && !schemaCall && fromCall.name === "from") {
231
+ const mutating = methods.some((m) => ["select", "insert", "update", "upsert", "delete"].includes(m.name));
232
+ if (!mutating && !(0, clients_1.looksLikeClient)(tail, { checker: this.tsChecker, names: this.clientNames }))
233
+ return;
234
+ }
235
+ let schema = continued?.relation.schema ?? paths_1.DEFAULT_SCHEMA;
236
+ let relation = continued?.relation;
237
+ const embedAliases = new Map(continued?.embeds ?? []);
238
+ if (schemaCall) {
239
+ if (!schemaCall.args[0]) {
240
+ this.fail(schemaCall.node, "dynamic .schema(...)");
241
+ return;
242
+ }
243
+ const schemaName = this.str(schemaCall.args[0]);
244
+ if (!schemaName) {
245
+ if (this.skipGenericParam(schemaCall.args[0]))
246
+ return;
247
+ this.fail(schemaCall.args[0], "dynamic .schema(...)");
248
+ return;
249
+ }
250
+ if (!this.catalog.schemas.has(schemaName)) {
251
+ this.fail(schemaCall.args[0], `unknown schema "${schemaName}"`);
252
+ return;
253
+ }
254
+ schema = schemaName;
255
+ }
256
+ if (rpcCall)
257
+ this.checkRpc(schema, rpcCall, scope);
258
+ if (fromCall) {
259
+ if (fromCall.args.length === 0) {
260
+ this.fail(fromCall.node, "dynamic .from(...)");
261
+ return;
262
+ }
263
+ const tableExpr = fromCall.args[0];
264
+ const tableName = this.str(tableExpr);
265
+ if (!tableName) {
266
+ if (this.skipGenericParam(tableExpr))
267
+ return;
268
+ this.fail(tableExpr, "dynamic .from(...)");
269
+ return;
270
+ }
271
+ relation = this.catalog.relations.get((0, types_1.relKey)(schema, tableName));
272
+ if (!relation) {
273
+ const elsewhere = this.catalog.byName.get(tableName);
274
+ const hint = elsewhere?.length
275
+ ? ` (exists in ${elsewhere.map((r) => r.schema).join(", ")})`
276
+ : "";
277
+ this.fail(tableExpr, `unknown relation "${tableName}" in schema "${schema}"${hint}`);
278
+ return;
279
+ }
280
+ embedAliases.clear();
281
+ }
282
+ if (!relation)
283
+ return;
284
+ for (const method of methods) {
285
+ this.checkMethod(relation, method, embedAliases, scope);
286
+ }
287
+ const name = (0, ast_1.assignedName)(tail);
288
+ if (name)
289
+ scope.setQuery(name, { relation, embeds: embedAliases });
290
+ }
291
+ skipGenericParam(expr) {
292
+ const ident = (0, ast_1.unwrapExpr)(expr);
293
+ if (!typescript_1.default.isIdentifier(ident))
294
+ return false;
295
+ if (this.subst.has(ident.text))
296
+ return false;
297
+ return (0, ast_1.isParameterIdentifier)(ident);
298
+ }
299
+ resolvePayload(expr, scope) {
300
+ if (!expr)
301
+ return undefined;
302
+ const node = (0, ast_1.unwrapExpr)(expr);
303
+ if (typescript_1.default.isIdentifier(node))
304
+ return scope.getObject(node.text) ?? expr;
305
+ return expr;
306
+ }
307
+ collectKeys(expr, scope, depth = 0) {
308
+ if (!expr || depth > 6)
309
+ return null;
310
+ const node = (0, ast_1.unwrapExpr)(expr);
311
+ if (typescript_1.default.isBinaryExpression(node) && node.operatorToken.kind === typescript_1.default.SyntaxKind.AmpersandAmpersandToken) {
312
+ return this.collectKeys(node.right, scope, depth + 1);
313
+ }
314
+ if (typescript_1.default.isConditionalExpression(node)) {
315
+ const left = this.collectKeys(node.whenTrue, scope, depth + 1) ?? { keys: [], unresolved: false };
316
+ const right = this.collectKeys(node.whenFalse, scope, depth + 1) ?? { keys: [], unresolved: false };
317
+ return {
318
+ keys: [...new Set([...left.keys, ...right.keys])],
319
+ unresolved: left.unresolved || right.unresolved,
320
+ };
321
+ }
322
+ if (typescript_1.default.isIdentifier(node)) {
323
+ const obj = scope.getObject(node.text);
324
+ if (obj)
325
+ return this.collectKeys(obj, scope, depth + 1);
326
+ if (this.tsChecker) {
327
+ const names = (0, program_1.propertyNamesFromType)(this.tsChecker, node);
328
+ if (names)
329
+ return { keys: names, unresolved: false };
330
+ }
331
+ return { keys: [], unresolved: true };
332
+ }
333
+ if (typescript_1.default.isCallExpression(node)) {
334
+ if (this.tsChecker) {
335
+ const names = (0, program_1.propertyNamesFromType)(this.tsChecker, node);
336
+ if (names)
337
+ return { keys: names, unresolved: false };
338
+ }
339
+ return { keys: [], unresolved: true };
340
+ }
341
+ if (typescript_1.default.isArrayLiteralExpression(node)) {
342
+ const keys = new Set();
343
+ let unresolved = false;
344
+ let any = false;
345
+ for (const el of node.elements) {
346
+ const inner = this.collectKeys(el, scope, depth + 1);
347
+ if (!inner)
348
+ continue;
349
+ any = true;
350
+ inner.keys.forEach((k) => keys.add(k));
351
+ unresolved = unresolved || inner.unresolved;
352
+ }
353
+ return any ? { keys: [...keys], unresolved } : null;
354
+ }
355
+ if (!typescript_1.default.isObjectLiteralExpression(node)) {
356
+ if (this.tsChecker) {
357
+ const names = (0, program_1.propertyNamesFromType)(this.tsChecker, node);
358
+ if (names)
359
+ return { keys: names, unresolved: false };
360
+ }
361
+ return { keys: [], unresolved: true };
362
+ }
363
+ const keys = new Set();
364
+ let unresolved = false;
365
+ for (const prop of node.properties) {
366
+ if (typescript_1.default.isSpreadAssignment(prop)) {
367
+ const inner = this.collectKeys(prop.expression, scope, depth + 1);
368
+ if (!inner || inner.unresolved)
369
+ unresolved = true;
370
+ inner?.keys.forEach((k) => keys.add(k));
371
+ continue;
372
+ }
373
+ if (typescript_1.default.isPropertyAssignment(prop) || typescript_1.default.isShorthandPropertyAssignment(prop)) {
374
+ const name = (0, ast_1.propName)(prop.name);
375
+ if (name)
376
+ keys.add(name);
377
+ }
378
+ }
379
+ return { keys: [...keys], unresolved };
380
+ }
381
+ checkRpc(schema, rpcCall, scope) {
382
+ if (rpcCall.args.length === 0) {
383
+ this.fail(rpcCall.node, "dynamic .rpc(...)");
384
+ return;
385
+ }
386
+ const fn = this.str(rpcCall.args[0]);
387
+ if (!fn) {
388
+ if (this.skipGenericParam(rpcCall.args[0]))
389
+ return;
390
+ this.fail(rpcCall.args[0], "dynamic .rpc(...)");
391
+ return;
392
+ }
393
+ const rpc = this.catalog.functions.get(schema)?.get(fn);
394
+ if (!rpc) {
395
+ const found = [...this.catalog.functions.entries()]
396
+ .filter(([, map]) => map.has(fn))
397
+ .map(([s]) => s);
398
+ const hint = found.length ? ` (exists in ${found.join(", ")})` : "";
399
+ this.fail(rpcCall.args[0], `unknown rpc "${fn}" in schema "${schema}"${hint}`);
400
+ return;
401
+ }
402
+ if (rpc.argsNever) {
403
+ if (rpcCall.args[1])
404
+ this.fail(rpcCall.args[1], `rpc "${fn}" takes no args`);
405
+ return;
406
+ }
407
+ if (!rpcCall.args[1]) {
408
+ this.fail(rpcCall.node, `rpc "${fn}": missing args object`);
409
+ return;
410
+ }
411
+ const collected = this.collectKeys(this.resolvePayload(rpcCall.args[1], scope), scope);
412
+ if (!collected) {
413
+ this.fail(rpcCall.args[1], `rpc "${fn}": dynamic args`);
414
+ return;
415
+ }
416
+ if (collected.unresolved) {
417
+ this.fail(rpcCall.args[1], `rpc "${fn}": unresolved spread/dynamic args`);
418
+ return;
419
+ }
420
+ for (const key of collected.keys) {
421
+ if (!rpc.argNames.has(key)) {
422
+ this.fail(rpcCall.args[1], `rpc "${fn}": unknown arg "${key}"`);
423
+ }
424
+ }
425
+ for (const req of rpc.argNames) {
426
+ if (!collected.keys.includes(req)) {
427
+ this.fail(rpcCall.args[1], `rpc "${fn}": missing arg "${req}"`);
428
+ }
429
+ }
430
+ }
431
+ checkMethod(relation, method, embedAliases, scope) {
432
+ switch (method.name) {
433
+ case "select":
434
+ this.checkSelect(relation, method, embedAliases);
435
+ break;
436
+ case "insert":
437
+ this.checkPayload(relation, method.args[0], "insert", relation.insertColumns, scope, true, method.node);
438
+ break;
439
+ case "update":
440
+ this.checkPayload(relation, method.args[0], "update", relation.updateColumns, scope, false, method.node);
441
+ break;
442
+ case "upsert":
443
+ this.checkPayload(relation, method.args[0], "upsert", relation.insertColumns, scope, false, method.node);
444
+ this.checkOnConflict(relation, method);
445
+ break;
446
+ case "match":
447
+ this.checkMatch(relation, method, embedAliases, scope);
448
+ break;
449
+ case "or":
450
+ this.checkOr(relation, method, embedAliases);
451
+ break;
452
+ default:
453
+ if (paths_1.FILTER_COLUMN_METHODS.has(method.name) && method.args[0]) {
454
+ this.checkFilterColumn(relation, method, embedAliases);
455
+ }
456
+ }
457
+ }
458
+ checkPayload(relation, expr, kind, allowed, scope, checkRequired, at, mode = "direct") {
459
+ if (!expr) {
460
+ this.fail(at, `${kind} on ${(0, types_1.relKey)(relation.schema, relation.name)}: missing payload`);
461
+ return;
462
+ }
463
+ if (this.subst.size > 0) {
464
+ const node = (0, ast_1.unwrapExpr)(expr);
465
+ if (typescript_1.default.isIdentifier(node) && (0, ast_1.isParameterIdentifier)(node))
466
+ return;
467
+ }
468
+ const collected = this.collectKeys(this.resolvePayload(expr, scope), scope);
469
+ if (collected) {
470
+ const keys = collected.keys;
471
+ for (const key of keys) {
472
+ if (!allowed.has(key)) {
473
+ this.fail(expr, `${kind} on ${(0, types_1.relKey)(relation.schema, relation.name)}: unknown column "${key}"`);
474
+ }
475
+ }
476
+ if (checkRequired && !collected.unresolved) {
477
+ for (const req of relation.insertRequired) {
478
+ if (!keys.includes(req)) {
479
+ this.fail(expr, `insert on ${(0, types_1.relKey)(relation.schema, relation.name)}: missing required column "${req}"`);
480
+ }
481
+ }
482
+ }
483
+ const parsed = (0, ast_1.objectKeys)(this.resolvePayload(expr, scope));
484
+ if (parsed) {
485
+ for (const key of parsed.keys) {
486
+ this.checkLiteralDomain(relation, key, this.payloadValue(this.resolvePayload(expr, scope), key), expr, kind);
487
+ }
488
+ }
489
+ if (!collected.unresolved)
490
+ return;
491
+ }
492
+ const ident = (0, ast_1.unwrapExpr)(expr);
493
+ if (typescript_1.default.isIdentifier(ident) && mode !== "callsite") {
494
+ const param = (0, ast_1.enclosingParam)(ident);
495
+ if (param) {
496
+ this.queuePending({
497
+ fn: param.fn,
498
+ paramIndex: param.index,
499
+ relation,
500
+ kind,
501
+ allowed,
502
+ checkRequired,
503
+ origin: expr,
504
+ hits: 0,
505
+ seenCalls: new Set(),
506
+ });
507
+ return;
508
+ }
509
+ }
510
+ this.fail(expr, `${kind} on ${(0, types_1.relKey)(relation.schema, relation.name)}: ${collected?.unresolved ? "unresolved spread in payload" : "dynamic payload"}`);
511
+ }
512
+ payloadValue(expr, key) {
513
+ if (!expr)
514
+ return undefined;
515
+ const node = (0, ast_1.unwrapExpr)(expr);
516
+ const objs = typescript_1.default.isArrayLiteralExpression(node)
517
+ ? node.elements.map((el) => (0, ast_1.unwrapExpr)(el)).filter(typescript_1.default.isObjectLiteralExpression)
518
+ : typescript_1.default.isObjectLiteralExpression(node)
519
+ ? [node]
520
+ : [];
521
+ for (const obj of objs) {
522
+ for (const prop of obj.properties) {
523
+ if (!typescript_1.default.isPropertyAssignment(prop) && !typescript_1.default.isShorthandPropertyAssignment(prop))
524
+ continue;
525
+ const name = typescript_1.default.isIdentifier(prop.name) || typescript_1.default.isStringLiteral(prop.name) ? prop.name.text : null;
526
+ if (name !== key)
527
+ continue;
528
+ if (typescript_1.default.isPropertyAssignment(prop))
529
+ return prop.initializer;
530
+ }
531
+ }
532
+ return undefined;
533
+ }
534
+ checkLiteralDomain(relation, column, valueExpr, node, via) {
535
+ if (!valueExpr)
536
+ return;
537
+ const domain = relation.valueDomain.get(column);
538
+ if (!domain)
539
+ return;
540
+ const value = (0, ast_1.literalValue)(valueExpr, this.consts, this.evalOpts());
541
+ if (typeof value !== "string")
542
+ return;
543
+ if (!domain.includes(value)) {
544
+ this.fail(node, `${via} on ${(0, types_1.relKey)(relation.schema, relation.name)}: "${column}" value "${value}" is not ${domain.join(" | ")}`);
545
+ }
546
+ }
547
+ checkOnConflict(relation, method) {
548
+ const onConflict = (0, ast_1.optionString)(method.args, ["onConflict"], this.consts, this.evalOpts());
549
+ if (onConflict) {
550
+ for (const col of onConflict.split(",").map((c) => c.trim()).filter(Boolean)) {
551
+ if (!relation.columns.has(col)) {
552
+ this.fail(method.args[1] ?? method.node, `upsert onConflict column "${col}" is not on ${(0, types_1.relKey)(relation.schema, relation.name)}`);
553
+ }
554
+ }
555
+ return;
556
+ }
557
+ const obj = method.args[1] ? (0, ast_1.unwrapExpr)(method.args[1]) : undefined;
558
+ if (!obj || !typescript_1.default.isObjectLiteralExpression(obj))
559
+ return;
560
+ for (const prop of obj.properties) {
561
+ if (!typescript_1.default.isPropertyAssignment(prop) && !typescript_1.default.isShorthandPropertyAssignment(prop))
562
+ continue;
563
+ if ((0, ast_1.propName)(prop.name) !== "onConflict")
564
+ continue;
565
+ const value = typescript_1.default.isPropertyAssignment(prop) ? prop.initializer : prop.name;
566
+ if (this.skipGenericParam(value))
567
+ return;
568
+ this.fail(prop, `upsert on ${(0, types_1.relKey)(relation.schema, relation.name)}: dynamic onConflict`);
569
+ }
570
+ }
571
+ checkMatch(relation, method, embedAliases, scope) {
572
+ const collected = this.collectKeys(this.resolvePayload(method.args[0], scope), scope);
573
+ if (!collected || collected.unresolved) {
574
+ this.fail(method.args[0] ?? method.node, `match on ${(0, types_1.relKey)(relation.schema, relation.name)}: dynamic payload`);
575
+ return;
576
+ }
577
+ for (const key of collected.keys) {
578
+ this.assertColumnOrPath(relation, method.args[0], key, embedAliases, "match");
579
+ }
580
+ }
581
+ checkOr(relation, method, embedAliases) {
582
+ if (!method.args[0])
583
+ return;
584
+ const extracted = (0, ast_1.stringish)(method.args[0], this.consts, this.evalOpts());
585
+ if (!extracted)
586
+ return;
587
+ const foreignTable = (0, ast_1.optionString)(method.args, ["foreignTable", "referencedTable"], this.consts, this.evalOpts());
588
+ let target = relation;
589
+ if (foreignTable) {
590
+ const resolved = this.resolveEmbed(relation, {
591
+ alias: null,
592
+ name: foreignTable,
593
+ hints: [],
594
+ children: [],
595
+ }, method.node);
596
+ if (!resolved)
597
+ return;
598
+ target = resolved;
599
+ embedAliases.set(foreignTable, resolved);
600
+ }
601
+ for (const col of (0, select_1.filterColumns)(extracted.text)) {
602
+ this.assertColumnOrPath(target, method.args[0], col, embedAliases, "or");
603
+ }
604
+ }
605
+ checkFilterColumn(relation, method, embedAliases) {
606
+ const cols = (0, ast_1.evalStrings)(method.args[0], this.consts, this.evalOpts());
607
+ if (!cols) {
608
+ if (this.skipGenericParam(method.args[0]))
609
+ return;
610
+ this.fail(method.args[0], `${method.name} on ${(0, types_1.relKey)(relation.schema, relation.name)}: dynamic column`);
611
+ return;
612
+ }
613
+ const foreignTable = (0, ast_1.optionString)(method.args, ["foreignTable", "referencedTable"], this.consts, this.evalOpts());
614
+ let target = relation;
615
+ if (foreignTable) {
616
+ const resolved = this.resolveEmbed(relation, {
617
+ alias: null,
618
+ name: foreignTable,
619
+ hints: [],
620
+ children: [],
621
+ }, method.node);
622
+ if (!resolved)
623
+ return;
624
+ target = resolved;
625
+ }
626
+ for (const extracted of cols) {
627
+ this.assertColumnOrPath(target, method.args[0], extracted, embedAliases, method.name);
628
+ if (method.args[1] && ["eq", "neq"].includes(method.name) && cols.length === 1) {
629
+ this.checkLiteralDomain(target, extracted, method.args[1], method.args[1], method.name);
630
+ }
631
+ }
632
+ }
633
+ checkSelect(relation, method, embedAliases) {
634
+ if (method.args.length === 0)
635
+ return;
636
+ const extracted = (0, ast_1.stringish)(method.args[0], this.consts, this.evalOpts());
637
+ if (!extracted?.complete) {
638
+ if (this.skipGenericParam(method.args[0]))
639
+ return;
640
+ this.fail(method.args[0], `dynamic .select(...) on ${(0, types_1.relKey)(relation.schema, relation.name)}`);
641
+ return;
642
+ }
643
+ this.walkSelect(relation, (0, select_1.parseSelectList)(extracted.text), method.args[0], embedAliases);
644
+ }
645
+ walkSelect(relation, items, node, embedAliases) {
646
+ for (const item of items) {
647
+ if (item.name === "*")
648
+ continue;
649
+ if (item.children) {
650
+ const target = this.resolveEmbed(relation, item, node);
651
+ if (!target)
652
+ continue;
653
+ const alias = item.alias ?? item.name;
654
+ embedAliases.set(alias, target);
655
+ embedAliases.set(item.name, target);
656
+ this.walkSelect(target, item.children, node, embedAliases);
657
+ continue;
658
+ }
659
+ if (paths_1.AGGREGATES.has(item.name))
660
+ continue;
661
+ if (!relation.columns.has(item.name)) {
662
+ this.fail(node, `select on ${(0, types_1.relKey)(relation.schema, relation.name)}: unknown column "${item.name}"`);
663
+ }
664
+ }
665
+ }
666
+ assertColumnOrPath(relation, node, column, embedAliases, via) {
667
+ if (relation.columns.has(column))
668
+ return;
669
+ if (embedAliases.has(column) && (via === "is" || via === "not"))
670
+ return;
671
+ if (column.includes(".")) {
672
+ const parts = column.split(".");
673
+ let current = relation;
674
+ for (let i = 0; i < parts.length; i++) {
675
+ const part = parts[i];
676
+ if (!current)
677
+ break;
678
+ if (i === parts.length - 1 && current.columns.has(part))
679
+ return;
680
+ const aliased = embedAliases.get(part);
681
+ if (aliased) {
682
+ current = aliased;
683
+ continue;
684
+ }
685
+ const next = this.resolveEmbed(current, {
686
+ alias: null,
687
+ name: part,
688
+ hints: [],
689
+ children: [],
690
+ }, node, true);
691
+ if (!next) {
692
+ this.fail(node, `${via} on ${(0, types_1.relKey)(relation.schema, relation.name)}: cannot resolve path "${column}" at "${part}"`);
693
+ return;
694
+ }
695
+ current = next;
696
+ }
697
+ return;
698
+ }
699
+ this.fail(node, `${via} on ${(0, types_1.relKey)(relation.schema, relation.name)}: unknown column "${column}"`);
700
+ }
701
+ resolveEmbed(from, item, node, silent = false) {
702
+ const hints = item.hints.filter((h) => !paths_1.JOIN_HINTS.has(h));
703
+ const matches = this.embedCandidates(from, item.name, hints);
704
+ if (matches.length === 1)
705
+ return matches[0].target;
706
+ if (matches.length > 1) {
707
+ if (!silent) {
708
+ const fks = matches.map((m) => m.via).join(", ");
709
+ this.fail(node, `ambiguous embed "${item.name}" on ${(0, types_1.relKey)(from.schema, from.name)} — hint with !fk (${fks})`);
710
+ }
711
+ return undefined;
712
+ }
713
+ if (!silent) {
714
+ const hint = hints.length ? ` with hint !${hints.join("!")}` : "";
715
+ this.fail(node, `no foreign key for embed "${item.name}"${hint} on ${(0, types_1.relKey)(from.schema, from.name)}`);
716
+ }
717
+ return undefined;
718
+ }
719
+ embedCandidates(from, name, hints) {
720
+ const out = [];
721
+ const seen = new Set();
722
+ const add = (target, via) => {
723
+ if (!target)
724
+ return;
725
+ const key = `${(0, types_1.relKey)(target.schema, target.name)}|${via}`;
726
+ if (seen.has(key))
727
+ return;
728
+ seen.add(key);
729
+ out.push({ target, via });
730
+ };
731
+ const hintOk = (rel) => {
732
+ if (hints.length === 0)
733
+ return true;
734
+ return hints.every((h) => rel.foreignKeyName === h || rel.columns.includes(h) || rel.referencedColumns.includes(h));
735
+ };
736
+ for (const rel of from.relationships) {
737
+ const target = (0, catalog_1.lookupTarget)(this.catalog, from.schema, rel.referencedRelation);
738
+ const nameMatches = rel.referencedRelation === name
739
+ || rel.foreignKeyName === name
740
+ || rel.columns.includes(name)
741
+ || target?.name === name;
742
+ if (!nameMatches || !hintOk(rel))
743
+ continue;
744
+ add(target, rel.foreignKeyName);
745
+ }
746
+ for (const other of this.catalog.relations.values()) {
747
+ for (const rel of other.relationships) {
748
+ if (!(0, catalog_1.pointsTo)(this.catalog, rel, other.schema, from))
749
+ continue;
750
+ const nameMatches = other.name === name
751
+ || rel.foreignKeyName === name
752
+ || rel.columns.includes(name);
753
+ if (!nameMatches || !hintOk(rel))
754
+ continue;
755
+ add(other, rel.foreignKeyName);
756
+ }
757
+ }
758
+ return out;
759
+ }
760
+ }
761
+ exports.Checker = Checker;
762
+ function sameFunction(a, b) {
763
+ return a === b
764
+ || (a.getSourceFile().fileName === b.getSourceFile().fileName && a.getStart() === b.getStart());
765
+ }
766
+ function flushUnhitAnyPayloads(pending, errors) {
767
+ for (const item of pending) {
768
+ if (item.hits > 0)
769
+ continue;
770
+ const sf = item.origin.getSourceFile();
771
+ const { line, column } = (0, ast_1.loc)(sf, item.origin);
772
+ errors.push({
773
+ file: (0, files_1.relPath)(sf.fileName),
774
+ line,
775
+ column,
776
+ message: `${item.kind} on ${(0, types_1.relKey)(item.relation.schema, item.relation.name)}: dynamic payload`,
777
+ });
778
+ }
779
+ }
780
+ function stringifySubst(subst) {
781
+ return [...subst.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",");
782
+ }
783
+ function sameSubst(a, b) {
784
+ if (a.size !== b.size)
785
+ return false;
786
+ for (const [k, v] of a) {
787
+ if (b.get(k) !== v)
788
+ return false;
789
+ }
790
+ return true;
791
+ }