@sequenceholdings/orm 0.1.0 → 0.1.2
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 +1 -1
- package/dist/authoring.cjs +794 -0
- package/dist/authoring.cjs.map +1 -0
- package/dist/authoring.d.cts +274 -0
- package/dist/authoring.d.ts +274 -0
- package/dist/authoring.js +763 -0
- package/dist/authoring.js.map +1 -0
- package/dist/canonical.cjs +146 -0
- package/dist/canonical.cjs.map +1 -0
- package/dist/canonical.d.cts +369 -0
- package/dist/canonical.d.ts +369 -0
- package/dist/canonical.js +131 -0
- package/dist/canonical.js.map +1 -0
- package/dist/cli.cjs +6208 -1693
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +8 -1
- package/dist/cli.d.ts +8 -1
- package/dist/cli.js +6262 -1749
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +120 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +74 -2
- package/dist/client.d.ts +74 -2
- package/dist/client.js +116 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +3001 -0
- package/dist/compiler.cjs.map +1 -0
- package/dist/compiler.d.cts +581 -0
- package/dist/compiler.d.ts +581 -0
- package/dist/compiler.js +2947 -0
- package/dist/compiler.js.map +1 -0
- package/dist/index.cjs +1730 -327
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +126 -56
- package/dist/index.d.ts +126 -56
- package/dist/index.js +1643 -324
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +407 -0
- package/dist/ir.cjs.map +1 -0
- package/dist/ir.d.cts +1179 -0
- package/dist/ir.d.ts +1179 -0
- package/dist/ir.js +388 -0
- package/dist/ir.js.map +1 -0
- package/dist/naming-CTvWSRTf.d.ts +79 -0
- package/dist/naming-DRyinU4u.d.cts +79 -0
- package/dist/sdl.cjs +833 -0
- package/dist/sdl.cjs.map +1 -0
- package/dist/sdl.d.cts +96 -0
- package/dist/sdl.d.ts +96 -0
- package/dist/sdl.js +809 -0
- package/dist/sdl.js.map +1 -0
- package/package.json +36 -3
- package/src/codegen-gql/apollo-hooks.cjs +98 -0
- package/src/codegen-gql/server-fns.cjs +101 -0
- package/templates/README.md +13 -0
- package/templates/orm-namespace/CLAUDE.md +81 -0
- package/templates/orm-namespace/codegen.ts +22 -0
- package/templates/orm-namespace/gitignore +5 -0
- package/templates/orm-namespace/graphql/example/queries.ts +56 -0
- package/templates/orm-namespace/package.json +24 -0
- package/templates/orm-namespace/schema/tables.ts +71 -0
- package/templates/orm-namespace/sequence.config.ts +32 -0
- package/templates/orm-namespace/tsconfig.json +15 -0
|
@@ -0,0 +1,3001 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var graphql = require('graphql');
|
|
6
|
+
|
|
7
|
+
// src/compiler-gql/artifact.ts
|
|
8
|
+
|
|
9
|
+
// src/constants.ts
|
|
10
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
11
|
+
var ORM_SCHEMA_PREFIX = "orm_";
|
|
12
|
+
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
13
|
+
|
|
14
|
+
// src/hash.ts
|
|
15
|
+
function stableJson(value) {
|
|
16
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
17
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
18
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
|
|
19
|
+
return `{${entries.join(",")}}`;
|
|
20
|
+
}
|
|
21
|
+
var CANONICAL_FORMAT = 2;
|
|
22
|
+
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
23
|
+
var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
24
|
+
var COLUMN_TYPES = [
|
|
25
|
+
"uuid",
|
|
26
|
+
"text",
|
|
27
|
+
"integer",
|
|
28
|
+
"bigint",
|
|
29
|
+
"numeric",
|
|
30
|
+
"double",
|
|
31
|
+
"boolean",
|
|
32
|
+
"timestamptz",
|
|
33
|
+
"date",
|
|
34
|
+
"jsonb"
|
|
35
|
+
];
|
|
36
|
+
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
37
|
+
var canonicalEnumSchema = zod.z.object({
|
|
38
|
+
name: identifier,
|
|
39
|
+
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
40
|
+
});
|
|
41
|
+
var canonicalColumnSchema = zod.z.object({
|
|
42
|
+
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
43
|
+
name: zod.z.string().min(1).max(63),
|
|
44
|
+
columnName: identifier,
|
|
45
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
|
|
46
|
+
nullable: zod.z.boolean().default(true),
|
|
47
|
+
unique: zod.z.boolean().default(false),
|
|
48
|
+
/** Literal default, or one of the allowlisted SQL functions. */
|
|
49
|
+
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
|
|
50
|
+
defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
51
|
+
description: zod.z.string().max(500).optional(),
|
|
52
|
+
/** Migration hints — same semantics as v1. */
|
|
53
|
+
renamedFrom: identifier.optional(),
|
|
54
|
+
cast: zod.z.string().max(200).optional(),
|
|
55
|
+
deprecated: zod.z.boolean().default(false)
|
|
56
|
+
});
|
|
57
|
+
var canonicalLinkSchema = zod.z.object({
|
|
58
|
+
name: zod.z.string().min(1).max(63),
|
|
59
|
+
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
60
|
+
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
61
|
+
target: identifier.optional(),
|
|
62
|
+
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
63
|
+
on: identifier,
|
|
64
|
+
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
65
|
+
references: identifier.optional(),
|
|
66
|
+
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
67
|
+
enforce: zod.z.boolean().default(false),
|
|
68
|
+
description: zod.z.string().max(500).optional()
|
|
69
|
+
});
|
|
70
|
+
var canonicalIndexSchema = zod.z.object({
|
|
71
|
+
columns: zod.z.array(identifier).min(1),
|
|
72
|
+
unique: zod.z.boolean().default(false),
|
|
73
|
+
name: identifier.optional()
|
|
74
|
+
});
|
|
75
|
+
var policyVariantSchema = zod.z.object({
|
|
76
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
77
|
+
when: zod.z.string().min(1).max(1e3),
|
|
78
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
79
|
+
allow: zod.z.literal(true).optional(),
|
|
80
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
81
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
82
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
83
|
+
});
|
|
84
|
+
var policyExpressionSchema = zod.z.union([
|
|
85
|
+
zod.z.string().min(1).max(2e3),
|
|
86
|
+
zod.z.object({
|
|
87
|
+
default: zod.z.string().min(1).max(2e3),
|
|
88
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
89
|
+
})
|
|
90
|
+
]);
|
|
91
|
+
var canonicalPoliciesSchema = zod.z.object({
|
|
92
|
+
read: policyExpressionSchema.optional(),
|
|
93
|
+
write: policyExpressionSchema.optional()
|
|
94
|
+
});
|
|
95
|
+
var canonicalActionSchema = zod.z.object({
|
|
96
|
+
name: identifier,
|
|
97
|
+
requires: zod.z.string().max(1e3).optional(),
|
|
98
|
+
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
99
|
+
type: columnTypeSchema,
|
|
100
|
+
nullable: zod.z.boolean().default(false)
|
|
101
|
+
})).default({}),
|
|
102
|
+
executes: zod.z.string().min(1).max(1e4),
|
|
103
|
+
returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
|
|
104
|
+
audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
|
|
105
|
+
});
|
|
106
|
+
var canonicalTableSchema = zod.z.object({
|
|
107
|
+
table: identifier,
|
|
108
|
+
description: zod.z.string().max(2e3).optional(),
|
|
109
|
+
/** Table-level rename hint (new in v2). */
|
|
110
|
+
renamedFrom: identifier.optional(),
|
|
111
|
+
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
112
|
+
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
113
|
+
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
114
|
+
primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
|
|
115
|
+
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
116
|
+
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
117
|
+
policies: canonicalPoliciesSchema.default({}),
|
|
118
|
+
actions: zod.z.array(canonicalActionSchema).default([])
|
|
119
|
+
});
|
|
120
|
+
var canonicalDefinitionSchema = zod.z.object({
|
|
121
|
+
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
122
|
+
namespace: identifier.max(41),
|
|
123
|
+
description: zod.z.string().max(2e3).optional(),
|
|
124
|
+
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
125
|
+
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
126
|
+
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
127
|
+
capabilities: zod.z.object({
|
|
128
|
+
roles: zod.z.unknown().optional(),
|
|
129
|
+
uses: zod.z.array(zod.z.string()).default([])
|
|
130
|
+
}).default({ uses: [] })
|
|
131
|
+
});
|
|
132
|
+
function normalizeCanonical(definition) {
|
|
133
|
+
const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
|
|
134
|
+
return {
|
|
135
|
+
...definition,
|
|
136
|
+
enums: sortByName(definition.enums),
|
|
137
|
+
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => ({
|
|
138
|
+
...t,
|
|
139
|
+
links: sortByName(t.links),
|
|
140
|
+
indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
141
|
+
actions: sortByName(t.actions)
|
|
142
|
+
}))
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/ir/index.ts
|
|
147
|
+
var IR_VERSION = 1;
|
|
148
|
+
var CLAIMS_CONTRACT = {
|
|
149
|
+
sub: "string",
|
|
150
|
+
org: "string?",
|
|
151
|
+
roles: "string[]",
|
|
152
|
+
capabilities: "string[]"
|
|
153
|
+
};
|
|
154
|
+
var scalarLiteral = zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]);
|
|
155
|
+
var valueIrSchema = zod.z.discriminatedUnion("kind", [
|
|
156
|
+
zod.z.object({ kind: zod.z.literal("literal"), value: zod.z.union([scalarLiteral, zod.z.array(scalarLiteral)]) }),
|
|
157
|
+
zod.z.object({ kind: zod.z.literal("variable"), name: zod.z.string() }),
|
|
158
|
+
zod.z.object({ kind: zod.z.literal("claim"), key: zod.z.enum(["sub", "org"]) })
|
|
159
|
+
]);
|
|
160
|
+
var CMP_OPS = [
|
|
161
|
+
"eq",
|
|
162
|
+
"neq",
|
|
163
|
+
"gt",
|
|
164
|
+
"gte",
|
|
165
|
+
"lt",
|
|
166
|
+
"lte",
|
|
167
|
+
"in",
|
|
168
|
+
"nin",
|
|
169
|
+
"like",
|
|
170
|
+
"ilike",
|
|
171
|
+
"isNull",
|
|
172
|
+
"contains",
|
|
173
|
+
"hasKey"
|
|
174
|
+
];
|
|
175
|
+
var predIrSchema = zod.z.lazy(
|
|
176
|
+
() => zod.z.discriminatedUnion("kind", [
|
|
177
|
+
zod.z.object({ kind: zod.z.literal("and"), operands: zod.z.array(predIrSchema).min(1) }),
|
|
178
|
+
zod.z.object({ kind: zod.z.literal("or"), operands: zod.z.array(predIrSchema).min(1) }),
|
|
179
|
+
zod.z.object({ kind: zod.z.literal("not"), operand: predIrSchema }),
|
|
180
|
+
zod.z.object({ kind: zod.z.literal("cmp"), op: zod.z.enum(CMP_OPS), column: zod.z.string(), value: valueIrSchema }),
|
|
181
|
+
zod.z.object({ kind: zod.z.literal("exists"), link: zod.z.string(), where: predIrSchema }),
|
|
182
|
+
zod.z.object({
|
|
183
|
+
kind: zod.z.literal("aggPredicate"),
|
|
184
|
+
link: zod.z.string(),
|
|
185
|
+
fn: zod.z.literal("count"),
|
|
186
|
+
op: zod.z.enum(["eq", "neq", "gt", "gte", "lt", "lte"]),
|
|
187
|
+
value: zod.z.number(),
|
|
188
|
+
where: predIrSchema.optional()
|
|
189
|
+
}),
|
|
190
|
+
zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
|
|
191
|
+
zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
|
|
192
|
+
zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
|
|
193
|
+
zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
|
|
194
|
+
])
|
|
195
|
+
);
|
|
196
|
+
var orderIrSchema = zod.z.discriminatedUnion("kind", [
|
|
197
|
+
zod.z.object({
|
|
198
|
+
kind: zod.z.literal("column"),
|
|
199
|
+
column: zod.z.string(),
|
|
200
|
+
/** Order through a to-one link: `link` is set and `column` is on the target. */
|
|
201
|
+
link: zod.z.string().optional(),
|
|
202
|
+
direction: zod.z.enum(["asc", "desc"]),
|
|
203
|
+
nulls: zod.z.enum(["first", "last"]).optional()
|
|
204
|
+
}),
|
|
205
|
+
/** §4f: `$orderBy: [FooOrderBy!]` variable — validated + lowered at runtime. */
|
|
206
|
+
zod.z.object({ kind: zod.z.literal("dynamic"), slot: zod.z.string() })
|
|
207
|
+
]);
|
|
208
|
+
var limitIrSchema = zod.z.object({
|
|
209
|
+
/** Fixed value or a variable, clamped to [1, max]; absent → default. */
|
|
210
|
+
variable: zod.z.string().optional(),
|
|
211
|
+
value: zod.z.number().int().positive().optional(),
|
|
212
|
+
default: zod.z.number().int().positive(),
|
|
213
|
+
max: zod.z.number().int().positive()
|
|
214
|
+
});
|
|
215
|
+
var selectionIrSchema = zod.z.lazy(
|
|
216
|
+
() => zod.z.object({
|
|
217
|
+
typename: zod.z.string(),
|
|
218
|
+
columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
|
|
219
|
+
relations: zod.z.array(zod.z.object({
|
|
220
|
+
alias: zod.z.string(),
|
|
221
|
+
link: zod.z.string(),
|
|
222
|
+
selection: selectionIrSchema,
|
|
223
|
+
where: predIrSchema.optional(),
|
|
224
|
+
orderBy: zod.z.array(orderIrSchema).optional(),
|
|
225
|
+
limit: limitIrSchema.optional(),
|
|
226
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional()
|
|
227
|
+
})),
|
|
228
|
+
aggregates: zod.z.array(zod.z.object({
|
|
229
|
+
alias: zod.z.string(),
|
|
230
|
+
link: zod.z.string().optional(),
|
|
231
|
+
where: predIrSchema.optional(),
|
|
232
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
233
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
234
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
235
|
+
nodes: selectionIrSchema.optional()
|
|
236
|
+
}))
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
|
|
240
|
+
var selectRootSchema = zod.z.object({
|
|
241
|
+
kind: zod.z.literal("select"),
|
|
242
|
+
alias: zod.z.string(),
|
|
243
|
+
table: zod.z.string(),
|
|
244
|
+
selection: selectionIrSchema,
|
|
245
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
246
|
+
where: predIrSchema,
|
|
247
|
+
orderBy: zod.z.array(orderIrSchema).default([]),
|
|
248
|
+
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
249
|
+
limit: limitIrSchema,
|
|
250
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
251
|
+
includeIf: includeIfSchema.optional()
|
|
252
|
+
});
|
|
253
|
+
var byPkRootSchema = zod.z.object({
|
|
254
|
+
kind: zod.z.literal("byPk"),
|
|
255
|
+
alias: zod.z.string(),
|
|
256
|
+
table: zod.z.string(),
|
|
257
|
+
selection: selectionIrSchema,
|
|
258
|
+
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
259
|
+
/** Policy predicate — ANDed with the PK match. */
|
|
260
|
+
where: predIrSchema,
|
|
261
|
+
includeIf: includeIfSchema.optional()
|
|
262
|
+
});
|
|
263
|
+
var aggregateRootSchema = zod.z.object({
|
|
264
|
+
kind: zod.z.literal("aggregate"),
|
|
265
|
+
alias: zod.z.string(),
|
|
266
|
+
table: zod.z.string(),
|
|
267
|
+
where: predIrSchema,
|
|
268
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
269
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
270
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
271
|
+
nodes: selectionIrSchema.optional(),
|
|
272
|
+
includeIf: includeIfSchema.optional()
|
|
273
|
+
});
|
|
274
|
+
var writeValuesSchema = zod.z.union([
|
|
275
|
+
zod.z.object({ slot: zod.z.string() }),
|
|
276
|
+
zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
|
|
277
|
+
]);
|
|
278
|
+
var onConflictIrSchema = zod.z.union([
|
|
279
|
+
zod.z.object({ slot: zod.z.string() }),
|
|
280
|
+
zod.z.object({ constraint: zod.z.string(), updateColumns: zod.z.array(zod.z.string()) })
|
|
281
|
+
]);
|
|
282
|
+
var mutationRootSchema = zod.z.object({
|
|
283
|
+
kind: zod.z.literal("mutation"),
|
|
284
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk"]),
|
|
285
|
+
alias: zod.z.string(),
|
|
286
|
+
table: zod.z.string(),
|
|
287
|
+
/** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
|
|
288
|
+
objects: zod.z.union([
|
|
289
|
+
zod.z.object({ slot: zod.z.string() }),
|
|
290
|
+
zod.z.object({ rows: zod.z.array(zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema }))) })
|
|
291
|
+
]).optional(),
|
|
292
|
+
/** Server-pinned columns (e.g. author_sub ← claim('sub')) merged into every inserted row. */
|
|
293
|
+
pinnedColumns: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).default([]),
|
|
294
|
+
onConflict: onConflictIrSchema.optional(),
|
|
295
|
+
set: writeValuesSchema.optional(),
|
|
296
|
+
append: writeValuesSchema.optional(),
|
|
297
|
+
inc: writeValuesSchema.optional(),
|
|
298
|
+
/** update/delete row filter: AND(writePolicy, documentWhere/pk, dynamicFilter?). */
|
|
299
|
+
where: predIrSchema.optional(),
|
|
300
|
+
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).optional(),
|
|
301
|
+
/** WITH CHECK analogue evaluated against NEW row values (insert/update). */
|
|
302
|
+
insertCheck: predIrSchema.optional(),
|
|
303
|
+
returning: selectionIrSchema.nullable(),
|
|
304
|
+
affectedRows: zod.z.boolean().default(false),
|
|
305
|
+
includeIf: includeIfSchema.optional()
|
|
306
|
+
});
|
|
307
|
+
var actionRootSchema = zod.z.object({
|
|
308
|
+
kind: zod.z.literal("action"),
|
|
309
|
+
alias: zod.z.string(),
|
|
310
|
+
name: zod.z.string(),
|
|
311
|
+
params: zod.z.array(zod.z.object({ name: zod.z.string(), value: valueIrSchema })),
|
|
312
|
+
returns: zod.z.enum(["rows", "rowsAffected", "scalar"])
|
|
313
|
+
});
|
|
314
|
+
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
315
|
+
selectRootSchema,
|
|
316
|
+
byPkRootSchema,
|
|
317
|
+
aggregateRootSchema,
|
|
318
|
+
mutationRootSchema,
|
|
319
|
+
actionRootSchema
|
|
320
|
+
]);
|
|
321
|
+
var INPUT_REF_KINDS = [
|
|
322
|
+
"boolExp",
|
|
323
|
+
"orderBy",
|
|
324
|
+
"selectColumn",
|
|
325
|
+
"insertInput",
|
|
326
|
+
"setInput",
|
|
327
|
+
"appendInput",
|
|
328
|
+
"onConflict"
|
|
329
|
+
];
|
|
330
|
+
var variableDefaultIrSchema = zod.z.object({
|
|
331
|
+
value: zod.z.union([scalarLiteral, zod.z.array(scalarLiteral)])
|
|
332
|
+
});
|
|
333
|
+
var variableIrSchema = zod.z.object({
|
|
334
|
+
name: zod.z.string(),
|
|
335
|
+
type: zod.z.union([
|
|
336
|
+
zod.z.object({ kind: zod.z.literal("scalar"), scalar: zod.z.enum(COLUMN_TYPES) }),
|
|
337
|
+
/** `values` is the declared membership whitelist — the executor validates against it. */
|
|
338
|
+
zod.z.object({ kind: zod.z.literal("enum"), name: zod.z.string(), values: zod.z.array(zod.z.string()).min(1) }),
|
|
339
|
+
/** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
|
|
340
|
+
zod.z.object({ kind: zod.z.literal("inputRef"), input: zod.z.enum(INPUT_REF_KINDS), table: zod.z.string() })
|
|
341
|
+
]),
|
|
342
|
+
nullable: zod.z.boolean(),
|
|
343
|
+
list: zod.z.boolean(),
|
|
344
|
+
/** Absent = no default declared; see {@link variableDefaultIrSchema}. */
|
|
345
|
+
defaultValue: variableDefaultIrSchema.optional()
|
|
346
|
+
});
|
|
347
|
+
var compiledOperationSchema = zod.z.object({
|
|
348
|
+
irVersion: zod.z.literal(IR_VERSION),
|
|
349
|
+
name: zod.z.string(),
|
|
350
|
+
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
351
|
+
kind: zod.z.enum(["query", "mutation", "action"]),
|
|
352
|
+
requires: zod.z.object({
|
|
353
|
+
capability: zod.z.string().optional(),
|
|
354
|
+
role: zod.z.string().optional()
|
|
355
|
+
}).nullable(),
|
|
356
|
+
variables: zod.z.array(variableIrSchema),
|
|
357
|
+
/** Every table the op touches — data-token scoping + audit. */
|
|
358
|
+
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
359
|
+
roots: zod.z.array(rootIrSchema).min(1)
|
|
360
|
+
});
|
|
361
|
+
var tableMetaIrSchema = zod.z.object({
|
|
362
|
+
table: zod.z.string(),
|
|
363
|
+
schema: zod.z.string(),
|
|
364
|
+
typename: zod.z.string(),
|
|
365
|
+
columns: zod.z.array(zod.z.object({
|
|
366
|
+
name: zod.z.string(),
|
|
367
|
+
columnName: zod.z.string(),
|
|
368
|
+
type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
|
|
369
|
+
nullable: zod.z.boolean()
|
|
370
|
+
})),
|
|
371
|
+
primaryKey: zod.z.array(zod.z.string()).min(1),
|
|
372
|
+
links: zod.z.array(zod.z.object({
|
|
373
|
+
name: zod.z.string(),
|
|
374
|
+
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
375
|
+
target: zod.z.string(),
|
|
376
|
+
on: zod.z.string(),
|
|
377
|
+
references: zod.z.string()
|
|
378
|
+
})),
|
|
379
|
+
/** §4f whitelists: what dynamic inputs may reference. */
|
|
380
|
+
filterable: zod.z.array(zod.z.string()),
|
|
381
|
+
orderable: zod.z.array(zod.z.string()),
|
|
382
|
+
insertable: zod.z.array(zod.z.string()),
|
|
383
|
+
settable: zod.z.array(zod.z.string()),
|
|
384
|
+
appendable: zod.z.array(zod.z.string()),
|
|
385
|
+
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
386
|
+
/** Depth/node caps for dynamic filter trees on this table. */
|
|
387
|
+
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
388
|
+
dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
|
|
389
|
+
});
|
|
390
|
+
var compiledArtifactSchema = zod.z.object({
|
|
391
|
+
irVersion: zod.z.literal(IR_VERSION),
|
|
392
|
+
namespace: zod.z.string(),
|
|
393
|
+
schemaHash: zod.z.string(),
|
|
394
|
+
opsHash: zod.z.string(),
|
|
395
|
+
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
396
|
+
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
397
|
+
});
|
|
398
|
+
var IrVersionError = class extends Error {
|
|
399
|
+
constructor(found) {
|
|
400
|
+
super(`Unsupported IR version ${String(found)} \u2014 this executor supports up to ${IR_VERSION}. Upgrade @sequenceholdings/orm or re-push the namespace.`);
|
|
401
|
+
this.name = "IrVersionError";
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function normalizeIr(raw) {
|
|
405
|
+
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
406
|
+
const version = raw.irVersion;
|
|
407
|
+
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
408
|
+
return compiledArtifactSchema.parse(raw);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/predicate-v2/ast.ts
|
|
412
|
+
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
413
|
+
function walkPolicy(node, visit) {
|
|
414
|
+
visit(node);
|
|
415
|
+
if (node.kind === "and" || node.kind === "or") {
|
|
416
|
+
for (const operand of node.operands) walkPolicy(operand, visit);
|
|
417
|
+
}
|
|
418
|
+
if (node.kind === "not") walkPolicy(node.operand, visit);
|
|
419
|
+
if (node.kind === "exists") walkPolicy(node.where, visit);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/predicate-v2/errors.ts
|
|
423
|
+
var PolicyParseError = class extends Error {
|
|
424
|
+
constructor(message, position) {
|
|
425
|
+
super(`${message} (at position ${position})`);
|
|
426
|
+
this.position = position;
|
|
427
|
+
}
|
|
428
|
+
name = "PolicyParseError";
|
|
429
|
+
};
|
|
430
|
+
var PolicyCompileError = class extends Error {
|
|
431
|
+
constructor(message, code, candidates = []) {
|
|
432
|
+
super(candidates.length > 0 ? `${message} (candidates: ${candidates.join(", ")})` : message);
|
|
433
|
+
this.code = code;
|
|
434
|
+
this.candidates = candidates;
|
|
435
|
+
}
|
|
436
|
+
name = "PolicyCompileError";
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// src/predicate-v2/parse.ts
|
|
440
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability", "role", "exists"]);
|
|
441
|
+
var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
442
|
+
var IDENTIFIER_START = /[a-zA-Z_]/;
|
|
443
|
+
var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
444
|
+
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
445
|
+
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
446
|
+
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
447
|
+
function tokenize(source) {
|
|
448
|
+
const tokens = [];
|
|
449
|
+
let position = 0;
|
|
450
|
+
while (position < source.length) {
|
|
451
|
+
const char = source[position];
|
|
452
|
+
if (/\s/.test(char)) {
|
|
453
|
+
position += 1;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (char === "(") {
|
|
457
|
+
tokens.push({ kind: "lparen", value: char, position });
|
|
458
|
+
position += 1;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (char === ")") {
|
|
462
|
+
tokens.push({ kind: "rparen", value: char, position });
|
|
463
|
+
position += 1;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (char === ",") {
|
|
467
|
+
tokens.push({ kind: "comma", value: char, position });
|
|
468
|
+
position += 1;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (char === ".") {
|
|
472
|
+
tokens.push({ kind: "dot", value: char, position });
|
|
473
|
+
position += 1;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (char === "'") {
|
|
477
|
+
const { value, end } = readString(source, position);
|
|
478
|
+
tokens.push({ kind: "string", value, position });
|
|
479
|
+
position = end;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const operator = OPERATOR_PATTERN.exec(source.slice(position));
|
|
483
|
+
if (operator) {
|
|
484
|
+
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
485
|
+
tokens.push({ kind: "operator", value, position });
|
|
486
|
+
position += operator[0].length;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const number = NUMBER_PATTERN.exec(source.slice(position));
|
|
490
|
+
if (number && !IDENTIFIER_START.test(char)) {
|
|
491
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
492
|
+
position += number[0].length;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (IDENTIFIER_START.test(char)) {
|
|
496
|
+
let end = position;
|
|
497
|
+
while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
|
|
498
|
+
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
499
|
+
position = end;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
throw new PolicyParseError(`unexpected character '${char}'`, position);
|
|
503
|
+
}
|
|
504
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
505
|
+
return tokens;
|
|
506
|
+
}
|
|
507
|
+
function readString(source, start) {
|
|
508
|
+
let value = "";
|
|
509
|
+
let position = start + 1;
|
|
510
|
+
while (position < source.length) {
|
|
511
|
+
const char = source[position];
|
|
512
|
+
if (char !== "'") {
|
|
513
|
+
value += char;
|
|
514
|
+
position += 1;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (source[position + 1] === "'") {
|
|
518
|
+
value += "'";
|
|
519
|
+
position += 2;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
return { value, end: position + 1 };
|
|
523
|
+
}
|
|
524
|
+
throw new PolicyParseError("unterminated string literal", start);
|
|
525
|
+
}
|
|
526
|
+
var Parser = class {
|
|
527
|
+
constructor(tokens) {
|
|
528
|
+
this.tokens = tokens;
|
|
529
|
+
}
|
|
530
|
+
index = 0;
|
|
531
|
+
parse() {
|
|
532
|
+
const node = this.parseOr();
|
|
533
|
+
this.expect("end", "end of expression");
|
|
534
|
+
return node;
|
|
535
|
+
}
|
|
536
|
+
parseOr() {
|
|
537
|
+
const operands = [this.parseAnd()];
|
|
538
|
+
while (this.matchKeyword("or")) operands.push(this.parseAnd());
|
|
539
|
+
return operands.length === 1 ? operands[0] : { kind: "or", operands };
|
|
540
|
+
}
|
|
541
|
+
parseAnd() {
|
|
542
|
+
const operands = [this.parseNot()];
|
|
543
|
+
while (this.matchKeyword("and")) operands.push(this.parseNot());
|
|
544
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
545
|
+
}
|
|
546
|
+
parseNot() {
|
|
547
|
+
if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
|
|
548
|
+
return this.parsePrimary();
|
|
549
|
+
}
|
|
550
|
+
parsePrimary() {
|
|
551
|
+
if (this.peek().kind === "lparen") {
|
|
552
|
+
this.advance();
|
|
553
|
+
const inner = this.parseOr();
|
|
554
|
+
this.expect("rparen", "')'");
|
|
555
|
+
return inner;
|
|
556
|
+
}
|
|
557
|
+
if (this.peekKeyword("capability") || this.peekKeyword("role")) {
|
|
558
|
+
const fn = this.peek().value;
|
|
559
|
+
const node = this.parseBooleanCall(fn);
|
|
560
|
+
const next = this.peek();
|
|
561
|
+
if (next.kind === "operator") {
|
|
562
|
+
throw new PolicyParseError(`${fn}() is boolean-valued and cannot appear in a comparison`, next.position);
|
|
563
|
+
}
|
|
564
|
+
return node;
|
|
565
|
+
}
|
|
566
|
+
if (this.peekKeyword("exists")) return this.parseExists();
|
|
567
|
+
return this.parseComparison();
|
|
568
|
+
}
|
|
569
|
+
parseBooleanCall(fn) {
|
|
570
|
+
this.advance();
|
|
571
|
+
const key = this.parseCallArgument(fn);
|
|
572
|
+
if (!CAPABILITY_KEY_PATTERN.test(key)) {
|
|
573
|
+
throw new PolicyParseError(`invalid ${fn} key '${key}'`, this.peek().position);
|
|
574
|
+
}
|
|
575
|
+
return { kind: fn, key };
|
|
576
|
+
}
|
|
577
|
+
parseExists() {
|
|
578
|
+
this.advance();
|
|
579
|
+
this.expect("lparen", "'(' after exists");
|
|
580
|
+
const path = this.parsePath("a link path in exists(...)");
|
|
581
|
+
this.expect("comma", "',' between the exists link path and its predicate");
|
|
582
|
+
const where = this.parseOr();
|
|
583
|
+
this.expect("rparen", "')' closing exists(...)");
|
|
584
|
+
return { kind: "exists", path, where };
|
|
585
|
+
}
|
|
586
|
+
parseComparison() {
|
|
587
|
+
const left = this.parseOperand();
|
|
588
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
589
|
+
if (!POLICY_COMPARE_OPS.includes(operator.value)) {
|
|
590
|
+
throw new PolicyParseError(`unsupported operator '${operator.value}'`, operator.position);
|
|
591
|
+
}
|
|
592
|
+
const right = this.parseOperand();
|
|
593
|
+
return { kind: "compare", op: operator.value, left, right };
|
|
594
|
+
}
|
|
595
|
+
parseOperand() {
|
|
596
|
+
const token = this.peek();
|
|
597
|
+
if (token.kind === "string") {
|
|
598
|
+
this.advance();
|
|
599
|
+
return { kind: "literal", value: token.value };
|
|
600
|
+
}
|
|
601
|
+
if (token.kind === "number") {
|
|
602
|
+
this.advance();
|
|
603
|
+
const value = Number(token.value);
|
|
604
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
605
|
+
throw new PolicyParseError(
|
|
606
|
+
`numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
|
|
607
|
+
token.position
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return { kind: "literal", value };
|
|
611
|
+
}
|
|
612
|
+
if (token.kind !== "identifier") {
|
|
613
|
+
throw new PolicyParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
|
|
614
|
+
}
|
|
615
|
+
if (token.value === "true" || token.value === "false") {
|
|
616
|
+
this.advance();
|
|
617
|
+
return { kind: "literal", value: token.value === "true" };
|
|
618
|
+
}
|
|
619
|
+
if (token.value === "null") {
|
|
620
|
+
this.advance();
|
|
621
|
+
return { kind: "literal", value: null };
|
|
622
|
+
}
|
|
623
|
+
if (token.value === "claim") {
|
|
624
|
+
this.advance();
|
|
625
|
+
const key = this.parseCallArgument("claim");
|
|
626
|
+
return { kind: "claim", key: this.validateClaimKey(key, token.position) };
|
|
627
|
+
}
|
|
628
|
+
if (token.value === "capability" || token.value === "role") {
|
|
629
|
+
throw new PolicyParseError(`${token.value}() is boolean-valued and cannot appear in a comparison`, token.position);
|
|
630
|
+
}
|
|
631
|
+
return { kind: "path", segments: this.parsePath("a column or link path") };
|
|
632
|
+
}
|
|
633
|
+
validateClaimKey(key, position) {
|
|
634
|
+
if (SCALAR_CLAIM_KEYS.includes(key)) return key;
|
|
635
|
+
if (ARRAY_CLAIM_KEYS.includes(key)) {
|
|
636
|
+
const fn = key === "roles" ? "role('\u2026')" : "capability('\u2026')";
|
|
637
|
+
throw new PolicyParseError(
|
|
638
|
+
`claim('${key}') is not reachable \u2014 '${key}' is an array claim; use ${fn} instead`,
|
|
639
|
+
position
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
throw new PolicyParseError(
|
|
643
|
+
`unknown claim key '${key}' \u2014 valid claim() keys are: ${SCALAR_CLAIM_KEYS.map((k) => `'${k}'`).join(", ")}`,
|
|
644
|
+
position
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
/** `ident(.ident)*` — first segment must not be a keyword; later segments may be (e.g. `author.role`). */
|
|
648
|
+
parsePath(description) {
|
|
649
|
+
const first = this.peek();
|
|
650
|
+
if (first.kind !== "identifier" || KEYWORDS.has(first.value)) {
|
|
651
|
+
throw new PolicyParseError(`expected ${description}, got '${first.value || "end"}'`, first.position);
|
|
652
|
+
}
|
|
653
|
+
this.advance();
|
|
654
|
+
const segments = [first.value];
|
|
655
|
+
while (this.peek().kind === "dot") {
|
|
656
|
+
this.advance();
|
|
657
|
+
const segment = this.expect("identifier", "a path segment after .");
|
|
658
|
+
segments.push(segment.value);
|
|
659
|
+
}
|
|
660
|
+
return segments;
|
|
661
|
+
}
|
|
662
|
+
parseCallArgument(fn) {
|
|
663
|
+
this.expect("lparen", `'(' after ${fn}`);
|
|
664
|
+
const argument = this.expect("string", `a quoted key in ${fn}(...)`);
|
|
665
|
+
this.expect("rparen", `')' after ${fn} key`);
|
|
666
|
+
return argument.value;
|
|
667
|
+
}
|
|
668
|
+
peek() {
|
|
669
|
+
return this.tokens[this.index];
|
|
670
|
+
}
|
|
671
|
+
peekKeyword(keyword) {
|
|
672
|
+
const token = this.peek();
|
|
673
|
+
return token.kind === "identifier" && token.value === keyword;
|
|
674
|
+
}
|
|
675
|
+
matchKeyword(keyword) {
|
|
676
|
+
if (!this.peekKeyword(keyword)) return false;
|
|
677
|
+
this.advance();
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
advance() {
|
|
681
|
+
const token = this.peek();
|
|
682
|
+
if (token.kind !== "end") this.index += 1;
|
|
683
|
+
return token;
|
|
684
|
+
}
|
|
685
|
+
expect(kind, description) {
|
|
686
|
+
const token = this.peek();
|
|
687
|
+
if (token.kind !== kind) {
|
|
688
|
+
throw new PolicyParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
|
|
689
|
+
}
|
|
690
|
+
return this.advance();
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
function parsePolicy(source) {
|
|
694
|
+
const trimmed = source.trim();
|
|
695
|
+
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
696
|
+
return { source: trimmed, ast: new Parser(tokenize(trimmed)).parse() };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/predicate-v2/compile.ts
|
|
700
|
+
var POLICY_MAX_LINK_DEPTH = 3;
|
|
701
|
+
var CMP_BY_OP = {
|
|
702
|
+
"=": "eq",
|
|
703
|
+
"!=": "neq",
|
|
704
|
+
"<": "lt",
|
|
705
|
+
"<=": "lte",
|
|
706
|
+
">": "gt",
|
|
707
|
+
">=": "gte"
|
|
708
|
+
};
|
|
709
|
+
var FLIPPED = {
|
|
710
|
+
"=": "=",
|
|
711
|
+
"!=": "!=",
|
|
712
|
+
"<": ">",
|
|
713
|
+
"<=": ">=",
|
|
714
|
+
">": "<",
|
|
715
|
+
">=": "<="
|
|
716
|
+
};
|
|
717
|
+
function resolveTable(resolver, table) {
|
|
718
|
+
const info = resolver.tables[table];
|
|
719
|
+
if (!info) {
|
|
720
|
+
throw new PolicyCompileError(`unknown table '${table}'`, "unknown-table", Object.keys(resolver.tables));
|
|
721
|
+
}
|
|
722
|
+
return info;
|
|
723
|
+
}
|
|
724
|
+
function resolveColumn(info, ref) {
|
|
725
|
+
const column = info.columns.find((c) => c.name === ref || c.columnName === ref);
|
|
726
|
+
return column?.name;
|
|
727
|
+
}
|
|
728
|
+
function resolveLink(info, ref) {
|
|
729
|
+
return info.links.find((l) => l.name === ref);
|
|
730
|
+
}
|
|
731
|
+
function columnCandidates(info) {
|
|
732
|
+
return [...info.columns.map((c) => c.name), ...info.links.map((l) => l.name)];
|
|
733
|
+
}
|
|
734
|
+
var PolicyCompiler = class {
|
|
735
|
+
constructor(ctx) {
|
|
736
|
+
this.ctx = ctx;
|
|
737
|
+
}
|
|
738
|
+
compile(node, table, depth) {
|
|
739
|
+
switch (node.kind) {
|
|
740
|
+
case "and":
|
|
741
|
+
case "or":
|
|
742
|
+
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
743
|
+
case "not":
|
|
744
|
+
return { kind: "not", operand: this.compile(node.operand, table, depth) };
|
|
745
|
+
case "capability":
|
|
746
|
+
return { kind: "capability", key: node.key };
|
|
747
|
+
case "role":
|
|
748
|
+
return { kind: "role", key: node.key };
|
|
749
|
+
case "exists":
|
|
750
|
+
return this.compileExists(node.path, node.where, table, depth);
|
|
751
|
+
case "compare":
|
|
752
|
+
return this.compileCompare(node, table, depth);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/** Traverse link path segments (any cardinality), then compile the inner predicate against the final target. */
|
|
756
|
+
compileExists(path, where, table, depth) {
|
|
757
|
+
const { links, finalTable } = this.resolveLinkPath(path, table, depth, { toOneOnly: false });
|
|
758
|
+
let pred = this.compile(where, finalTable, depth + links.length);
|
|
759
|
+
for (let i = links.length - 1; i >= 0; i -= 1) {
|
|
760
|
+
pred = { kind: "exists", link: links[i].name, where: pred };
|
|
761
|
+
}
|
|
762
|
+
return pred;
|
|
763
|
+
}
|
|
764
|
+
resolveLinkPath(path, table, depth, { toOneOnly }) {
|
|
765
|
+
const links = [];
|
|
766
|
+
let current = table;
|
|
767
|
+
for (const segment of path) {
|
|
768
|
+
const info = resolveTable(this.ctx.resolver, current);
|
|
769
|
+
const link = resolveLink(info, segment);
|
|
770
|
+
if (!link) {
|
|
771
|
+
throw new PolicyCompileError(
|
|
772
|
+
`unknown link '${segment}' on table '${current}'`,
|
|
773
|
+
"unknown-link",
|
|
774
|
+
info.links.map((l) => l.name)
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
if (toOneOnly && link.kind === "toMany") {
|
|
778
|
+
throw new PolicyCompileError(
|
|
779
|
+
`dotted paths traverse to-one links only \u2014 '${segment}' on '${current}' is to-many; use exists(${path.join(".")}, \u2026)`,
|
|
780
|
+
"not-to-one"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
links.push(link);
|
|
784
|
+
current = link.target;
|
|
785
|
+
if (depth + links.length > this.ctx.maxLinkDepth) {
|
|
786
|
+
throw new PolicyCompileError(
|
|
787
|
+
`link traversal depth exceeds ${this.ctx.maxLinkDepth} (at '${segment}')`,
|
|
788
|
+
"link-depth-exceeded"
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return { links, finalTable: current };
|
|
793
|
+
}
|
|
794
|
+
compileCompare(node, table, depth) {
|
|
795
|
+
const leftIsPath = node.left.kind === "path";
|
|
796
|
+
const rightIsPath = node.right.kind === "path";
|
|
797
|
+
if (leftIsPath && rightIsPath) {
|
|
798
|
+
throw new PolicyCompileError(
|
|
799
|
+
"column-to-column comparisons are not supported in v2.0 policies",
|
|
800
|
+
"unsupported-comparison"
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (!leftIsPath && !rightIsPath) {
|
|
804
|
+
throw new PolicyCompileError(
|
|
805
|
+
"a comparison must reference a column \u2014 claims-to-literal comparisons are not representable in v2.0; use capability() or role()",
|
|
806
|
+
"unsupported-comparison"
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
const [pathSide, valueSide, op] = leftIsPath ? [node.left, node.right, node.op] : [node.right, node.left, FLIPPED[node.op]];
|
|
810
|
+
const segments = pathSide.segments;
|
|
811
|
+
const linkSegments = segments.slice(0, -1);
|
|
812
|
+
const columnRef = segments[segments.length - 1];
|
|
813
|
+
const { links, finalTable } = this.resolveLinkPath(linkSegments, table, depth, { toOneOnly: true });
|
|
814
|
+
const info = resolveTable(this.ctx.resolver, finalTable);
|
|
815
|
+
const column = resolveColumn(info, columnRef);
|
|
816
|
+
if (!column) {
|
|
817
|
+
throw new PolicyCompileError(
|
|
818
|
+
`unknown column '${columnRef}' on table '${finalTable}'`,
|
|
819
|
+
"unknown-column",
|
|
820
|
+
columnCandidates(info)
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
let cmp = this.buildCmp(op, column, valueSide);
|
|
824
|
+
for (let i = links.length - 1; i >= 0; i -= 1) {
|
|
825
|
+
cmp = { kind: "exists", link: links[i].name, where: cmp };
|
|
826
|
+
}
|
|
827
|
+
return cmp;
|
|
828
|
+
}
|
|
829
|
+
buildCmp(op, column, valueSide) {
|
|
830
|
+
if (valueSide.kind === "literal" && valueSide.value === null) {
|
|
831
|
+
if (op !== "=" && op !== "!=") {
|
|
832
|
+
throw new PolicyCompileError(
|
|
833
|
+
`null only supports = and != comparisons, got '${op}'`,
|
|
834
|
+
"invalid-null-comparison"
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
return { kind: "cmp", op: "isNull", column, value: { kind: "literal", value: op === "=" } };
|
|
838
|
+
}
|
|
839
|
+
const value = valueSide.kind === "claim" ? { kind: "claim", key: valueSide.key } : { kind: "literal", value: valueSide.value };
|
|
840
|
+
return { kind: "cmp", op: CMP_BY_OP[op], column, value };
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
function compilePredicateV2({ expression, table, resolver, maxLinkDepth }) {
|
|
844
|
+
const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
|
|
845
|
+
const compiler = new PolicyCompiler({ resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH });
|
|
846
|
+
return compiler.compile(parsed.ast, table, 0);
|
|
847
|
+
}
|
|
848
|
+
function assertClaimsOnlyWhen(parsed) {
|
|
849
|
+
walkPolicy(parsed.ast, (node) => {
|
|
850
|
+
if (node.kind === "compare") {
|
|
851
|
+
const columnRef = [node.left, node.right].find((operand) => operand.kind === "path");
|
|
852
|
+
if (columnRef) {
|
|
853
|
+
throw new PolicyCompileError(
|
|
854
|
+
`variant 'when' guards are claims-only \u2014 column reference '${columnRef.segments.join(".")}' is not allowed`,
|
|
855
|
+
"when-not-claims-only"
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
throw new PolicyCompileError(
|
|
859
|
+
"variant 'when' guards support capability() and role() checks only in v2.0",
|
|
860
|
+
"when-not-claims-only"
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (node.kind === "exists") {
|
|
864
|
+
throw new PolicyCompileError(
|
|
865
|
+
"variant 'when' guards are claims-only \u2014 exists() is not allowed",
|
|
866
|
+
"when-not-claims-only"
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
function compilePolicy({ expression, table, resolver, maxLinkDepth }) {
|
|
872
|
+
if (typeof expression === "string") {
|
|
873
|
+
return compilePredicateV2({ expression, table, resolver, maxLinkDepth });
|
|
874
|
+
}
|
|
875
|
+
const defaultPred = compilePredicateV2({ expression: expression.default, table, resolver, maxLinkDepth });
|
|
876
|
+
if (expression.variants.length === 0) return defaultPred;
|
|
877
|
+
const branches = expression.variants.map((variant) => {
|
|
878
|
+
const parsedWhen = parsePolicy(variant.when);
|
|
879
|
+
assertClaimsOnlyWhen(parsedWhen);
|
|
880
|
+
const when = compilePredicateV2({ expression: parsedWhen, table, resolver, maxLinkDepth });
|
|
881
|
+
const grant = variant.allow === true ? { kind: "const", value: true } : compilePredicateV2({ expression: variant.policy, table, resolver, maxLinkDepth });
|
|
882
|
+
return { kind: "and", operands: [when, grant] };
|
|
883
|
+
});
|
|
884
|
+
return { kind: "or", operands: [defaultPred, ...branches] };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/compiler-gql/diagnostics.ts
|
|
888
|
+
var DocumentCompileError = class extends Error {
|
|
889
|
+
constructor(message, code, candidates = []) {
|
|
890
|
+
super(candidates.length > 0 ? `${message} (candidates: ${candidates.join(", ")})` : message);
|
|
891
|
+
this.code = code;
|
|
892
|
+
this.candidates = candidates;
|
|
893
|
+
}
|
|
894
|
+
name = "DocumentCompileError";
|
|
895
|
+
};
|
|
896
|
+
var NamespaceCompileError = class extends Error {
|
|
897
|
+
constructor(diagnostics) {
|
|
898
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
899
|
+
super(
|
|
900
|
+
`namespace compilation failed with ${errors.length} error${errors.length === 1 ? "" : "s"}:
|
|
901
|
+
` + errors.map((d) => ` - [${d.code}] ${d.message}`).join("\n")
|
|
902
|
+
);
|
|
903
|
+
this.diagnostics = diagnostics;
|
|
904
|
+
}
|
|
905
|
+
name = "NamespaceCompileError";
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// src/compiler-gql/pred-utils.ts
|
|
909
|
+
var CONST_TRUE = { kind: "const", value: true };
|
|
910
|
+
function andPred(...preds) {
|
|
911
|
+
const operands = preds.filter(
|
|
912
|
+
(pred) => pred !== void 0 && !(pred.kind === "const" && pred.value)
|
|
913
|
+
);
|
|
914
|
+
if (operands.length === 0) return CONST_TRUE;
|
|
915
|
+
if (operands.length === 1) return operands[0];
|
|
916
|
+
return { kind: "and", operands };
|
|
917
|
+
}
|
|
918
|
+
function walkPred(pred, visit) {
|
|
919
|
+
visit(pred);
|
|
920
|
+
switch (pred.kind) {
|
|
921
|
+
case "and":
|
|
922
|
+
case "or":
|
|
923
|
+
for (const operand of pred.operands) walkPred(operand, visit);
|
|
924
|
+
return;
|
|
925
|
+
case "not":
|
|
926
|
+
walkPred(pred.operand, visit);
|
|
927
|
+
return;
|
|
928
|
+
case "exists":
|
|
929
|
+
walkPred(pred.where, visit);
|
|
930
|
+
return;
|
|
931
|
+
case "aggPredicate":
|
|
932
|
+
if (pred.where) walkPred(pred.where, visit);
|
|
933
|
+
return;
|
|
934
|
+
default:
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function predContainsExists(pred) {
|
|
939
|
+
let found = false;
|
|
940
|
+
walkPred(pred, (node) => {
|
|
941
|
+
if (node.kind === "exists" || node.kind === "aggPredicate") found = true;
|
|
942
|
+
});
|
|
943
|
+
return found;
|
|
944
|
+
}
|
|
945
|
+
function predContains(haystack, needle) {
|
|
946
|
+
const needleJson = JSON.stringify(needle);
|
|
947
|
+
let found = false;
|
|
948
|
+
walkPred(haystack, (node) => {
|
|
949
|
+
if (!found && JSON.stringify(node) === needleJson) found = true;
|
|
950
|
+
});
|
|
951
|
+
return found;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/compiler-gql/inject.ts
|
|
955
|
+
function collectPinnedColumns(writePolicy) {
|
|
956
|
+
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
957
|
+
const visit = (node) => {
|
|
958
|
+
if (node.kind === "and") {
|
|
959
|
+
const merged = [];
|
|
960
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
961
|
+
for (const operand of node.operands) {
|
|
962
|
+
for (const pin of visit(operand)) {
|
|
963
|
+
if (seen2.has(key(pin))) continue;
|
|
964
|
+
seen2.add(key(pin));
|
|
965
|
+
merged.push(pin);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return merged;
|
|
969
|
+
}
|
|
970
|
+
if (node.kind === "or") {
|
|
971
|
+
const [first, ...rest] = node.operands;
|
|
972
|
+
if (first === void 0) return [];
|
|
973
|
+
let common = visit(first);
|
|
974
|
+
for (const operand of rest) {
|
|
975
|
+
const branch = new Set(visit(operand).map(key));
|
|
976
|
+
common = common.filter((pin) => branch.has(key(pin)));
|
|
977
|
+
if (common.length === 0) break;
|
|
978
|
+
}
|
|
979
|
+
return common;
|
|
980
|
+
}
|
|
981
|
+
if (node.kind === "cmp" && node.op === "eq" && node.value.kind === "claim") {
|
|
982
|
+
return [{ column: node.column, claim: node.value.key }];
|
|
983
|
+
}
|
|
984
|
+
return [];
|
|
985
|
+
};
|
|
986
|
+
const seen = /* @__PURE__ */ new Set();
|
|
987
|
+
return visit(writePolicy).filter((pin) => {
|
|
988
|
+
if (seen.has(pin.column)) return false;
|
|
989
|
+
seen.add(pin.column);
|
|
990
|
+
return true;
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
var PolicyInjector = class {
|
|
994
|
+
constructor(ctx) {
|
|
995
|
+
this.ctx = ctx;
|
|
996
|
+
}
|
|
997
|
+
read(table) {
|
|
998
|
+
return this.ctx.readPolicies.get(table) ?? { kind: "const", value: true };
|
|
999
|
+
}
|
|
1000
|
+
write(table) {
|
|
1001
|
+
return this.ctx.writePolicies.get(table) ?? { kind: "const", value: true };
|
|
1002
|
+
}
|
|
1003
|
+
linkTarget(table, link) {
|
|
1004
|
+
const meta = this.ctx.tables[table];
|
|
1005
|
+
const resolved = meta?.links.find((l) => l.name === link);
|
|
1006
|
+
if (!resolved) throw new DocumentCompileError(`internal: unresolved link '${link}' on '${table}' during injection`, "internal");
|
|
1007
|
+
return resolved.target;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Walk a DOCUMENT-authored predicate and AND the target read policy into
|
|
1011
|
+
* every relationship traversal. Policy predicates themselves never pass
|
|
1012
|
+
* through here — they are composed around the result, not rewritten.
|
|
1013
|
+
*/
|
|
1014
|
+
injectDocPred(pred, table) {
|
|
1015
|
+
switch (pred.kind) {
|
|
1016
|
+
case "and":
|
|
1017
|
+
case "or":
|
|
1018
|
+
return { kind: pred.kind, operands: pred.operands.map((operand) => this.injectDocPred(operand, table)) };
|
|
1019
|
+
case "not":
|
|
1020
|
+
return { kind: "not", operand: this.injectDocPred(pred.operand, table) };
|
|
1021
|
+
case "exists": {
|
|
1022
|
+
const target = this.linkTarget(table, pred.link);
|
|
1023
|
+
return { kind: "exists", link: pred.link, where: andPred(this.read(target), this.injectDocPred(pred.where, target)) };
|
|
1024
|
+
}
|
|
1025
|
+
case "aggPredicate": {
|
|
1026
|
+
const target = this.linkTarget(table, pred.link);
|
|
1027
|
+
const where = andPred(this.read(target), pred.where ? this.injectDocPred(pred.where, target) : void 0);
|
|
1028
|
+
return { ...pred, where };
|
|
1029
|
+
}
|
|
1030
|
+
default:
|
|
1031
|
+
return pred;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
injectSelection(selection, table) {
|
|
1035
|
+
return {
|
|
1036
|
+
...selection,
|
|
1037
|
+
relations: selection.relations.map((relation) => {
|
|
1038
|
+
const target = this.linkTarget(table, relation.link);
|
|
1039
|
+
return {
|
|
1040
|
+
...relation,
|
|
1041
|
+
selection: this.injectSelection(relation.selection, target),
|
|
1042
|
+
where: andPred(this.read(target), relation.where ? this.injectDocPred(relation.where, target) : void 0)
|
|
1043
|
+
};
|
|
1044
|
+
}),
|
|
1045
|
+
aggregates: selection.aggregates.map((aggregate) => {
|
|
1046
|
+
const target = aggregate.link ? this.linkTarget(table, aggregate.link) : table;
|
|
1047
|
+
return {
|
|
1048
|
+
...aggregate,
|
|
1049
|
+
where: andPred(this.read(target), aggregate.where ? this.injectDocPred(aggregate.where, target) : void 0),
|
|
1050
|
+
...aggregate.nodes ? { nodes: this.injectSelection(aggregate.nodes, target) } : {}
|
|
1051
|
+
};
|
|
1052
|
+
})
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
injectRoot(root) {
|
|
1056
|
+
switch (root.kind) {
|
|
1057
|
+
case "select":
|
|
1058
|
+
return {
|
|
1059
|
+
...root,
|
|
1060
|
+
selection: this.injectSelection(root.selection, root.table),
|
|
1061
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
1062
|
+
};
|
|
1063
|
+
case "byPk":
|
|
1064
|
+
return {
|
|
1065
|
+
...root,
|
|
1066
|
+
selection: this.injectSelection(root.selection, root.table),
|
|
1067
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
1068
|
+
};
|
|
1069
|
+
case "aggregate":
|
|
1070
|
+
return {
|
|
1071
|
+
...root,
|
|
1072
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
1073
|
+
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
1074
|
+
};
|
|
1075
|
+
case "mutation":
|
|
1076
|
+
return this.injectMutation(root);
|
|
1077
|
+
case "action":
|
|
1078
|
+
return root;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
injectMutation(root) {
|
|
1082
|
+
const table = root.table;
|
|
1083
|
+
const writePolicy = this.write(table);
|
|
1084
|
+
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
1085
|
+
if (root.op === "insert" || root.op === "insertOne") {
|
|
1086
|
+
if (predContainsExists(writePolicy)) {
|
|
1087
|
+
throw new DocumentCompileError(
|
|
1088
|
+
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its write policy traverses a link (exists)`,
|
|
1089
|
+
"insert-check-unrepresentable"
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
1093
|
+
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
1094
|
+
);
|
|
1095
|
+
return { ...root, pinnedColumns: pins, insertCheck: writePolicy, returning };
|
|
1096
|
+
}
|
|
1097
|
+
const where = andPred(writePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
1098
|
+
if (root.op === "update" || root.op === "updateByPk") {
|
|
1099
|
+
const check = predContainsExists(writePolicy) ? {} : { insertCheck: writePolicy };
|
|
1100
|
+
return { ...root, where, ...check, returning };
|
|
1101
|
+
}
|
|
1102
|
+
return { ...root, where, returning };
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
function collectTableSet(roots, ctx) {
|
|
1106
|
+
const tables = /* @__PURE__ */ new Set();
|
|
1107
|
+
const linkTarget = (table, link) => ctx.tables[table]?.links.find((l) => l.name === link)?.target;
|
|
1108
|
+
const visitPred = (pred, table) => {
|
|
1109
|
+
switch (pred.kind) {
|
|
1110
|
+
case "and":
|
|
1111
|
+
case "or":
|
|
1112
|
+
for (const operand of pred.operands) visitPred(operand, table);
|
|
1113
|
+
return;
|
|
1114
|
+
case "not":
|
|
1115
|
+
visitPred(pred.operand, table);
|
|
1116
|
+
return;
|
|
1117
|
+
case "exists":
|
|
1118
|
+
case "aggPredicate": {
|
|
1119
|
+
const target = linkTarget(table, pred.link);
|
|
1120
|
+
if (!target) return;
|
|
1121
|
+
tables.add(target);
|
|
1122
|
+
if (pred.kind === "exists") visitPred(pred.where, target);
|
|
1123
|
+
else if (pred.where) visitPred(pred.where, target);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
default:
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
const visitSelection = (selection, table) => {
|
|
1131
|
+
for (const relation of selection.relations) {
|
|
1132
|
+
const target = linkTarget(table, relation.link);
|
|
1133
|
+
if (!target) continue;
|
|
1134
|
+
tables.add(target);
|
|
1135
|
+
if (relation.where) visitPred(relation.where, target);
|
|
1136
|
+
visitSelection(relation.selection, target);
|
|
1137
|
+
}
|
|
1138
|
+
for (const aggregate of selection.aggregates) {
|
|
1139
|
+
const target = aggregate.link ? linkTarget(table, aggregate.link) : table;
|
|
1140
|
+
if (!target) continue;
|
|
1141
|
+
tables.add(target);
|
|
1142
|
+
if (aggregate.where) visitPred(aggregate.where, target);
|
|
1143
|
+
if (aggregate.nodes) visitSelection(aggregate.nodes, target);
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
for (const root of roots) {
|
|
1147
|
+
if (root.kind === "action") continue;
|
|
1148
|
+
tables.add(root.table);
|
|
1149
|
+
if (root.kind === "select" || root.kind === "byPk") {
|
|
1150
|
+
visitPred(root.where, root.table);
|
|
1151
|
+
visitSelection(root.selection, root.table);
|
|
1152
|
+
}
|
|
1153
|
+
if (root.kind === "aggregate") {
|
|
1154
|
+
visitPred(root.where, root.table);
|
|
1155
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
1156
|
+
}
|
|
1157
|
+
if (root.kind === "mutation") {
|
|
1158
|
+
if (root.where) visitPred(root.where, root.table);
|
|
1159
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
1160
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return [...tables].sort();
|
|
1164
|
+
}
|
|
1165
|
+
function injectPolicies({ roots, ctx }) {
|
|
1166
|
+
const injector = new PolicyInjector(ctx);
|
|
1167
|
+
const injected = roots.map((root) => injector.injectRoot(root));
|
|
1168
|
+
return { roots: injected, tableSet: collectTableSet(injected, ctx) };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/sdl/naming.ts
|
|
1172
|
+
function camelCase(name) {
|
|
1173
|
+
const segments = name.split("_").filter((segment) => segment.length > 0);
|
|
1174
|
+
const first = segments[0];
|
|
1175
|
+
if (first === void 0) return name;
|
|
1176
|
+
const head = first.charAt(0).toLowerCase() + first.slice(1);
|
|
1177
|
+
return head + segments.slice(1).map(capitalizeFirst).join("");
|
|
1178
|
+
}
|
|
1179
|
+
function pascalCase(name) {
|
|
1180
|
+
return capitalizeFirst(camelCase(name));
|
|
1181
|
+
}
|
|
1182
|
+
function capitalizeFirst(segment) {
|
|
1183
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
1184
|
+
}
|
|
1185
|
+
function singularizeWord(word) {
|
|
1186
|
+
if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
|
|
1187
|
+
for (const suffix of ["sses", "xes", "ches", "shes"]) {
|
|
1188
|
+
if (word.endsWith(suffix)) return word.slice(0, -2);
|
|
1189
|
+
}
|
|
1190
|
+
if (word.endsWith("ss")) return word;
|
|
1191
|
+
if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
|
|
1192
|
+
return word;
|
|
1193
|
+
}
|
|
1194
|
+
function typeName(table) {
|
|
1195
|
+
const segments = table.split("_").filter((segment) => segment.length > 0);
|
|
1196
|
+
const last = segments[segments.length - 1];
|
|
1197
|
+
if (last === void 0) return pascalCase(table);
|
|
1198
|
+
return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
|
|
1199
|
+
}
|
|
1200
|
+
function enumTypeName(name) {
|
|
1201
|
+
return pascalCase(name);
|
|
1202
|
+
}
|
|
1203
|
+
var SCALAR_FOR_COLUMN_TYPE = {
|
|
1204
|
+
uuid: "uuid",
|
|
1205
|
+
text: "String",
|
|
1206
|
+
integer: "Int",
|
|
1207
|
+
bigint: "bigint",
|
|
1208
|
+
numeric: "numeric",
|
|
1209
|
+
double: "Float",
|
|
1210
|
+
boolean: "Boolean",
|
|
1211
|
+
timestamptz: "timestamptz",
|
|
1212
|
+
date: "date",
|
|
1213
|
+
jsonb: "jsonb"
|
|
1214
|
+
};
|
|
1215
|
+
function graphqlScalarFor(type) {
|
|
1216
|
+
return SCALAR_FOR_COLUMN_TYPE[type];
|
|
1217
|
+
}
|
|
1218
|
+
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1219
|
+
["ID", "text"],
|
|
1220
|
+
...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
|
|
1221
|
+
]);
|
|
1222
|
+
function columnTypeForScalar(scalarName) {
|
|
1223
|
+
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
1224
|
+
}
|
|
1225
|
+
var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
|
|
1226
|
+
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
1227
|
+
function tableNames(table) {
|
|
1228
|
+
const root = camelCase(table);
|
|
1229
|
+
const rootPascal = pascalCase(table);
|
|
1230
|
+
const type = typeName(table);
|
|
1231
|
+
return {
|
|
1232
|
+
typename: type,
|
|
1233
|
+
selectRoot: root,
|
|
1234
|
+
byPkRoot: `${root}ByPk`,
|
|
1235
|
+
aggregateRoot: `${root}Aggregate`,
|
|
1236
|
+
insertRoot: `insert${rootPascal}`,
|
|
1237
|
+
insertOneRoot: `insert${rootPascal}One`,
|
|
1238
|
+
updateRoot: `update${rootPascal}`,
|
|
1239
|
+
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1240
|
+
deleteRoot: `delete${rootPascal}`,
|
|
1241
|
+
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
1242
|
+
boolExp: `${type}BoolExp`,
|
|
1243
|
+
orderBy: `${type}OrderBy`,
|
|
1244
|
+
selectColumn: `${type}SelectColumn`,
|
|
1245
|
+
insertInput: `${type}InsertInput`,
|
|
1246
|
+
setInput: `${type}SetInput`,
|
|
1247
|
+
appendInput: `${type}AppendInput`,
|
|
1248
|
+
incInput: `${type}IncInput`,
|
|
1249
|
+
onConflict: `${type}OnConflict`,
|
|
1250
|
+
pkColumnsInput: `${type}PkColumnsInput`,
|
|
1251
|
+
constraintEnum: `${type}Constraint`,
|
|
1252
|
+
updateColumnEnum: `${type}UpdateColumn`,
|
|
1253
|
+
mutationResponse: `${type}MutationResponse`,
|
|
1254
|
+
aggregate: `${type}Aggregate`,
|
|
1255
|
+
aggregateFields: `${type}AggregateFields`,
|
|
1256
|
+
sumFields: `${type}SumFields`,
|
|
1257
|
+
avgFields: `${type}AvgFields`,
|
|
1258
|
+
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1259
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/compiler-gql/naming.ts
|
|
1264
|
+
var PLATFORM_USER_TABLE = "platformUsers";
|
|
1265
|
+
function typeNameForTable(table) {
|
|
1266
|
+
return table === PLATFORM_USER_TABLE ? PLATFORM_USER_TYPENAME : typeName(table);
|
|
1267
|
+
}
|
|
1268
|
+
function queryRootNames(table) {
|
|
1269
|
+
const names = tableNames(table);
|
|
1270
|
+
return { select: names.selectRoot, byPk: names.byPkRoot, aggregate: names.aggregateRoot };
|
|
1271
|
+
}
|
|
1272
|
+
function mutationRootNames(table) {
|
|
1273
|
+
const names = tableNames(table);
|
|
1274
|
+
return {
|
|
1275
|
+
insert: names.insertRoot,
|
|
1276
|
+
insertOne: names.insertOneRoot,
|
|
1277
|
+
update: names.updateRoot,
|
|
1278
|
+
updateByPk: names.updateByPkRoot,
|
|
1279
|
+
delete: names.deleteRoot,
|
|
1280
|
+
deleteByPk: names.deleteByPkRoot
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
var INPUT_TYPE_SUFFIXES = [
|
|
1284
|
+
["BoolExp", "boolExp"],
|
|
1285
|
+
["OrderBy", "orderBy"],
|
|
1286
|
+
["SelectColumn", "selectColumn"],
|
|
1287
|
+
["InsertInput", "insertInput"],
|
|
1288
|
+
["SetInput", "setInput"],
|
|
1289
|
+
["AppendInput", "appendInput"],
|
|
1290
|
+
["OnConflict", "onConflict"]
|
|
1291
|
+
];
|
|
1292
|
+
var NAME_KEY_BY_KIND = {
|
|
1293
|
+
boolExp: "boolExp",
|
|
1294
|
+
orderBy: "orderBy",
|
|
1295
|
+
selectColumn: "selectColumn",
|
|
1296
|
+
insertInput: "insertInput",
|
|
1297
|
+
setInput: "setInput",
|
|
1298
|
+
appendInput: "appendInput",
|
|
1299
|
+
onConflict: "onConflict"
|
|
1300
|
+
};
|
|
1301
|
+
function inputTypeName(table, kind) {
|
|
1302
|
+
return tableNames(table)[NAME_KEY_BY_KIND[kind]];
|
|
1303
|
+
}
|
|
1304
|
+
function classifyInputType(name, tableByTypename) {
|
|
1305
|
+
for (const [suffix, input] of INPUT_TYPE_SUFFIXES) {
|
|
1306
|
+
if (!name.endsWith(suffix) || name.length <= suffix.length) continue;
|
|
1307
|
+
const table = tableByTypename.get(name.slice(0, -suffix.length));
|
|
1308
|
+
if (table !== void 0) return { input, table };
|
|
1309
|
+
}
|
|
1310
|
+
return void 0;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/compiler-gql/context.ts
|
|
1314
|
+
function platformUserMeta() {
|
|
1315
|
+
const columns = [
|
|
1316
|
+
// `sub` is the view's key (auth0_id, filtered NOT NULL in the view).
|
|
1317
|
+
{ name: "sub", columnName: "sub", type: "text", nullable: false },
|
|
1318
|
+
{ name: "email", columnName: "email", type: "text", nullable: true },
|
|
1319
|
+
{ name: "name", columnName: "name", type: "text", nullable: true },
|
|
1320
|
+
{ name: "organization", columnName: "organization", type: "text", nullable: true },
|
|
1321
|
+
{ name: "role", columnName: "role", type: "text", nullable: true },
|
|
1322
|
+
{ name: "managerId", columnName: "manager_id", type: "text", nullable: true },
|
|
1323
|
+
// NOT NULL DEFAULT true on chat.users, so non-null through the view.
|
|
1324
|
+
{ name: "accountEnabled", columnName: "account_enabled", type: "boolean", nullable: false }
|
|
1325
|
+
];
|
|
1326
|
+
return {
|
|
1327
|
+
table: "users_directory",
|
|
1328
|
+
schema: "platform",
|
|
1329
|
+
typename: PLATFORM_USER_TYPENAME,
|
|
1330
|
+
columns: columns.map((c) => ({ ...c })),
|
|
1331
|
+
primaryKey: ["sub"],
|
|
1332
|
+
links: [],
|
|
1333
|
+
filterable: columns.map((c) => c.name),
|
|
1334
|
+
orderable: columns.map((c) => c.name),
|
|
1335
|
+
insertable: [],
|
|
1336
|
+
settable: [],
|
|
1337
|
+
appendable: [],
|
|
1338
|
+
constraints: [],
|
|
1339
|
+
dynamicFilterMaxDepth: 6,
|
|
1340
|
+
dynamicFilterMaxNodes: 64
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function resolveColumnEntries(table, enums, diagnostics) {
|
|
1344
|
+
return table.columns.map((column) => {
|
|
1345
|
+
let type;
|
|
1346
|
+
if (typeof column.type === "string") {
|
|
1347
|
+
type = column.type;
|
|
1348
|
+
} else {
|
|
1349
|
+
const enumDef = enums.get(column.type.enum);
|
|
1350
|
+
if (!enumDef) {
|
|
1351
|
+
diagnostics.push({
|
|
1352
|
+
severity: "error",
|
|
1353
|
+
code: "unknown-enum",
|
|
1354
|
+
message: `column '${column.name}' on table '${table.table}' references undeclared enum '${column.type.enum}'`,
|
|
1355
|
+
table: table.table
|
|
1356
|
+
});
|
|
1357
|
+
type = "text";
|
|
1358
|
+
} else {
|
|
1359
|
+
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return { name: column.name, columnName: column.columnName, type, nullable: column.nullable, deprecated: column.deprecated };
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
function logicalColumn(entries, ref) {
|
|
1366
|
+
return entries.find((c) => c.name === ref || c.columnName === ref)?.name;
|
|
1367
|
+
}
|
|
1368
|
+
function buildCompileContext(rawCanonical) {
|
|
1369
|
+
const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
|
|
1370
|
+
const diagnostics = [];
|
|
1371
|
+
const enums = new Map(canonical.enums.map((e) => [e.name, e]));
|
|
1372
|
+
const columnEntries = /* @__PURE__ */ new Map();
|
|
1373
|
+
const tables = {};
|
|
1374
|
+
const usesPlatformUsers = canonical.tables.some((t) => t.links.some((l) => l.kind === "toPlatformUser"));
|
|
1375
|
+
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
1376
|
+
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
1377
|
+
for (const table of canonical.tables) {
|
|
1378
|
+
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
1379
|
+
columnEntries.set(table.table, entries);
|
|
1380
|
+
const pkRefs = table.primaryKey ?? ["id"];
|
|
1381
|
+
const primaryKey = [];
|
|
1382
|
+
for (const ref of pkRefs) {
|
|
1383
|
+
const resolved = logicalColumn(entries, ref);
|
|
1384
|
+
if (!resolved) {
|
|
1385
|
+
diagnostics.push({
|
|
1386
|
+
severity: "error",
|
|
1387
|
+
code: "unknown-pk-column",
|
|
1388
|
+
message: `primary key column '${ref}' does not exist on table '${table.table}'`,
|
|
1389
|
+
table: table.table
|
|
1390
|
+
});
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
primaryKey.push(resolved);
|
|
1394
|
+
}
|
|
1395
|
+
const active = entries.filter((c) => !c.deprecated);
|
|
1396
|
+
const constraints = [];
|
|
1397
|
+
if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
|
|
1398
|
+
for (const column of table.columns) {
|
|
1399
|
+
if (column.unique) {
|
|
1400
|
+
const logical = logicalColumn(entries, column.columnName);
|
|
1401
|
+
constraints.push({ name: `${table.table}_${column.columnName}_key`, columns: [logical] });
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
for (const index of table.indexes) {
|
|
1405
|
+
if (!index.unique) continue;
|
|
1406
|
+
const logicalCols = index.columns.map((ref) => logicalColumn(entries, ref)).filter((c) => !!c);
|
|
1407
|
+
constraints.push({ name: index.name ?? `${table.table}_${index.columns.join("_")}_key`, columns: logicalCols });
|
|
1408
|
+
}
|
|
1409
|
+
tables[table.table] = {
|
|
1410
|
+
table: table.table,
|
|
1411
|
+
schema: ormSchemaName(canonical.namespace),
|
|
1412
|
+
typename: typeNameForTable(table.table),
|
|
1413
|
+
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
1414
|
+
primaryKey: primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1415
|
+
links: [],
|
|
1416
|
+
filterable: entries.map((c) => c.name),
|
|
1417
|
+
orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
1418
|
+
insertable: active.map((c) => c.name),
|
|
1419
|
+
settable: active.map((c) => c.name),
|
|
1420
|
+
appendable: active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1421
|
+
constraints,
|
|
1422
|
+
dynamicFilterMaxDepth: 6,
|
|
1423
|
+
dynamicFilterMaxNodes: 64
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
for (const table of canonical.tables) {
|
|
1427
|
+
const meta = tables[table.table];
|
|
1428
|
+
const entries = columnEntries.get(table.table);
|
|
1429
|
+
for (const link of table.links) {
|
|
1430
|
+
const targetName = link.kind === "toPlatformUser" ? PLATFORM_USER_TABLE : link.target;
|
|
1431
|
+
const target = targetName === void 0 ? void 0 : tables[targetName];
|
|
1432
|
+
if (!targetName || !target) {
|
|
1433
|
+
diagnostics.push({
|
|
1434
|
+
severity: "error",
|
|
1435
|
+
code: "unknown-link-target",
|
|
1436
|
+
message: `link '${link.name}' on table '${table.table}' targets unknown table '${targetName ?? "(none)"}'`,
|
|
1437
|
+
table: table.table
|
|
1438
|
+
});
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
1442
|
+
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
1443
|
+
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
1444
|
+
const on = logicalColumn(onEntries, link.on);
|
|
1445
|
+
const defaultReferences = link.kind === "toPlatformUser" ? "sub" : link.kind === "toMany" ? meta.primaryKey[0] : target.primaryKey[0];
|
|
1446
|
+
const references = link.references ? logicalColumn(referencesEntries, link.references) : defaultReferences;
|
|
1447
|
+
if (!on || !references) {
|
|
1448
|
+
diagnostics.push({
|
|
1449
|
+
severity: "error",
|
|
1450
|
+
code: "unknown-link-column",
|
|
1451
|
+
message: `link '${link.name}' on table '${table.table}' references unknown column '${!on ? link.on : link.references}'`,
|
|
1452
|
+
table: table.table
|
|
1453
|
+
});
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
meta.links.push({ name: link.name, kind: link.kind, target: targetName, on, references });
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const readPolicies = /* @__PURE__ */ new Map();
|
|
1460
|
+
const writePolicies = /* @__PURE__ */ new Map();
|
|
1461
|
+
const pins = /* @__PURE__ */ new Map();
|
|
1462
|
+
const resolver = { tables };
|
|
1463
|
+
for (const table of canonical.tables) {
|
|
1464
|
+
for (const [access, store] of [
|
|
1465
|
+
["read", readPolicies],
|
|
1466
|
+
["write", writePolicies]
|
|
1467
|
+
]) {
|
|
1468
|
+
const expression = table.policies[access];
|
|
1469
|
+
if (expression === void 0) {
|
|
1470
|
+
store.set(table.table, CONST_TRUE);
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
try {
|
|
1474
|
+
store.set(table.table, compilePolicy({ expression, table: table.table, resolver }));
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
if (error instanceof PolicyCompileError || error instanceof PolicyParseError) {
|
|
1477
|
+
diagnostics.push({
|
|
1478
|
+
severity: "error",
|
|
1479
|
+
code: error instanceof PolicyCompileError ? error.code : "policy-parse-error",
|
|
1480
|
+
message: `${access} policy on table '${table.table}': ${error.message}`,
|
|
1481
|
+
table: table.table
|
|
1482
|
+
});
|
|
1483
|
+
store.set(table.table, CONST_TRUE);
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
throw error;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
const tablePins = collectPinnedColumns(writePolicies.get(table.table));
|
|
1490
|
+
pins.set(table.table, tablePins);
|
|
1491
|
+
const pinned = new Set(tablePins.map((pin) => pin.column));
|
|
1492
|
+
const meta = tables[table.table];
|
|
1493
|
+
meta.insertable = meta.insertable.filter((column) => !pinned.has(column));
|
|
1494
|
+
meta.settable = meta.settable.filter((column) => !pinned.has(column));
|
|
1495
|
+
}
|
|
1496
|
+
if (platformMeta) {
|
|
1497
|
+
readPolicies.set(PLATFORM_USER_TABLE, CONST_TRUE);
|
|
1498
|
+
writePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
1499
|
+
pins.set(PLATFORM_USER_TABLE, []);
|
|
1500
|
+
}
|
|
1501
|
+
const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
|
|
1502
|
+
const queryRoots = /* @__PURE__ */ new Map();
|
|
1503
|
+
const mutationRoots = /* @__PURE__ */ new Map();
|
|
1504
|
+
for (const table of canonical.tables) {
|
|
1505
|
+
const roots = queryRootNames(table.table);
|
|
1506
|
+
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
1507
|
+
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
1508
|
+
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
1509
|
+
if (table.source === void 0) {
|
|
1510
|
+
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
1511
|
+
mutationRoots.set(field, { op, table: table.table });
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const enumsByTypeName = new Map(canonical.enums.map((e) => [enumTypeName(e.name), e]));
|
|
1516
|
+
return {
|
|
1517
|
+
ctx: {
|
|
1518
|
+
namespace: canonical.namespace,
|
|
1519
|
+
tables,
|
|
1520
|
+
tableByTypename,
|
|
1521
|
+
queryRoots,
|
|
1522
|
+
mutationRoots,
|
|
1523
|
+
enumsByTypeName,
|
|
1524
|
+
readPolicies,
|
|
1525
|
+
writePolicies,
|
|
1526
|
+
pins
|
|
1527
|
+
},
|
|
1528
|
+
diagnostics
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
1532
|
+
const selections = [];
|
|
1533
|
+
for (const selection of selectionSet.selections) {
|
|
1534
|
+
if (selection.kind === graphql.Kind.FIELD) {
|
|
1535
|
+
selections.push(
|
|
1536
|
+
selection.selectionSet ? { ...selection, selectionSet: flattenSelectionSet(selection.selectionSet, fragments, stack) } : selection
|
|
1537
|
+
);
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
|
|
1541
|
+
const name = selection.name.value;
|
|
1542
|
+
if (selection.directives && selection.directives.length > 0) {
|
|
1543
|
+
throw new DocumentCompileError(`directives on fragment spread '...${name}' are not supported`, "fragment-directive");
|
|
1544
|
+
}
|
|
1545
|
+
if (stack.includes(name)) {
|
|
1546
|
+
throw new DocumentCompileError(`fragment cycle detected through '${name}'`, "fragment-cycle");
|
|
1547
|
+
}
|
|
1548
|
+
const fragment = fragments.get(name);
|
|
1549
|
+
if (!fragment) {
|
|
1550
|
+
throw new DocumentCompileError(`unknown fragment '${name}'`, "unknown-fragment");
|
|
1551
|
+
}
|
|
1552
|
+
const inlined2 = flattenSelectionSet(fragment.selectionSet, fragments, [...stack, name]);
|
|
1553
|
+
selections.push(...inlined2.selections);
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
if (selection.directives && selection.directives.length > 0) {
|
|
1557
|
+
throw new DocumentCompileError("directives on inline fragments are not supported", "fragment-directive");
|
|
1558
|
+
}
|
|
1559
|
+
const inlined = flattenSelectionSet(selection.selectionSet, fragments, stack);
|
|
1560
|
+
selections.push(...inlined.selections);
|
|
1561
|
+
}
|
|
1562
|
+
return { ...selectionSet, selections };
|
|
1563
|
+
}
|
|
1564
|
+
function flattenOperation({
|
|
1565
|
+
operation,
|
|
1566
|
+
fragments
|
|
1567
|
+
}) {
|
|
1568
|
+
return { ...operation, selectionSet: flattenSelectionSet(operation.selectionSet, fragments, []) };
|
|
1569
|
+
}
|
|
1570
|
+
var COMPILER_MAJOR = 1;
|
|
1571
|
+
var OP_HASH_DOMAIN = "sequence-orm-op\0";
|
|
1572
|
+
var OPS_HASH_DOMAIN = "sequence-orm-opset\0";
|
|
1573
|
+
function canonicalOperationText(definition) {
|
|
1574
|
+
const document = { kind: graphql.Kind.DOCUMENT, definitions: [definition] };
|
|
1575
|
+
return graphql.print(document);
|
|
1576
|
+
}
|
|
1577
|
+
function computeOpHash({
|
|
1578
|
+
definition,
|
|
1579
|
+
schemaHash
|
|
1580
|
+
}) {
|
|
1581
|
+
const digest = crypto.createHash("sha256").update(OP_HASH_DOMAIN).update(`${COMPILER_MAJOR}\0${schemaHash}\0`).update(canonicalOperationText(definition)).digest("hex");
|
|
1582
|
+
return `sha256:${digest}`;
|
|
1583
|
+
}
|
|
1584
|
+
function computeOpsHash(opHashes) {
|
|
1585
|
+
const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
1586
|
+
return `sha256:${digest}`;
|
|
1587
|
+
}
|
|
1588
|
+
var DEFAULT_LIMIT = 100;
|
|
1589
|
+
var MAX_LIMIT = 1e3;
|
|
1590
|
+
var BOOLEXP_OPS = {
|
|
1591
|
+
_eq: "eq",
|
|
1592
|
+
_neq: "neq",
|
|
1593
|
+
_gt: "gt",
|
|
1594
|
+
_gte: "gte",
|
|
1595
|
+
_lt: "lt",
|
|
1596
|
+
_lte: "lte",
|
|
1597
|
+
_in: "in",
|
|
1598
|
+
_nin: "nin",
|
|
1599
|
+
_like: "like",
|
|
1600
|
+
_ilike: "ilike",
|
|
1601
|
+
_isNull: "isNull",
|
|
1602
|
+
_contains: "contains",
|
|
1603
|
+
_hasKey: "hasKey"
|
|
1604
|
+
};
|
|
1605
|
+
var AGG_PREDICATE_OPS = ["_eq", "_neq", "_gt", "_gte", "_lt", "_lte"];
|
|
1606
|
+
var ORDER_DIRECTIONS = {
|
|
1607
|
+
ASC: { direction: "asc" },
|
|
1608
|
+
DESC: { direction: "desc" },
|
|
1609
|
+
ASC_NULLS_FIRST: { direction: "asc", nulls: "first" },
|
|
1610
|
+
ASC_NULLS_LAST: { direction: "asc", nulls: "last" },
|
|
1611
|
+
DESC_NULLS_FIRST: { direction: "desc", nulls: "first" },
|
|
1612
|
+
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
1613
|
+
};
|
|
1614
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
|
|
1615
|
+
function fail(message, code, candidates = []) {
|
|
1616
|
+
throw new DocumentCompileError(message, code, candidates);
|
|
1617
|
+
}
|
|
1618
|
+
function argsByName(field) {
|
|
1619
|
+
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
1620
|
+
}
|
|
1621
|
+
function assertKnownArgs(field, allowed) {
|
|
1622
|
+
for (const arg of field.arguments ?? []) {
|
|
1623
|
+
if (!allowed.includes(arg.name.value)) {
|
|
1624
|
+
fail(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
var OperationLowerer = class {
|
|
1629
|
+
constructor(ctx, restricted) {
|
|
1630
|
+
this.ctx = ctx;
|
|
1631
|
+
this.variables = new Map(restricted.variables.map((v) => [v.name, v]));
|
|
1632
|
+
}
|
|
1633
|
+
variables;
|
|
1634
|
+
meta(table) {
|
|
1635
|
+
return this.ctx.tables[table];
|
|
1636
|
+
}
|
|
1637
|
+
column(table, name) {
|
|
1638
|
+
return this.meta(table).columns.find((c) => c.name === name);
|
|
1639
|
+
}
|
|
1640
|
+
link(table, name) {
|
|
1641
|
+
return this.meta(table).links.find((l) => l.name === name);
|
|
1642
|
+
}
|
|
1643
|
+
fieldCandidates(table) {
|
|
1644
|
+
const meta = this.meta(table);
|
|
1645
|
+
return [
|
|
1646
|
+
...meta.columns.map((c) => c.name),
|
|
1647
|
+
...meta.links.map((l) => l.name),
|
|
1648
|
+
...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
|
|
1649
|
+
];
|
|
1650
|
+
}
|
|
1651
|
+
/** Validate that a whole-variable argument is the declared §4f slot for this table. */
|
|
1652
|
+
slot(name, input, table) {
|
|
1653
|
+
const variable = this.variables.get(name);
|
|
1654
|
+
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
1655
|
+
fail(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
1656
|
+
}
|
|
1657
|
+
return name;
|
|
1658
|
+
}
|
|
1659
|
+
// -------------------------------------------------------------------------
|
|
1660
|
+
// Values
|
|
1661
|
+
// -------------------------------------------------------------------------
|
|
1662
|
+
lowerValue(node) {
|
|
1663
|
+
switch (node.kind) {
|
|
1664
|
+
case graphql.Kind.VARIABLE:
|
|
1665
|
+
return { kind: "variable", name: node.name.value };
|
|
1666
|
+
case graphql.Kind.INT:
|
|
1667
|
+
case graphql.Kind.FLOAT:
|
|
1668
|
+
return { kind: "literal", value: Number(node.value) };
|
|
1669
|
+
case graphql.Kind.STRING:
|
|
1670
|
+
case graphql.Kind.ENUM:
|
|
1671
|
+
return { kind: "literal", value: node.value };
|
|
1672
|
+
case graphql.Kind.BOOLEAN:
|
|
1673
|
+
return { kind: "literal", value: node.value };
|
|
1674
|
+
case graphql.Kind.NULL:
|
|
1675
|
+
return { kind: "literal", value: null };
|
|
1676
|
+
case graphql.Kind.LIST: {
|
|
1677
|
+
const items = node.values.map((item) => {
|
|
1678
|
+
const lowered = this.lowerValue(item);
|
|
1679
|
+
if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
|
|
1680
|
+
fail("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
|
|
1681
|
+
}
|
|
1682
|
+
return lowered.value;
|
|
1683
|
+
});
|
|
1684
|
+
return { kind: "literal", value: items };
|
|
1685
|
+
}
|
|
1686
|
+
case graphql.Kind.OBJECT:
|
|
1687
|
+
fail("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
// -------------------------------------------------------------------------
|
|
1691
|
+
// BoolExp / orderBy / distinctOn / paging
|
|
1692
|
+
// -------------------------------------------------------------------------
|
|
1693
|
+
lowerBoolExp(node, table) {
|
|
1694
|
+
if (node.kind === graphql.Kind.VARIABLE) {
|
|
1695
|
+
return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
|
|
1696
|
+
}
|
|
1697
|
+
if (node.kind !== graphql.Kind.OBJECT) fail(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
|
|
1698
|
+
const operands = [];
|
|
1699
|
+
for (const field of node.fields) {
|
|
1700
|
+
const key = field.name.value;
|
|
1701
|
+
if (key === "_and" || key === "_or") {
|
|
1702
|
+
const children = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
|
|
1703
|
+
const lowered = children.map((child) => this.lowerBoolExp(child, table));
|
|
1704
|
+
if (lowered.length > 0) operands.push(lowered.length === 1 ? lowered[0] : { kind: key === "_and" ? "and" : "or", operands: lowered });
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
if (key === "_not") {
|
|
1708
|
+
operands.push({ kind: "not", operand: this.lowerBoolExp(field.value, table) });
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const column = this.column(table, key);
|
|
1712
|
+
if (column) {
|
|
1713
|
+
operands.push(...this.lowerComparison(field.value, key, table));
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
const link = this.link(table, key);
|
|
1717
|
+
if (link) {
|
|
1718
|
+
operands.push({ kind: "exists", link: link.name, where: this.lowerBoolExp(field.value, link.target) });
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (key.endsWith("Aggregate")) {
|
|
1722
|
+
const aggLink = this.link(table, key.slice(0, -"Aggregate".length));
|
|
1723
|
+
if (aggLink && aggLink.kind === "toMany") {
|
|
1724
|
+
operands.push(this.lowerAggPredicate(field.value, aggLink.name, aggLink.target));
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
fail(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
|
|
1729
|
+
}
|
|
1730
|
+
if (operands.length === 0) return CONST_TRUE;
|
|
1731
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
1732
|
+
}
|
|
1733
|
+
lowerComparison(node, column, table) {
|
|
1734
|
+
if (node.kind !== graphql.Kind.OBJECT) {
|
|
1735
|
+
fail(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
|
|
1736
|
+
}
|
|
1737
|
+
return node.fields.map((field) => {
|
|
1738
|
+
const op = BOOLEXP_OPS[field.name.value];
|
|
1739
|
+
if (!op) {
|
|
1740
|
+
fail(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
|
|
1741
|
+
}
|
|
1742
|
+
return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
/** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
|
|
1746
|
+
lowerAggPredicate(node, link, target) {
|
|
1747
|
+
if (node.kind !== graphql.Kind.OBJECT) fail(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
1748
|
+
const countField = node.fields.find((f) => f.name.value === "count");
|
|
1749
|
+
if (!countField || node.fields.length !== 1) {
|
|
1750
|
+
fail(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
|
|
1751
|
+
}
|
|
1752
|
+
if (countField.value.kind !== graphql.Kind.OBJECT) fail(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
1753
|
+
let predicate;
|
|
1754
|
+
let where;
|
|
1755
|
+
for (const field of countField.value.fields) {
|
|
1756
|
+
if (field.name.value === "predicate") {
|
|
1757
|
+
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
1758
|
+
fail(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
|
|
1759
|
+
}
|
|
1760
|
+
const opField = field.value.fields[0];
|
|
1761
|
+
const opName = opField.name.value;
|
|
1762
|
+
if (!AGG_PREDICATE_OPS.includes(opName)) {
|
|
1763
|
+
fail(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
|
|
1764
|
+
}
|
|
1765
|
+
if (opField.value.kind !== graphql.Kind.INT) {
|
|
1766
|
+
fail(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
|
|
1767
|
+
}
|
|
1768
|
+
predicate = { op: opName.slice(1), value: Number(opField.value.value) };
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
if (field.name.value === "filter") {
|
|
1772
|
+
where = this.lowerBoolExp(field.value, target);
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
fail(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
|
|
1776
|
+
}
|
|
1777
|
+
if (!predicate) fail(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
|
|
1778
|
+
return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
|
|
1779
|
+
}
|
|
1780
|
+
lowerOrderBy(node, table) {
|
|
1781
|
+
if (node.kind === graphql.Kind.VARIABLE) {
|
|
1782
|
+
return [{ kind: "dynamic", slot: this.slot(node.name.value, "orderBy", table) }];
|
|
1783
|
+
}
|
|
1784
|
+
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
1785
|
+
const lowered = [];
|
|
1786
|
+
for (const item of items) {
|
|
1787
|
+
if (item.kind !== graphql.Kind.OBJECT) fail(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
|
|
1788
|
+
for (const field of item.fields) {
|
|
1789
|
+
const key = field.name.value;
|
|
1790
|
+
const column = this.column(table, key);
|
|
1791
|
+
if (column) {
|
|
1792
|
+
lowered.push({ kind: "column", column: key, ...this.lowerDirection(field.value, table, key) });
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const link = this.link(table, key);
|
|
1796
|
+
if (link && link.kind !== "toMany") {
|
|
1797
|
+
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
1798
|
+
fail(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
|
|
1799
|
+
}
|
|
1800
|
+
const inner = field.value.fields[0];
|
|
1801
|
+
const targetColumn = this.column(link.target, inner.name.value);
|
|
1802
|
+
if (!targetColumn) {
|
|
1803
|
+
fail(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
|
|
1804
|
+
}
|
|
1805
|
+
lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
fail(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return lowered;
|
|
1812
|
+
}
|
|
1813
|
+
lowerDirection(node, table, column) {
|
|
1814
|
+
if (node.kind !== graphql.Kind.ENUM) fail(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
1815
|
+
const direction = ORDER_DIRECTIONS[node.value];
|
|
1816
|
+
if (!direction) fail(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
1817
|
+
return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
|
|
1818
|
+
}
|
|
1819
|
+
lowerDistinctOn(node, table) {
|
|
1820
|
+
if (node.kind === graphql.Kind.VARIABLE) {
|
|
1821
|
+
return { slot: this.slot(node.name.value, "selectColumn", table) };
|
|
1822
|
+
}
|
|
1823
|
+
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
1824
|
+
return items.map((item) => {
|
|
1825
|
+
if (item.kind !== graphql.Kind.ENUM) fail(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
|
|
1826
|
+
if (!this.column(table, item.value)) {
|
|
1827
|
+
fail(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
1828
|
+
}
|
|
1829
|
+
return item.value;
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
lowerLimit(node) {
|
|
1833
|
+
const base = { default: DEFAULT_LIMIT, max: MAX_LIMIT };
|
|
1834
|
+
if (!node) return base;
|
|
1835
|
+
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
1836
|
+
if (node.kind === graphql.Kind.INT) {
|
|
1837
|
+
const value = Number(node.value);
|
|
1838
|
+
if (value < 1) fail("limit must be a positive integer", "invalid-limit");
|
|
1839
|
+
return { ...base, value: Math.min(value, MAX_LIMIT) };
|
|
1840
|
+
}
|
|
1841
|
+
fail("limit must be an integer literal or a variable", "invalid-limit");
|
|
1842
|
+
}
|
|
1843
|
+
lowerOffset(node) {
|
|
1844
|
+
if (!node) return void 0;
|
|
1845
|
+
if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
|
|
1846
|
+
if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
|
|
1847
|
+
fail("offset must be an integer literal or a variable", "invalid-offset");
|
|
1848
|
+
}
|
|
1849
|
+
// -------------------------------------------------------------------------
|
|
1850
|
+
// Selections
|
|
1851
|
+
// -------------------------------------------------------------------------
|
|
1852
|
+
lowerSelection(selectionSet, table) {
|
|
1853
|
+
const meta = this.meta(table);
|
|
1854
|
+
const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
|
|
1855
|
+
for (const node of selectionSet.selections) {
|
|
1856
|
+
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
1857
|
+
if (node.directives && node.directives.length > 0) {
|
|
1858
|
+
fail(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
1859
|
+
}
|
|
1860
|
+
const name = node.name.value;
|
|
1861
|
+
if (name === "__typename") continue;
|
|
1862
|
+
const alias = node.alias?.value ?? name;
|
|
1863
|
+
const column = this.column(table, name);
|
|
1864
|
+
if (column) {
|
|
1865
|
+
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
1866
|
+
fail(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
|
|
1867
|
+
}
|
|
1868
|
+
selection.columns.push({ alias, column: name });
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
const link = this.link(table, name);
|
|
1872
|
+
if (link) {
|
|
1873
|
+
if (!node.selectionSet) fail(`link '${table}.${name}' needs a selection set`, "invalid-selection");
|
|
1874
|
+
const relation = {
|
|
1875
|
+
alias,
|
|
1876
|
+
link: name,
|
|
1877
|
+
selection: this.lowerSelection(node.selectionSet, link.target)
|
|
1878
|
+
};
|
|
1879
|
+
if (link.kind === "toMany") {
|
|
1880
|
+
assertKnownArgs(node, ["where", "orderBy", "limit", "offset"]);
|
|
1881
|
+
const args = argsByName(node);
|
|
1882
|
+
const where = args.get("where");
|
|
1883
|
+
if (where) relation.where = this.lowerBoolExp(where.value, link.target);
|
|
1884
|
+
const orderBy = args.get("orderBy");
|
|
1885
|
+
if (orderBy) relation.orderBy = this.lowerOrderBy(orderBy.value, link.target);
|
|
1886
|
+
if (args.get("limit") ?? args.get("offset")) {
|
|
1887
|
+
relation.limit = this.lowerLimit(args.get("limit")?.value);
|
|
1888
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
1889
|
+
if (offset) relation.offset = offset;
|
|
1890
|
+
}
|
|
1891
|
+
} else if (node.arguments && node.arguments.length > 0) {
|
|
1892
|
+
fail(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
|
|
1893
|
+
}
|
|
1894
|
+
selection.relations.push(relation);
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (name.endsWith("Aggregate")) {
|
|
1898
|
+
const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
|
|
1899
|
+
if (aggLink && aggLink.kind === "toMany") {
|
|
1900
|
+
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
1901
|
+
assertKnownArgs(node, ["where"]);
|
|
1902
|
+
const whereArg = argsByName(node).get("where");
|
|
1903
|
+
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
1904
|
+
selection.aggregates.push({
|
|
1905
|
+
alias,
|
|
1906
|
+
link: aggLink.name,
|
|
1907
|
+
...whereArg ? { where: this.lowerBoolExp(whereArg.value, aggLink.target) } : {},
|
|
1908
|
+
...aggregate
|
|
1909
|
+
});
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
fail(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
1914
|
+
}
|
|
1915
|
+
return selection;
|
|
1916
|
+
}
|
|
1917
|
+
/** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
|
|
1918
|
+
lowerAggregateSelection(selectionSet, table) {
|
|
1919
|
+
const result = {};
|
|
1920
|
+
let sawAggregate = false;
|
|
1921
|
+
for (const node of selectionSet.selections) {
|
|
1922
|
+
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
1923
|
+
const name = node.name.value;
|
|
1924
|
+
if (name === "__typename") continue;
|
|
1925
|
+
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
1926
|
+
if (name === "nodes") {
|
|
1927
|
+
if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
|
|
1928
|
+
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
if (name !== "aggregate") fail(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
|
|
1932
|
+
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
1933
|
+
sawAggregate = true;
|
|
1934
|
+
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
1935
|
+
for (const aggField of node.selectionSet.selections) {
|
|
1936
|
+
if (aggField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
1937
|
+
const aggName = aggField.name.value;
|
|
1938
|
+
if (aggName === "__typename") continue;
|
|
1939
|
+
if (aggField.alias) fail(`aliases are not supported inside aggregate blocks ('${aggField.alias.value}')`, "invalid-aggregate");
|
|
1940
|
+
if (aggName === "count") {
|
|
1941
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
1942
|
+
const args = argsByName(aggField);
|
|
1943
|
+
const count = {};
|
|
1944
|
+
const columns = args.get("columns");
|
|
1945
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
1946
|
+
const distinct = args.get("distinct");
|
|
1947
|
+
if (distinct) {
|
|
1948
|
+
if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
1949
|
+
count.distinct = distinct.value.value;
|
|
1950
|
+
}
|
|
1951
|
+
result.count = count;
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
1955
|
+
if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
1956
|
+
const columns = [];
|
|
1957
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
1958
|
+
if (columnField.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
1959
|
+
if (columnField.name.value === "__typename") continue;
|
|
1960
|
+
const column = this.column(table, columnField.name.value);
|
|
1961
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
1962
|
+
fail(
|
|
1963
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
1964
|
+
"invalid-aggregate",
|
|
1965
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
columns.push(columnField.name.value);
|
|
1969
|
+
}
|
|
1970
|
+
result[aggName] = columns;
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
1977
|
+
return result;
|
|
1978
|
+
}
|
|
1979
|
+
// -------------------------------------------------------------------------
|
|
1980
|
+
// Roots
|
|
1981
|
+
// -------------------------------------------------------------------------
|
|
1982
|
+
lowerRoots(operation, kind) {
|
|
1983
|
+
const roots = [];
|
|
1984
|
+
for (const node of operation.selectionSet.selections) {
|
|
1985
|
+
if (node.kind !== graphql.Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
|
|
1986
|
+
const gate = this.parseRootDirectives(node);
|
|
1987
|
+
if (gate.omit) continue;
|
|
1988
|
+
const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
1989
|
+
roots.push(gate.includeIf ? { ...root, includeIf: gate.includeIf } : root);
|
|
1990
|
+
}
|
|
1991
|
+
if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
|
|
1992
|
+
return roots;
|
|
1993
|
+
}
|
|
1994
|
+
parseRootDirectives(node) {
|
|
1995
|
+
let includeIf;
|
|
1996
|
+
let omit = false;
|
|
1997
|
+
for (const directive of node.directives ?? []) {
|
|
1998
|
+
const name = directive.name.value;
|
|
1999
|
+
if (name !== "include" && name !== "skip") {
|
|
2000
|
+
fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
2001
|
+
}
|
|
2002
|
+
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
2003
|
+
if (!ifArg) fail(`@${name} needs an if: argument`, "invalid-directive");
|
|
2004
|
+
if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
|
|
2005
|
+
const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
2006
|
+
if (!keep) omit = true;
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
|
|
2010
|
+
if (includeIf) fail("at most one of @include/@skip per root field", "invalid-directive");
|
|
2011
|
+
includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
|
|
2012
|
+
}
|
|
2013
|
+
return omit ? { omit: true } : { includeIf, omit: false };
|
|
2014
|
+
}
|
|
2015
|
+
lowerQueryRoot(node) {
|
|
2016
|
+
const name = node.name.value;
|
|
2017
|
+
const alias = node.alias?.value ?? name;
|
|
2018
|
+
const rootInfo = this.ctx.queryRoots.get(name);
|
|
2019
|
+
if (!rootInfo) fail(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
2020
|
+
const { table } = rootInfo;
|
|
2021
|
+
const args = argsByName(node);
|
|
2022
|
+
if (rootInfo.kind === "select") {
|
|
2023
|
+
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
2024
|
+
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
2025
|
+
const where = args.get("where");
|
|
2026
|
+
const orderBy = args.get("orderBy");
|
|
2027
|
+
const distinctOn = args.get("distinctOn");
|
|
2028
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
2029
|
+
const distinct = distinctOn ? this.lowerDistinctOn(distinctOn.value, table) : void 0;
|
|
2030
|
+
return {
|
|
2031
|
+
kind: "select",
|
|
2032
|
+
alias,
|
|
2033
|
+
table,
|
|
2034
|
+
selection: this.lowerSelection(node.selectionSet, table),
|
|
2035
|
+
where: where ? this.lowerBoolExp(where.value, table) : CONST_TRUE,
|
|
2036
|
+
orderBy: orderBy ? this.lowerOrderBy(orderBy.value, table) : [],
|
|
2037
|
+
...distinct ? { distinctOn: distinct } : {},
|
|
2038
|
+
limit: this.lowerLimit(args.get("limit")?.value),
|
|
2039
|
+
...offset ? { offset } : {}
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
if (rootInfo.kind === "byPk") {
|
|
2043
|
+
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
2044
|
+
const pk = this.lowerPkArgs(node, table);
|
|
2045
|
+
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
2046
|
+
}
|
|
2047
|
+
assertKnownArgs(node, ["where"]);
|
|
2048
|
+
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
2049
|
+
const whereArg = args.get("where");
|
|
2050
|
+
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
2051
|
+
return {
|
|
2052
|
+
kind: "aggregate",
|
|
2053
|
+
alias,
|
|
2054
|
+
table,
|
|
2055
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
2056
|
+
...aggregate
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
2060
|
+
lowerPkArgs(node, table) {
|
|
2061
|
+
const meta = this.meta(table);
|
|
2062
|
+
const args = argsByName(node);
|
|
2063
|
+
assertKnownArgs(node, meta.primaryKey);
|
|
2064
|
+
return meta.primaryKey.map((column) => {
|
|
2065
|
+
const arg = args.get(column);
|
|
2066
|
+
if (!arg) fail(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
|
|
2067
|
+
return { column, value: this.lowerValue(arg.value) };
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
// -------------------------------------------------------------------------
|
|
2071
|
+
// Mutations
|
|
2072
|
+
// -------------------------------------------------------------------------
|
|
2073
|
+
lowerMutationRoot(node) {
|
|
2074
|
+
const name = node.name.value;
|
|
2075
|
+
const alias = node.alias?.value ?? name;
|
|
2076
|
+
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
2077
|
+
if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
2078
|
+
const { op, table } = rootInfo;
|
|
2079
|
+
const args = argsByName(node);
|
|
2080
|
+
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
2081
|
+
const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
2082
|
+
switch (op) {
|
|
2083
|
+
case "insert": {
|
|
2084
|
+
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
2085
|
+
const objects = args.get("objects");
|
|
2086
|
+
if (!objects) fail(`'${name}' needs objects:`, "missing-argument");
|
|
2087
|
+
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
2088
|
+
return {
|
|
2089
|
+
...base,
|
|
2090
|
+
...response,
|
|
2091
|
+
objects: this.lowerInsertObjects(objects.value, table, { list: true }),
|
|
2092
|
+
...this.lowerOnConflictArg(args.get("onConflict"), table)
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
case "insertOne": {
|
|
2096
|
+
assertKnownArgs(node, ["object", "onConflict"]);
|
|
2097
|
+
const object = args.get("object");
|
|
2098
|
+
if (!object) fail(`'${name}' needs object:`, "missing-argument");
|
|
2099
|
+
return {
|
|
2100
|
+
...base,
|
|
2101
|
+
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
2102
|
+
...this.lowerOnConflictArg(args.get("onConflict"), table),
|
|
2103
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
case "update": {
|
|
2107
|
+
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
2108
|
+
const where = args.get("where");
|
|
2109
|
+
if (!where) fail(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
2110
|
+
return {
|
|
2111
|
+
...base,
|
|
2112
|
+
...this.lowerMutationResponse(node.selectionSet, table),
|
|
2113
|
+
where: this.lowerBoolExp(where.value, table),
|
|
2114
|
+
...this.lowerWriteValues(args, table)
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
case "updateByPk": {
|
|
2118
|
+
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
2119
|
+
const pkColumns = args.get("pkColumns");
|
|
2120
|
+
if (!pkColumns) fail(`'${name}' needs pkColumns:`, "missing-argument");
|
|
2121
|
+
return {
|
|
2122
|
+
...base,
|
|
2123
|
+
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
2124
|
+
...this.lowerWriteValues(args, table),
|
|
2125
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
case "delete": {
|
|
2129
|
+
assertKnownArgs(node, ["where"]);
|
|
2130
|
+
const where = args.get("where");
|
|
2131
|
+
if (!where) fail(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
2132
|
+
return {
|
|
2133
|
+
...base,
|
|
2134
|
+
...this.lowerMutationResponse(node.selectionSet, table),
|
|
2135
|
+
where: this.lowerBoolExp(where.value, table)
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
case "deleteByPk": {
|
|
2139
|
+
return {
|
|
2140
|
+
...base,
|
|
2141
|
+
pk: this.lowerPkArgs(node, table),
|
|
2142
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
lowerMutationResponse(selectionSet, table) {
|
|
2148
|
+
let affectedRows = false;
|
|
2149
|
+
let returning = null;
|
|
2150
|
+
for (const node of selectionSet.selections) {
|
|
2151
|
+
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2152
|
+
const name = node.name.value;
|
|
2153
|
+
if (name === "__typename") continue;
|
|
2154
|
+
if (name === "affectedRows") {
|
|
2155
|
+
affectedRows = true;
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
if (name === "returning") {
|
|
2159
|
+
if (!node.selectionSet) fail("'returning' needs a selection set", "invalid-selection");
|
|
2160
|
+
returning = this.lowerSelection(node.selectionSet, table);
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
fail(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
2164
|
+
}
|
|
2165
|
+
return { affectedRows, returning };
|
|
2166
|
+
}
|
|
2167
|
+
lowerInsertObjects(node, table, { list }) {
|
|
2168
|
+
if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
2169
|
+
if (list) {
|
|
2170
|
+
if (node.kind !== graphql.Kind.LIST) fail(`objects: must be a list literal or a variable`, "invalid-insert");
|
|
2171
|
+
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
2172
|
+
}
|
|
2173
|
+
return { rows: [this.lowerInsertRow(node, table)] };
|
|
2174
|
+
}
|
|
2175
|
+
lowerInsertRow(node, table) {
|
|
2176
|
+
if (node.kind !== graphql.Kind.OBJECT) fail(`insert rows on '${table}' must be objects`, "invalid-insert");
|
|
2177
|
+
const meta = this.meta(table);
|
|
2178
|
+
return node.fields.map((field) => {
|
|
2179
|
+
const key = field.name.value;
|
|
2180
|
+
if (this.link(table, key)) {
|
|
2181
|
+
fail(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
|
|
2182
|
+
}
|
|
2183
|
+
if (!meta.insertable.includes(key)) {
|
|
2184
|
+
const column = this.column(table, key);
|
|
2185
|
+
if (column) fail(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
|
|
2186
|
+
fail(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
|
|
2187
|
+
}
|
|
2188
|
+
return { column: key, value: this.lowerValue(field.value) };
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
lowerOnConflictArg(arg, table) {
|
|
2192
|
+
if (!arg) return {};
|
|
2193
|
+
const node = arg.value;
|
|
2194
|
+
if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
|
|
2195
|
+
if (node.kind !== graphql.Kind.OBJECT) fail("onConflict: must be an object literal or a variable", "invalid-on-conflict");
|
|
2196
|
+
const meta = this.meta(table);
|
|
2197
|
+
let constraint;
|
|
2198
|
+
const updateColumns = [];
|
|
2199
|
+
for (const field of node.fields) {
|
|
2200
|
+
if (field.name.value === "constraint") {
|
|
2201
|
+
if (field.value.kind !== graphql.Kind.ENUM) fail("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
|
|
2202
|
+
constraint = field.value.value;
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
if (field.name.value === "updateColumns") {
|
|
2206
|
+
const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
|
|
2207
|
+
for (const item of items) {
|
|
2208
|
+
if (item.kind !== graphql.Kind.ENUM) fail("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
|
|
2209
|
+
if (!meta.settable.includes(item.value)) {
|
|
2210
|
+
fail(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
|
|
2211
|
+
}
|
|
2212
|
+
updateColumns.push(item.value);
|
|
2213
|
+
}
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
fail(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
|
|
2217
|
+
}
|
|
2218
|
+
if (!constraint) fail("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
2219
|
+
if (!meta.constraints.some((c) => c.name === constraint)) {
|
|
2220
|
+
fail(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
2221
|
+
}
|
|
2222
|
+
return { onConflict: { constraint, updateColumns } };
|
|
2223
|
+
}
|
|
2224
|
+
lowerWriteValues(args, table) {
|
|
2225
|
+
const meta = this.meta(table);
|
|
2226
|
+
const result = {};
|
|
2227
|
+
const set = args.get("_set");
|
|
2228
|
+
if (set) {
|
|
2229
|
+
result.set = set.value.kind === graphql.Kind.VARIABLE ? { slot: this.slot(set.value.name.value, "setInput", table) } : { entries: this.lowerColumnEntries(set.value, table, meta.settable, "_set") };
|
|
2230
|
+
}
|
|
2231
|
+
const append = args.get("_append");
|
|
2232
|
+
if (append) {
|
|
2233
|
+
result.append = append.value.kind === graphql.Kind.VARIABLE ? { slot: this.slot(append.value.name.value, "appendInput", table) } : { entries: this.lowerColumnEntries(append.value, table, meta.appendable, "_append") };
|
|
2234
|
+
}
|
|
2235
|
+
const inc = args.get("_inc");
|
|
2236
|
+
if (inc) {
|
|
2237
|
+
if (inc.value.kind === graphql.Kind.VARIABLE) {
|
|
2238
|
+
fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
2239
|
+
}
|
|
2240
|
+
const numeric = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
|
|
2241
|
+
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric, "_inc") };
|
|
2242
|
+
}
|
|
2243
|
+
if (!result.set && !result.append && !result.inc) {
|
|
2244
|
+
fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
2245
|
+
}
|
|
2246
|
+
return result;
|
|
2247
|
+
}
|
|
2248
|
+
lowerColumnEntries(node, table, whitelist, argName) {
|
|
2249
|
+
if (node.kind !== graphql.Kind.OBJECT) fail(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
|
|
2250
|
+
return node.fields.map((field) => {
|
|
2251
|
+
const key = field.name.value;
|
|
2252
|
+
if (!whitelist.includes(key)) {
|
|
2253
|
+
fail(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
|
|
2254
|
+
}
|
|
2255
|
+
return { column: key, value: this.lowerValue(field.value) };
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
lowerPkColumnsArg(node, table) {
|
|
2259
|
+
if (node.kind !== graphql.Kind.OBJECT) fail(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
|
|
2260
|
+
const meta = this.meta(table);
|
|
2261
|
+
const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
|
|
2262
|
+
for (const key of provided.keys()) {
|
|
2263
|
+
if (!meta.primaryKey.includes(key)) fail(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
|
|
2264
|
+
}
|
|
2265
|
+
return meta.primaryKey.map((column) => {
|
|
2266
|
+
const value = provided.get(column);
|
|
2267
|
+
if (!value) fail(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
|
|
2268
|
+
return { column, value: this.lowerValue(value) };
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
function lowerOperation({ restricted, ctx }) {
|
|
2273
|
+
const lowerer = new OperationLowerer(ctx, restricted);
|
|
2274
|
+
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
2275
|
+
}
|
|
2276
|
+
function parseSdl(sdl) {
|
|
2277
|
+
return graphql.buildSchema(sdl);
|
|
2278
|
+
}
|
|
2279
|
+
function parseDocuments({
|
|
2280
|
+
schema,
|
|
2281
|
+
documents
|
|
2282
|
+
}) {
|
|
2283
|
+
const parsed = [];
|
|
2284
|
+
const diagnostics = [];
|
|
2285
|
+
for (const source of documents) {
|
|
2286
|
+
let document;
|
|
2287
|
+
try {
|
|
2288
|
+
document = graphql.parse(source.contents);
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
diagnostics.push({
|
|
2291
|
+
severity: "error",
|
|
2292
|
+
code: "graphql-syntax-error",
|
|
2293
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2294
|
+
document: source.path
|
|
2295
|
+
});
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
const validationErrors = graphql.validate(schema, document);
|
|
2299
|
+
if (validationErrors.length > 0) {
|
|
2300
|
+
for (const validationError of validationErrors) {
|
|
2301
|
+
diagnostics.push({
|
|
2302
|
+
severity: "error",
|
|
2303
|
+
code: "graphql-validation-error",
|
|
2304
|
+
message: validationError.message,
|
|
2305
|
+
document: source.path
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
const operations = [];
|
|
2311
|
+
const fragments = /* @__PURE__ */ new Map();
|
|
2312
|
+
for (const definition of document.definitions) {
|
|
2313
|
+
if (definition.kind === graphql.Kind.OPERATION_DEFINITION) operations.push(definition);
|
|
2314
|
+
if (definition.kind === graphql.Kind.FRAGMENT_DEFINITION) fragments.set(definition.name.value, definition);
|
|
2315
|
+
}
|
|
2316
|
+
parsed.push({ path: source.path, document, operations, fragments });
|
|
2317
|
+
}
|
|
2318
|
+
return { documents: parsed, diagnostics };
|
|
2319
|
+
}
|
|
2320
|
+
function unwrapType(node, variable) {
|
|
2321
|
+
let nullable = true;
|
|
2322
|
+
let list = false;
|
|
2323
|
+
let current = node;
|
|
2324
|
+
for (; ; ) {
|
|
2325
|
+
if (current.kind === graphql.Kind.NON_NULL_TYPE) {
|
|
2326
|
+
if (current === node) nullable = false;
|
|
2327
|
+
current = current.type;
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
if (current.kind === graphql.Kind.LIST_TYPE) {
|
|
2331
|
+
if (list) {
|
|
2332
|
+
throw new DocumentCompileError(`variable $${variable}: nested list types are not supported`, "unsupported-variable-type");
|
|
2333
|
+
}
|
|
2334
|
+
list = true;
|
|
2335
|
+
current = current.type;
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
return { named: current.name.value, list, nullable };
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
function classifyVariableType({ named, list }, variable, ctx) {
|
|
2342
|
+
const inputRef = classifyInputType(named, ctx.tableByTypename);
|
|
2343
|
+
if (inputRef) {
|
|
2344
|
+
const listable = {
|
|
2345
|
+
boolExp: "never",
|
|
2346
|
+
onConflict: "never",
|
|
2347
|
+
setInput: "never",
|
|
2348
|
+
appendInput: "never",
|
|
2349
|
+
orderBy: "must",
|
|
2350
|
+
selectColumn: "must",
|
|
2351
|
+
insertInput: "either"
|
|
2352
|
+
};
|
|
2353
|
+
const rule = listable[inputRef.input];
|
|
2354
|
+
if (rule === "must" && !list) {
|
|
2355
|
+
throw new DocumentCompileError(
|
|
2356
|
+
`variable $${variable}: ${named} must be used as a list (e.g. [${named}!])`,
|
|
2357
|
+
"unsupported-variable-type"
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
if (rule === "never" && list) {
|
|
2361
|
+
throw new DocumentCompileError(
|
|
2362
|
+
`variable $${variable}: ${named} cannot be used as a list`,
|
|
2363
|
+
"unsupported-variable-type"
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
return { kind: "inputRef", input: inputRef.input, table: inputRef.table };
|
|
2367
|
+
}
|
|
2368
|
+
const scalar = columnTypeForScalar(named);
|
|
2369
|
+
if (scalar) return { kind: "scalar", scalar };
|
|
2370
|
+
const enumDef = ctx.enumsByTypeName.get(named);
|
|
2371
|
+
if (enumDef) return { kind: "enum", name: enumDef.name, values: [...enumDef.values] };
|
|
2372
|
+
throw new DocumentCompileError(
|
|
2373
|
+
`variable $${variable}: type '${named}' is not usable as a variable \u2014 allowed: scalars (${VARIABLE_SCALAR_NAMES.join(", ")}), declared enums, or the generated dynamic input types (FooBoolExp, [FooOrderBy!], [FooSelectColumn!], FooInsertInput, FooSetInput, FooAppendInput, FooOnConflict)`,
|
|
2374
|
+
"unsupported-variable-type"
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
function restrictDefaultLiteral(item, variable, type) {
|
|
2378
|
+
if (item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
|
|
2379
|
+
throw new DocumentCompileError(
|
|
2380
|
+
`variable $${variable}: default values must be scalar literals \u2014 got ${JSON.stringify(item) ?? "undefined"}`,
|
|
2381
|
+
"unsupported-variable-default"
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
if (type.kind === "enum" && !(typeof item === "string" && type.values.includes(item))) {
|
|
2385
|
+
throw new DocumentCompileError(
|
|
2386
|
+
`variable $${variable}: default ${JSON.stringify(item)} is not a value of enum ${type.name}`,
|
|
2387
|
+
"unsupported-variable-default",
|
|
2388
|
+
type.values
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
return item;
|
|
2392
|
+
}
|
|
2393
|
+
function restrictDefault({
|
|
2394
|
+
node,
|
|
2395
|
+
variable,
|
|
2396
|
+
type,
|
|
2397
|
+
list,
|
|
2398
|
+
nullable
|
|
2399
|
+
}) {
|
|
2400
|
+
if (type.kind === "inputRef") {
|
|
2401
|
+
throw new DocumentCompileError(
|
|
2402
|
+
`variable $${variable}: dynamic input types cannot declare a default \u2014 omit the variable instead (an absent slot prunes to a no-op)`,
|
|
2403
|
+
"unsupported-variable-default"
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
const value = graphql.valueFromASTUntyped(node);
|
|
2407
|
+
if (value === null) {
|
|
2408
|
+
if (!nullable) {
|
|
2409
|
+
throw new DocumentCompileError(
|
|
2410
|
+
`variable $${variable}: default null is not valid for a non-nullable variable`,
|
|
2411
|
+
"unsupported-variable-default"
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
return { value: null };
|
|
2415
|
+
}
|
|
2416
|
+
if (Array.isArray(value)) {
|
|
2417
|
+
if (!list) {
|
|
2418
|
+
throw new DocumentCompileError(
|
|
2419
|
+
`variable $${variable}: default for a non-list variable must not be a list`,
|
|
2420
|
+
"unsupported-variable-default"
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
return { value: value.map((item) => restrictDefaultLiteral(item, variable, type)) };
|
|
2424
|
+
}
|
|
2425
|
+
if (list) {
|
|
2426
|
+
throw new DocumentCompileError(
|
|
2427
|
+
`variable $${variable}: default for a list variable must be a list`,
|
|
2428
|
+
"unsupported-variable-default"
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
return { value: restrictDefaultLiteral(value, variable, type) };
|
|
2432
|
+
}
|
|
2433
|
+
function parseRequires(operation) {
|
|
2434
|
+
const directive = operation.directives?.find((d) => d.name.value === "requires");
|
|
2435
|
+
if (!directive) return null;
|
|
2436
|
+
const requires = {};
|
|
2437
|
+
for (const key of ["capability", "role"]) {
|
|
2438
|
+
const arg = directive.arguments?.find((a) => a.name.value === key);
|
|
2439
|
+
if (!arg) continue;
|
|
2440
|
+
if (arg.value.kind !== graphql.Kind.STRING) {
|
|
2441
|
+
throw new DocumentCompileError(`@requires(${key}:) must be a string literal`, "invalid-requires");
|
|
2442
|
+
}
|
|
2443
|
+
if (!CAPABILITY_KEY_PATTERN.test(arg.value.value)) {
|
|
2444
|
+
throw new DocumentCompileError(`@requires(${key}: "${arg.value.value}") is not a valid key`, "invalid-requires");
|
|
2445
|
+
}
|
|
2446
|
+
requires[key] = arg.value.value;
|
|
2447
|
+
}
|
|
2448
|
+
if (requires.capability === void 0 && requires.role === void 0) {
|
|
2449
|
+
throw new DocumentCompileError("@requires needs a capability or a role argument", "invalid-requires");
|
|
2450
|
+
}
|
|
2451
|
+
return requires;
|
|
2452
|
+
}
|
|
2453
|
+
function restrictOperation({
|
|
2454
|
+
operation,
|
|
2455
|
+
ctx
|
|
2456
|
+
}) {
|
|
2457
|
+
if (operation.operation === "subscription") {
|
|
2458
|
+
throw new DocumentCompileError("subscriptions are not supported in v2.0 \u2014 poll a query instead", "subscription-unsupported");
|
|
2459
|
+
}
|
|
2460
|
+
const name = operation.name?.value;
|
|
2461
|
+
if (!name) {
|
|
2462
|
+
throw new DocumentCompileError(
|
|
2463
|
+
"anonymous operations are not supported \u2014 every operation must be named (the name keys the registry)",
|
|
2464
|
+
"anonymous-operation"
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
const variables = (operation.variableDefinitions ?? []).map((definition) => {
|
|
2468
|
+
const variable = definition.variable.name.value;
|
|
2469
|
+
const unwrapped = unwrapType(definition.type, variable);
|
|
2470
|
+
const { list, nullable } = unwrapped;
|
|
2471
|
+
const type = classifyVariableType(unwrapped, variable, ctx);
|
|
2472
|
+
const ir = { name: variable, type, nullable, list };
|
|
2473
|
+
if (definition.defaultValue === void 0) return ir;
|
|
2474
|
+
return { ...ir, defaultValue: restrictDefault({ node: definition.defaultValue, variable, type, list, nullable }) };
|
|
2475
|
+
});
|
|
2476
|
+
return {
|
|
2477
|
+
name,
|
|
2478
|
+
kind: operation.operation === "mutation" ? "mutation" : "query",
|
|
2479
|
+
requires: parseRequires(operation),
|
|
2480
|
+
variables,
|
|
2481
|
+
definition: operation
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
// src/compiler-gql/artifact.ts
|
|
2486
|
+
var SCHEMA_HASH_DOMAIN = "sequence-orm-canonical-v2\0";
|
|
2487
|
+
function computeSchemaHash(canonical) {
|
|
2488
|
+
const digest = crypto.createHash("sha256").update(SCHEMA_HASH_DOMAIN).update(stableJson(canonical)).digest("hex");
|
|
2489
|
+
return `sha256:${digest}`;
|
|
2490
|
+
}
|
|
2491
|
+
function compileNamespace({ canonical, sdl, documents }) {
|
|
2492
|
+
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
2493
|
+
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
2494
|
+
const schema = parseSdl(sdl);
|
|
2495
|
+
const schemaHash = computeSchemaHash(canonical);
|
|
2496
|
+
const parsed = parseDocuments({ schema, documents });
|
|
2497
|
+
diagnostics.push(...parsed.diagnostics);
|
|
2498
|
+
const operations = {};
|
|
2499
|
+
const operationSources = /* @__PURE__ */ new Map();
|
|
2500
|
+
for (const document of parsed.documents) {
|
|
2501
|
+
for (const operationNode of document.operations) {
|
|
2502
|
+
const operationName = operationNode.name?.value;
|
|
2503
|
+
try {
|
|
2504
|
+
const restricted = restrictOperation({ operation: operationNode, ctx });
|
|
2505
|
+
const flattened = flattenOperation({ operation: restricted.definition, fragments: document.fragments });
|
|
2506
|
+
const previousSource = operationSources.get(restricted.name);
|
|
2507
|
+
if (previousSource !== void 0) {
|
|
2508
|
+
throw new DocumentCompileError(
|
|
2509
|
+
`duplicate operation name '${restricted.name}' (also defined in ${previousSource})`,
|
|
2510
|
+
"duplicate-operation"
|
|
2511
|
+
);
|
|
2512
|
+
}
|
|
2513
|
+
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
2514
|
+
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
2515
|
+
operations[restricted.name] = {
|
|
2516
|
+
irVersion: IR_VERSION,
|
|
2517
|
+
name: restricted.name,
|
|
2518
|
+
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
2519
|
+
kind: restricted.kind,
|
|
2520
|
+
requires: restricted.requires,
|
|
2521
|
+
variables: restricted.variables,
|
|
2522
|
+
tableSet,
|
|
2523
|
+
roots
|
|
2524
|
+
};
|
|
2525
|
+
operationSources.set(restricted.name, document.path);
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
if (error instanceof DocumentCompileError) {
|
|
2528
|
+
diagnostics.push({
|
|
2529
|
+
severity: "error",
|
|
2530
|
+
code: error.code,
|
|
2531
|
+
message: error.message,
|
|
2532
|
+
document: document.path,
|
|
2533
|
+
...operationName !== void 0 ? { operation: operationName } : {}
|
|
2534
|
+
});
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
throw error;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
2542
|
+
const artifact = {
|
|
2543
|
+
irVersion: IR_VERSION,
|
|
2544
|
+
namespace: ctx.namespace,
|
|
2545
|
+
schemaHash,
|
|
2546
|
+
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|
|
2547
|
+
tables: ctx.tables,
|
|
2548
|
+
operations
|
|
2549
|
+
};
|
|
2550
|
+
const normalized = normalizeIr(artifact);
|
|
2551
|
+
const manifest = {
|
|
2552
|
+
namespace: ctx.namespace,
|
|
2553
|
+
schemaHash,
|
|
2554
|
+
opsHash: normalized.opsHash,
|
|
2555
|
+
operations: Object.fromEntries(
|
|
2556
|
+
Object.values(normalized.operations).map((op) => [
|
|
2557
|
+
op.name,
|
|
2558
|
+
{ opHash: op.opHash, kind: op.kind, requires: op.requires }
|
|
2559
|
+
])
|
|
2560
|
+
)
|
|
2561
|
+
};
|
|
2562
|
+
return { ok: true, artifact: normalized, manifest, diagnostics };
|
|
2563
|
+
}
|
|
2564
|
+
function compileNamespaceOrThrow(input) {
|
|
2565
|
+
const result = compileNamespace(input);
|
|
2566
|
+
if (!result.ok) throw new NamespaceCompileError(result.diagnostics);
|
|
2567
|
+
return result;
|
|
2568
|
+
}
|
|
2569
|
+
function computePinnedColumnsByTable(canonical) {
|
|
2570
|
+
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
2571
|
+
if (diagnostics.some((d) => d.severity === "error")) {
|
|
2572
|
+
throw new NamespaceCompileError(diagnostics);
|
|
2573
|
+
}
|
|
2574
|
+
return Object.fromEntries([...ctx.pins.entries()].map(([table, pins]) => [table, pins]));
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// src/compiler-gql/dynamic-input.ts
|
|
2578
|
+
var CONST_TRUE2 = { kind: "and", operands: [] };
|
|
2579
|
+
var ORDER_DIRECTIONS2 = {
|
|
2580
|
+
ASC: { direction: "asc" },
|
|
2581
|
+
DESC: { direction: "desc" },
|
|
2582
|
+
ASC_NULLS_FIRST: { direction: "asc", nulls: "first" },
|
|
2583
|
+
ASC_NULLS_LAST: { direction: "asc", nulls: "last" },
|
|
2584
|
+
DESC_NULLS_FIRST: { direction: "desc", nulls: "first" },
|
|
2585
|
+
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
2586
|
+
};
|
|
2587
|
+
var BOOLEXP_OPS2 = {
|
|
2588
|
+
_eq: "eq",
|
|
2589
|
+
_neq: "neq",
|
|
2590
|
+
_gt: "gt",
|
|
2591
|
+
_gte: "gte",
|
|
2592
|
+
_lt: "lt",
|
|
2593
|
+
_lte: "lte",
|
|
2594
|
+
_in: "in",
|
|
2595
|
+
_nin: "nin",
|
|
2596
|
+
_like: "like",
|
|
2597
|
+
_ilike: "ilike",
|
|
2598
|
+
_isNull: "isNull",
|
|
2599
|
+
_contains: "contains",
|
|
2600
|
+
_hasKey: "hasKey"
|
|
2601
|
+
};
|
|
2602
|
+
function allowedOps(type) {
|
|
2603
|
+
if (typeof type !== "string") return ["eq", "neq", "in", "nin", "isNull"];
|
|
2604
|
+
switch (type) {
|
|
2605
|
+
case "text":
|
|
2606
|
+
return ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "isNull"];
|
|
2607
|
+
case "uuid":
|
|
2608
|
+
return ["eq", "neq", "in", "nin", "isNull"];
|
|
2609
|
+
case "integer":
|
|
2610
|
+
case "bigint":
|
|
2611
|
+
case "numeric":
|
|
2612
|
+
case "double":
|
|
2613
|
+
case "timestamptz":
|
|
2614
|
+
case "date":
|
|
2615
|
+
return ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "isNull"];
|
|
2616
|
+
case "boolean":
|
|
2617
|
+
return ["eq", "neq", "isNull"];
|
|
2618
|
+
case "jsonb":
|
|
2619
|
+
return ["eq", "neq", "contains", "hasKey", "isNull"];
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function isPlainObject(value) {
|
|
2623
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2624
|
+
}
|
|
2625
|
+
function scalarMatches(type, value) {
|
|
2626
|
+
if (typeof type !== "string") return typeof value === "string" && type.values.includes(value);
|
|
2627
|
+
switch (type) {
|
|
2628
|
+
case "text":
|
|
2629
|
+
case "uuid":
|
|
2630
|
+
case "timestamptz":
|
|
2631
|
+
case "date":
|
|
2632
|
+
return typeof value === "string";
|
|
2633
|
+
case "integer":
|
|
2634
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
2635
|
+
case "double":
|
|
2636
|
+
return typeof value === "number";
|
|
2637
|
+
// bigint/numeric exceed JS number precision — the JSON wire form may be a string.
|
|
2638
|
+
case "bigint":
|
|
2639
|
+
case "numeric":
|
|
2640
|
+
return typeof value === "number" || typeof value === "string";
|
|
2641
|
+
case "boolean":
|
|
2642
|
+
return typeof value === "boolean";
|
|
2643
|
+
case "jsonb":
|
|
2644
|
+
return true;
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
var IssueCollector = class {
|
|
2648
|
+
issues = [];
|
|
2649
|
+
add(code, path, message) {
|
|
2650
|
+
this.issues.push({ code, path, message });
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
function finish(collector, value) {
|
|
2654
|
+
return collector.issues.length > 0 ? { ok: false, issues: collector.issues } : { ok: true, value };
|
|
2655
|
+
}
|
|
2656
|
+
function validateBoolExpInput({ table, tables, value, maxDepth, maxNodes }) {
|
|
2657
|
+
const collector = new IssueCollector();
|
|
2658
|
+
const depthCap = maxDepth ?? table.dynamicFilterMaxDepth;
|
|
2659
|
+
const nodeCap = maxNodes ?? table.dynamicFilterMaxNodes;
|
|
2660
|
+
let nodes = 0;
|
|
2661
|
+
let capped = false;
|
|
2662
|
+
const consumeNode = (path) => {
|
|
2663
|
+
nodes += 1;
|
|
2664
|
+
if (nodes <= nodeCap) return true;
|
|
2665
|
+
if (!capped) {
|
|
2666
|
+
capped = true;
|
|
2667
|
+
collector.add("too-many-nodes", path, `filter tree exceeds the node cap of ${nodeCap}`);
|
|
2668
|
+
}
|
|
2669
|
+
return false;
|
|
2670
|
+
};
|
|
2671
|
+
const walk = (input, meta, depth, path) => {
|
|
2672
|
+
if (capped) return CONST_TRUE2;
|
|
2673
|
+
if (depth > depthCap) {
|
|
2674
|
+
collector.add("too-deep", path, `filter tree exceeds the depth cap of ${depthCap}`);
|
|
2675
|
+
return CONST_TRUE2;
|
|
2676
|
+
}
|
|
2677
|
+
if (!isPlainObject(input)) {
|
|
2678
|
+
collector.add("invalid-shape", path, "a boolean expression must be an object");
|
|
2679
|
+
return CONST_TRUE2;
|
|
2680
|
+
}
|
|
2681
|
+
const operands = [];
|
|
2682
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
2683
|
+
if (entry === void 0 || entry === null) continue;
|
|
2684
|
+
if (!consumeNode(path)) return CONST_TRUE2;
|
|
2685
|
+
if (key === "_and" || key === "_or") {
|
|
2686
|
+
const items = Array.isArray(entry) ? entry : [entry];
|
|
2687
|
+
const children = items.map((item, index) => walk(item, meta, depth + 1, `${path}.${key}[${index}]`));
|
|
2688
|
+
if (children.length > 0) operands.push({ kind: key === "_and" ? "and" : "or", operands: children });
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
if (key === "_not") {
|
|
2692
|
+
operands.push({ kind: "not", operand: walk(entry, meta, depth + 1, `${path}._not`) });
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
const column = meta.columns.find((c) => c.name === key);
|
|
2696
|
+
if (column) {
|
|
2697
|
+
if (!meta.filterable.includes(key)) {
|
|
2698
|
+
collector.add("column-not-allowed", `${path}.${key}`, `column '${key}' is not filterable on '${meta.table}'`);
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
if (!isPlainObject(entry)) {
|
|
2702
|
+
collector.add("invalid-shape", `${path}.${key}`, `comparison on '${key}' must be an object like { _eq: \u2026 }`);
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
for (const [opKey, opValue] of Object.entries(entry)) {
|
|
2706
|
+
if (opValue === void 0) continue;
|
|
2707
|
+
if (!consumeNode(`${path}.${key}`)) return CONST_TRUE2;
|
|
2708
|
+
const op = BOOLEXP_OPS2[opKey];
|
|
2709
|
+
if (!op || !allowedOps(column.type).includes(op)) {
|
|
2710
|
+
collector.add("unknown-operator", `${path}.${key}.${opKey}`, `operator '${opKey}' is not allowed on '${meta.table}.${key}'`);
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
const issue = checkOperatorValue(op, column.type, opValue);
|
|
2714
|
+
if (issue) {
|
|
2715
|
+
collector.add("invalid-value", `${path}.${key}.${opKey}`, issue);
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
operands.push({ kind: "cmp", op, column: key, value: opValue });
|
|
2719
|
+
}
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
const link = meta.links.find((l) => l.name === key);
|
|
2723
|
+
if (link) {
|
|
2724
|
+
const target = tables[link.target];
|
|
2725
|
+
if (!target) {
|
|
2726
|
+
collector.add("unknown-link", `${path}.${key}`, `link target '${link.target}' is not in the artifact`);
|
|
2727
|
+
continue;
|
|
2728
|
+
}
|
|
2729
|
+
operands.push({ kind: "exists", link: key, target: link.target, where: walk(entry, target, depth + 1, `${path}.${key}`) });
|
|
2730
|
+
continue;
|
|
2731
|
+
}
|
|
2732
|
+
collector.add(
|
|
2733
|
+
"unknown-column",
|
|
2734
|
+
`${path}.${key}`,
|
|
2735
|
+
`unknown field '${key}' in a filter on '${meta.table}' (declared columns and links only)`
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
if (operands.length === 0) return CONST_TRUE2;
|
|
2739
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
2740
|
+
};
|
|
2741
|
+
const normalized = walk(value, table, 1, "$");
|
|
2742
|
+
return finish(collector, normalized);
|
|
2743
|
+
}
|
|
2744
|
+
function checkOperatorValue(op, type, value) {
|
|
2745
|
+
if (op === "isNull") return typeof value === "boolean" ? void 0 : "_isNull takes a boolean";
|
|
2746
|
+
if (op === "hasKey") return typeof value === "string" ? void 0 : "_hasKey takes a string key";
|
|
2747
|
+
if (op === "contains") return void 0;
|
|
2748
|
+
if (op === "in" || op === "nin") {
|
|
2749
|
+
if (!Array.isArray(value)) return `${op === "in" ? "_in" : "_nin"} takes an array`;
|
|
2750
|
+
return value.every((item) => scalarMatches(type, item)) ? void 0 : "array items do not match the column type";
|
|
2751
|
+
}
|
|
2752
|
+
if (op === "like" || op === "ilike") return typeof value === "string" ? void 0 : "pattern must be a string";
|
|
2753
|
+
return scalarMatches(type, value) ? void 0 : "value does not match the column type";
|
|
2754
|
+
}
|
|
2755
|
+
function validateOrderByInput({
|
|
2756
|
+
table,
|
|
2757
|
+
tables,
|
|
2758
|
+
value
|
|
2759
|
+
}) {
|
|
2760
|
+
const collector = new IssueCollector();
|
|
2761
|
+
const normalized = [];
|
|
2762
|
+
const items = Array.isArray(value) ? value : [value];
|
|
2763
|
+
items.forEach((item, index) => {
|
|
2764
|
+
const path = `$[${index}]`;
|
|
2765
|
+
if (!isPlainObject(item)) {
|
|
2766
|
+
collector.add("invalid-shape", path, "orderBy entries must be objects");
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
for (const [key, direction] of Object.entries(item)) {
|
|
2770
|
+
if (direction === void 0 || direction === null) continue;
|
|
2771
|
+
if (table.orderable.includes(key)) {
|
|
2772
|
+
const resolved = typeof direction === "string" ? ORDER_DIRECTIONS2[direction] : void 0;
|
|
2773
|
+
if (!resolved) {
|
|
2774
|
+
collector.add("invalid-value", `${path}.${key}`, `unknown order direction '${String(direction)}'`);
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
normalized.push({ column: key, ...resolved });
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
const link = table.links.find((l) => l.name === key);
|
|
2781
|
+
if (link && link.kind !== "toMany") {
|
|
2782
|
+
const target = tables[link.target];
|
|
2783
|
+
if (!target || !isPlainObject(direction) || Object.keys(direction).length !== 1) {
|
|
2784
|
+
collector.add("invalid-shape", `${path}.${key}`, `orderBy through '${key}' needs exactly one target column`);
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
const [targetColumn, targetDirection] = Object.entries(direction)[0];
|
|
2788
|
+
const resolved = typeof targetDirection === "string" ? ORDER_DIRECTIONS2[targetDirection] : void 0;
|
|
2789
|
+
if (!target.orderable.includes(targetColumn)) {
|
|
2790
|
+
collector.add("unknown-column", `${path}.${key}.${targetColumn}`, `'${targetColumn}' is not orderable on '${link.target}'`);
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
if (!resolved) {
|
|
2794
|
+
collector.add("invalid-value", `${path}.${key}.${targetColumn}`, `unknown order direction '${String(targetDirection)}'`);
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
normalized.push({ column: targetColumn, link: key, ...resolved });
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
collector.add("unknown-column", `${path}.${key}`, `'${key}' is not orderable on '${table.table}' (columns and to-one links only)`);
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
return finish(collector, normalized);
|
|
2804
|
+
}
|
|
2805
|
+
function validateSelectColumns({ table, value }) {
|
|
2806
|
+
const collector = new IssueCollector();
|
|
2807
|
+
const items = Array.isArray(value) ? value : [value];
|
|
2808
|
+
const normalized = [];
|
|
2809
|
+
items.forEach((item, index) => {
|
|
2810
|
+
if (typeof item !== "string" || !table.filterable.includes(item)) {
|
|
2811
|
+
collector.add("unknown-column", `$[${index}]`, `'${String(item)}' is not a selectable column of '${table.table}'`);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
normalized.push(item);
|
|
2815
|
+
});
|
|
2816
|
+
return finish(collector, normalized);
|
|
2817
|
+
}
|
|
2818
|
+
function validateInsertInput({
|
|
2819
|
+
table,
|
|
2820
|
+
value,
|
|
2821
|
+
list,
|
|
2822
|
+
maxRows
|
|
2823
|
+
}) {
|
|
2824
|
+
const collector = new IssueCollector();
|
|
2825
|
+
const rowCap = maxRows ?? 1e3;
|
|
2826
|
+
const rows = list ? value : [value];
|
|
2827
|
+
if (!Array.isArray(rows)) {
|
|
2828
|
+
collector.add("invalid-shape", "$", "objects must be an array of rows");
|
|
2829
|
+
return { ok: false, issues: collector.issues };
|
|
2830
|
+
}
|
|
2831
|
+
if (rows.length > rowCap) {
|
|
2832
|
+
collector.add("too-many-rows", "$", `insert exceeds the row cap of ${rowCap}`);
|
|
2833
|
+
return { ok: false, issues: collector.issues };
|
|
2834
|
+
}
|
|
2835
|
+
const normalized = [];
|
|
2836
|
+
rows.forEach((row, index) => {
|
|
2837
|
+
const path = list ? `$[${index}]` : "$";
|
|
2838
|
+
if (!isPlainObject(row)) {
|
|
2839
|
+
collector.add("invalid-shape", path, "insert rows must be objects");
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
const out = {};
|
|
2843
|
+
for (const [key, entry] of Object.entries(row)) {
|
|
2844
|
+
if (entry === void 0) continue;
|
|
2845
|
+
if (table.links.some((l) => l.name === key)) {
|
|
2846
|
+
collector.add("nested-insert", `${path}.${key}`, `nested inserts are not supported \u2014 '${key}' is a link (a { data: \u2026 } shape was rejected)`);
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
const column = table.columns.find((c) => c.name === key);
|
|
2850
|
+
if (!column || !table.insertable.includes(key)) {
|
|
2851
|
+
const reason = column ? "not insertable (pinned by the write policy or deprecated)" : "not a declared column";
|
|
2852
|
+
collector.add(column ? "column-not-allowed" : "unknown-column", `${path}.${key}`, `'${key}' is ${reason} on '${table.table}'`);
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2855
|
+
if (entry === null) {
|
|
2856
|
+
if (!column.nullable) {
|
|
2857
|
+
collector.add("invalid-value", `${path}.${key}`, `'${key}' is not nullable`);
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2860
|
+
} else if (!scalarMatches(column.type, entry)) {
|
|
2861
|
+
collector.add("invalid-value", `${path}.${key}`, `value for '${key}' does not match its column type`);
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
out[key] = entry;
|
|
2865
|
+
}
|
|
2866
|
+
normalized.push(out);
|
|
2867
|
+
});
|
|
2868
|
+
return finish(collector, normalized);
|
|
2869
|
+
}
|
|
2870
|
+
function validateColumnMap({
|
|
2871
|
+
table,
|
|
2872
|
+
value,
|
|
2873
|
+
whitelist,
|
|
2874
|
+
argName,
|
|
2875
|
+
typeCheck
|
|
2876
|
+
}) {
|
|
2877
|
+
const collector = new IssueCollector();
|
|
2878
|
+
if (!isPlainObject(value)) {
|
|
2879
|
+
collector.add("invalid-shape", "$", `${argName} must be an object`);
|
|
2880
|
+
return { ok: false, issues: collector.issues };
|
|
2881
|
+
}
|
|
2882
|
+
const out = {};
|
|
2883
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2884
|
+
if (entry === void 0) continue;
|
|
2885
|
+
if (!whitelist.includes(key)) {
|
|
2886
|
+
const known = table.columns.some((c) => c.name === key);
|
|
2887
|
+
collector.add(known ? "column-not-allowed" : "unknown-column", `$.${key}`, `'${key}' is not allowed in ${argName} on '${table.table}'`);
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
const column = table.columns.find((c) => c.name === key);
|
|
2891
|
+
if (column && entry === null && !column.nullable) {
|
|
2892
|
+
collector.add("invalid-value", `$.${key}`, `'${key}' is not nullable`);
|
|
2893
|
+
continue;
|
|
2894
|
+
}
|
|
2895
|
+
if (typeCheck && column && entry !== null && !scalarMatches(column.type, entry)) {
|
|
2896
|
+
collector.add("invalid-value", `$.${key}`, `value for '${key}' does not match its column type`);
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
out[key] = entry;
|
|
2900
|
+
}
|
|
2901
|
+
return finish(collector, out);
|
|
2902
|
+
}
|
|
2903
|
+
function validateSetInput({ table, value }) {
|
|
2904
|
+
return validateColumnMap({ table, value, whitelist: table.settable, argName: "_set", typeCheck: true });
|
|
2905
|
+
}
|
|
2906
|
+
function validateAppendInput({ table, value }) {
|
|
2907
|
+
return validateColumnMap({ table, value, whitelist: table.appendable, argName: "_append", typeCheck: false });
|
|
2908
|
+
}
|
|
2909
|
+
function validateOnConflictInput({
|
|
2910
|
+
table,
|
|
2911
|
+
value
|
|
2912
|
+
}) {
|
|
2913
|
+
const collector = new IssueCollector();
|
|
2914
|
+
if (!isPlainObject(value)) {
|
|
2915
|
+
collector.add("invalid-shape", "$", "onConflict must be an object");
|
|
2916
|
+
return { ok: false, issues: collector.issues };
|
|
2917
|
+
}
|
|
2918
|
+
for (const key of Object.keys(value)) {
|
|
2919
|
+
if (key !== "constraint" && key !== "updateColumns") {
|
|
2920
|
+
collector.add("invalid-shape", `$.${key}`, `unknown onConflict key '${key}' (constraint, updateColumns)`);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
const constraint = value["constraint"];
|
|
2924
|
+
if (typeof constraint !== "string" || !table.constraints.some((c) => c.name === constraint)) {
|
|
2925
|
+
collector.add(
|
|
2926
|
+
"unknown-constraint",
|
|
2927
|
+
"$.constraint",
|
|
2928
|
+
`'${String(constraint)}' is not a declared constraint of '${table.table}'`
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
const updateColumns = [];
|
|
2932
|
+
const rawColumns = value["updateColumns"] ?? [];
|
|
2933
|
+
if (!Array.isArray(rawColumns)) {
|
|
2934
|
+
collector.add("invalid-shape", "$.updateColumns", "updateColumns must be an array");
|
|
2935
|
+
} else {
|
|
2936
|
+
rawColumns.forEach((item, index) => {
|
|
2937
|
+
if (typeof item !== "string" || !table.settable.includes(item)) {
|
|
2938
|
+
collector.add("column-not-allowed", `$.updateColumns[${index}]`, `'${String(item)}' is not updatable on '${table.table}'`);
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
updateColumns.push(item);
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
return finish(collector, { constraint, updateColumns });
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
exports.COMPILER_MAJOR = COMPILER_MAJOR;
|
|
2948
|
+
exports.CONST_TRUE = CONST_TRUE;
|
|
2949
|
+
exports.DEFAULT_LIMIT = DEFAULT_LIMIT;
|
|
2950
|
+
exports.DocumentCompileError = DocumentCompileError;
|
|
2951
|
+
exports.MAX_LIMIT = MAX_LIMIT;
|
|
2952
|
+
exports.NamespaceCompileError = NamespaceCompileError;
|
|
2953
|
+
exports.OperationLowerer = OperationLowerer;
|
|
2954
|
+
exports.PLATFORM_USER_TABLE = PLATFORM_USER_TABLE;
|
|
2955
|
+
exports.PLATFORM_USER_TYPENAME = PLATFORM_USER_TYPENAME;
|
|
2956
|
+
exports.POLICY_COMPARE_OPS = POLICY_COMPARE_OPS;
|
|
2957
|
+
exports.POLICY_MAX_LINK_DEPTH = POLICY_MAX_LINK_DEPTH;
|
|
2958
|
+
exports.PolicyCompileError = PolicyCompileError;
|
|
2959
|
+
exports.PolicyParseError = PolicyParseError;
|
|
2960
|
+
exports.andPred = andPred;
|
|
2961
|
+
exports.assertClaimsOnlyWhen = assertClaimsOnlyWhen;
|
|
2962
|
+
exports.buildCompileContext = buildCompileContext;
|
|
2963
|
+
exports.camelCase = camelCase;
|
|
2964
|
+
exports.canonicalOperationText = canonicalOperationText;
|
|
2965
|
+
exports.classifyInputType = classifyInputType;
|
|
2966
|
+
exports.collectPinnedColumns = collectPinnedColumns;
|
|
2967
|
+
exports.compileNamespace = compileNamespace;
|
|
2968
|
+
exports.compileNamespaceOrThrow = compileNamespaceOrThrow;
|
|
2969
|
+
exports.compilePolicy = compilePolicy;
|
|
2970
|
+
exports.compilePredicateV2 = compilePredicateV2;
|
|
2971
|
+
exports.computeOpHash = computeOpHash;
|
|
2972
|
+
exports.computeOpsHash = computeOpsHash;
|
|
2973
|
+
exports.computePinnedColumnsByTable = computePinnedColumnsByTable;
|
|
2974
|
+
exports.computeSchemaHash = computeSchemaHash;
|
|
2975
|
+
exports.enumTypeName = enumTypeName;
|
|
2976
|
+
exports.flattenOperation = flattenOperation;
|
|
2977
|
+
exports.injectPolicies = injectPolicies;
|
|
2978
|
+
exports.inputTypeName = inputTypeName;
|
|
2979
|
+
exports.lowerOperation = lowerOperation;
|
|
2980
|
+
exports.mutationRootNames = mutationRootNames;
|
|
2981
|
+
exports.parseDocuments = parseDocuments;
|
|
2982
|
+
exports.parsePolicy = parsePolicy;
|
|
2983
|
+
exports.parseSdl = parseSdl;
|
|
2984
|
+
exports.pascalCase = pascalCase;
|
|
2985
|
+
exports.platformUserMeta = platformUserMeta;
|
|
2986
|
+
exports.predContains = predContains;
|
|
2987
|
+
exports.predContainsExists = predContainsExists;
|
|
2988
|
+
exports.queryRootNames = queryRootNames;
|
|
2989
|
+
exports.restrictOperation = restrictOperation;
|
|
2990
|
+
exports.typeNameForTable = typeNameForTable;
|
|
2991
|
+
exports.validateAppendInput = validateAppendInput;
|
|
2992
|
+
exports.validateBoolExpInput = validateBoolExpInput;
|
|
2993
|
+
exports.validateInsertInput = validateInsertInput;
|
|
2994
|
+
exports.validateOnConflictInput = validateOnConflictInput;
|
|
2995
|
+
exports.validateOrderByInput = validateOrderByInput;
|
|
2996
|
+
exports.validateSelectColumns = validateSelectColumns;
|
|
2997
|
+
exports.validateSetInput = validateSetInput;
|
|
2998
|
+
exports.walkPolicy = walkPolicy;
|
|
2999
|
+
exports.walkPred = walkPred;
|
|
3000
|
+
//# sourceMappingURL=compiler.cjs.map
|
|
3001
|
+
//# sourceMappingURL=compiler.cjs.map
|