better-supabase 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -1
- package/bin/better-supabase.js +3 -0
- package/dist/casing-da0uRTqt.js +14 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +15 -0
- package/dist/cli/index.d.ts +226 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli-CoByWYja.js +6472 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +112 -0
- package/dist/compiler-DL18ukPe.d.ts +14 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +3 -0
- package/dist/config-D-aR2ZoN.js +276 -0
- package/dist/define-CTfWw-SS.d.ts +662 -0
- package/dist/define-QGkq04oe.js +1068 -0
- package/dist/edge/index.d.ts +43 -0
- package/dist/edge/index.js +103 -0
- package/dist/entitlements-BBg61DQZ.d.ts +20 -0
- package/dist/entitlements-BC_khwRy.js +17 -0
- package/dist/env/index.d.ts +2 -0
- package/dist/env/index.js +2 -0
- package/dist/env-BEToab0I.js +204 -0
- package/dist/errors-Bl3rkOzo.js +196 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.js +2 -0
- package/dist/events-2OJsVG_d.d.ts +84 -0
- package/dist/events-BuzTgn2D.js +135 -0
- package/dist/executor-Bazhgsgq.d.ts +195 -0
- package/dist/executor-Bje4suQF.d.ts +25 -0
- package/dist/executor-Ceq60hPM.js +89 -0
- package/dist/hono/index.d.ts +45 -0
- package/dist/hono/index.js +70 -0
- package/dist/index-BD6wKthp.d.ts +597 -0
- package/dist/index-DQVvHzjW.d.ts +74 -0
- package/dist/index-DRid0qIv.d.ts +76 -0
- package/dist/index-Dup-nSe9.d.ts +117 -0
- package/dist/index-Dy6kuF0P.d.ts +71 -0
- package/dist/index-Hf8PP9sP.d.ts +116 -0
- package/dist/index-JXHBuS53.d.ts +144 -0
- package/dist/index-Nxo3TJcm.d.ts +208 -0
- package/dist/index-Sw43J8HG.d.ts +62 -0
- package/dist/index-cEeweTeM.d.ts +38 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +42 -0
- package/dist/invalidate-DbxuL2jL.js +18 -0
- package/dist/jobs/index.d.ts +173 -0
- package/dist/jobs/index.js +477 -0
- package/dist/json-schema-BbwO2mZg.js +181 -0
- package/dist/lint/index.d.ts +75 -0
- package/dist/lint/index.js +167 -0
- package/dist/list/index.d.ts +2 -0
- package/dist/list/index.js +410 -0
- package/dist/live-6b9i72Ux.d.ts +180 -0
- package/dist/live-D8rV1Ldw.js +168 -0
- package/dist/mcp/index.d.ts +105 -0
- package/dist/mcp/index.js +330 -0
- package/dist/mfa-CpogP66z.js +521 -0
- package/dist/mfa-TVia554c.d.ts +44 -0
- package/dist/next/image/index.d.ts +34 -0
- package/dist/next/image/index.js +40 -0
- package/dist/next/index.d.ts +210 -0
- package/dist/next/index.js +426 -0
- package/dist/openapi/index.d.ts +2 -0
- package/dist/openapi/index.js +332 -0
- package/dist/orpc/index.d.ts +46 -0
- package/dist/orpc/index.js +50 -0
- package/dist/otel/index.d.ts +40 -0
- package/dist/otel/index.js +168 -0
- package/dist/path-Cv__-OTj.d.ts +17 -0
- package/dist/plugin-BqR6wKMB.js +23 -0
- package/dist/plugin-HvGqCurB.d.ts +118 -0
- package/dist/plugins/actor/index.d.ts +13 -0
- package/dist/plugins/actor/index.js +43 -0
- package/dist/plugins/rules/index.d.ts +88 -0
- package/dist/plugins/rules/index.js +174 -0
- package/dist/plugins/soft-delete/index.d.ts +38 -0
- package/dist/plugins/soft-delete/index.js +69 -0
- package/dist/plugins/tenant/index.d.ts +43 -0
- package/dist/plugins/tenant/index.js +73 -0
- package/dist/plugins/timestamps/index.d.ts +9 -0
- package/dist/plugins/timestamps/index.js +37 -0
- package/dist/plugins/validation/index.d.ts +24 -0
- package/dist/plugins/validation/index.js +59 -0
- package/dist/pool-BFeqzwQS.d.ts +37 -0
- package/dist/postgres/index.d.ts +28 -0
- package/dist/postgres/index.js +79 -0
- package/dist/postgrest-Dm-vhY6R.d.ts +43 -0
- package/dist/problem-DPu6Fh-t.js +97 -0
- package/dist/problem-DlgB0lYl.d.ts +47 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +3 -0
- package/dist/query-ChvDbsDE.js +186 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +5 -0
- package/dist/react/server.d.ts +13 -0
- package/dist/react/server.js +25 -0
- package/dist/react/session.d.ts +2 -0
- package/dist/react/session.js +3 -0
- package/dist/react-CrOvrnIP.js +241 -0
- package/dist/read-set-DhX2d04c.js +216 -0
- package/dist/realtime/index.d.ts +3 -0
- package/dist/realtime/index.js +253 -0
- package/dist/resolve-DW8h9NPm.d.ts +155 -0
- package/dist/resource-CYiu6Ygi.js +175 -0
- package/dist/resource-SkPTRJDD.d.ts +65 -0
- package/dist/respond-4-dhoV7x.js +380 -0
- package/dist/respond-PoTpy5DT.d.ts +169 -0
- package/dist/result-CTkCZ6JA.d.ts +198 -0
- package/dist/result-DWXatkd6.js +103 -0
- package/dist/scope-CDHcS7dE.js +90 -0
- package/dist/seed-CzecODcJ.js +101 -0
- package/dist/server/index.d.ts +42 -0
- package/dist/server/index.js +68 -0
- package/dist/session-C3osL21k.d.ts +21 -0
- package/dist/session-CD-3orO_.js +19 -0
- package/dist/shared-_6-uPL7E.js +57 -0
- package/dist/simplify-oHiDKRVQ.js +665 -0
- package/dist/spec-pins-CJPVghHy.d.ts +20 -0
- package/dist/spec-pins-t6sPaHvl.js +20 -0
- package/dist/sql-M8l3fnS0.js +260 -0
- package/dist/ssr/index.d.ts +5 -0
- package/dist/ssr/index.js +4 -0
- package/dist/standard-CyhRzctb.d.ts +7 -0
- package/dist/standard-FUi3Z3-7.js +21 -0
- package/dist/stats-3iy8u3_P.js +566 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage-BWXVUXGk.js +461 -0
- package/dist/tables-rZxRxNPW.js +59 -0
- package/dist/template-BeOGnynM.js +93 -0
- package/dist/template-mbvlysOo.d.ts +6 -0
- package/dist/testing/index.d.ts +294 -0
- package/dist/testing/index.js +751 -0
- package/dist/types-6cYK9pbJ.js +29 -0
- package/dist/types-C2fslP1z.d.ts +195 -0
- package/dist/version-Cqq1m3UY.js +4 -0
- package/dist/view-B0ltUmTi.d.ts +43 -0
- package/dist/view-B6F9QbG-.js +35 -0
- package/dist/webhooks/index.d.ts +152 -0
- package/dist/webhooks/index.js +2 -0
- package/dist/webhooks-DbMTJJX2.js +139 -0
- package/package.json +246 -17
- package/schemas/config-v1.json +328 -0
- package/schemas/doctor-report-v1.json +105 -0
- package/schemas/snapshot-v2.json +1002 -0
- package/skills/better-supabase/SKILL.md +84 -0
- package/skills/better-supabase/references/plugins.md +56 -0
- package/skills/better-supabase/references/troubleshooting.md +39 -0
- package/skills/better-supabase-api/SKILL.md +67 -0
- package/skills/better-supabase-api/references/adapters.md +108 -0
- package/skills/better-supabase-testing/SKILL.md +65 -0
- package/index.js +0 -1
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { n as dbError, t as DbException } from "./errors-Bl3rkOzo.js";
|
|
2
|
+
import { i as or, r as not, t as and } from "./types-6cYK9pbJ.js";
|
|
3
|
+
//#region src/schema/define.ts
|
|
4
|
+
/**
|
|
5
|
+
* Binds generated model types to runtime metadata. Called from the generated
|
|
6
|
+
* module; apps import the returned `schema`, never call this themselves.
|
|
7
|
+
*/
|
|
8
|
+
function defineSchema(meta) {
|
|
9
|
+
return { meta: validateMeta(meta) };
|
|
10
|
+
}
|
|
11
|
+
function validateMeta(meta) {
|
|
12
|
+
if (meta.version !== 1) throw new TypeError(`better-supabase: unsupported schema metadata version ${String(meta.version)}. Run \`better-supabase gen\` again.`);
|
|
13
|
+
return meta;
|
|
14
|
+
}
|
|
15
|
+
function tableMeta(meta, table) {
|
|
16
|
+
const found = meta.tables[table];
|
|
17
|
+
if (!found) throw new TypeError(`better-supabase: unknown table "${table}"`);
|
|
18
|
+
return found;
|
|
19
|
+
}
|
|
20
|
+
function relationMeta(meta, table, relation) {
|
|
21
|
+
const found = table.relations[relation];
|
|
22
|
+
if (!found) throw new TypeError(`better-supabase: unknown relation "${relation}" on "${table.key}"`);
|
|
23
|
+
return {
|
|
24
|
+
relation: found,
|
|
25
|
+
target: tableMeta(meta, found.table)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/ir/codec.ts
|
|
30
|
+
/** Wire form of an app value: `Date` to ISO text, `bigint` to decimal text. */
|
|
31
|
+
function encodeValue(value) {
|
|
32
|
+
if (value instanceof Date) return value.toISOString();
|
|
33
|
+
if (typeof value === "bigint") return value.toString();
|
|
34
|
+
if (Array.isArray(value)) return value.map(encodeValue);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function decodeScalar(codec, value) {
|
|
38
|
+
if (value === null || value === void 0) return value;
|
|
39
|
+
switch (codec) {
|
|
40
|
+
case "date": return new Date(value);
|
|
41
|
+
case "bigint": return BigInt(value);
|
|
42
|
+
case "string": return String(value);
|
|
43
|
+
default: return codec;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function decode(codec, value) {
|
|
47
|
+
return Array.isArray(value) ? value.map((item) => decodeScalar(codec, item)) : decodeScalar(codec, value);
|
|
48
|
+
}
|
|
49
|
+
/** Whether decoding `selection` changes anything, so plain reads skip the walk. */
|
|
50
|
+
function needsDecoding(selection) {
|
|
51
|
+
return selection.aggregate !== void 0 || selection.columns.some((column) => column.codec !== void 0) || selection.includes.some((include) => include.count !== void 0 || include.aggregate !== void 0 || needsDecoding(include.selection));
|
|
52
|
+
}
|
|
53
|
+
/** PostgREST returns `[{ count }]` for a `(count)` embed; SQL returns the number. */
|
|
54
|
+
function countOf(value) {
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
const first = value[0];
|
|
57
|
+
return Number(first?.count ?? 0);
|
|
58
|
+
}
|
|
59
|
+
return Number(value ?? 0);
|
|
60
|
+
}
|
|
61
|
+
function measureOf(measure, value) {
|
|
62
|
+
if (value === void 0 || value === null) return null;
|
|
63
|
+
return measure.codec ? decodeScalar(measure.codec, value) : value;
|
|
64
|
+
}
|
|
65
|
+
/** Adds `value` under `row[key][name]`, next to what is already there. */
|
|
66
|
+
function fold(row, key, name, value) {
|
|
67
|
+
row[key] = {
|
|
68
|
+
...row[key] ?? {},
|
|
69
|
+
[name]: value
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function decodeRow(selection, row) {
|
|
73
|
+
const out = { ...row };
|
|
74
|
+
for (const column of selection.columns) if (column.codec && column.alias in out) out[column.alias] = decode(column.codec, out[column.alias]);
|
|
75
|
+
if (selection.aggregate) {
|
|
76
|
+
if (selection.aggregate.count) out["_count"] = countOf(out["_count"]);
|
|
77
|
+
for (const measure of selection.aggregate.measures) {
|
|
78
|
+
const value = out[measure.key];
|
|
79
|
+
delete out[measure.key];
|
|
80
|
+
fold(out, `_${measure.fn}`, measure.alias, measureOf(measure, value));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const include of selection.includes) {
|
|
84
|
+
const value = out[include.alias];
|
|
85
|
+
if (include.count !== void 0) {
|
|
86
|
+
if (!(include.alias in out)) continue;
|
|
87
|
+
delete out[include.alias];
|
|
88
|
+
fold(out, "_count", include.count, countOf(value));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (include.aggregate !== void 0) {
|
|
92
|
+
if (!(include.alias in out)) continue;
|
|
93
|
+
delete out[include.alias];
|
|
94
|
+
const inner = Array.isArray(value) ? value[0] : value;
|
|
95
|
+
const measures = {};
|
|
96
|
+
for (const measure of include.selection.aggregate?.measures ?? []) measures[measure.alias] = measureOf(measure, inner?.[measure.key]);
|
|
97
|
+
fold(out, `_${include.aggregate.fn}`, include.aggregate.name, measures);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) out[include.alias] = value.map((item) => decodeRow(include.selection, item));
|
|
101
|
+
else if (value && typeof value === "object") out[include.alias] = decodeRow(include.selection, value);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/** Applies column codecs to returned rows, including embedded relations. */
|
|
106
|
+
function decodeRows(selection, rows) {
|
|
107
|
+
if (!selection || !needsDecoding(selection)) return rows;
|
|
108
|
+
return rows.map((row) => decodeRow(selection, row));
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/ir/build.ts
|
|
112
|
+
const AGGREGATE_KEYS = {
|
|
113
|
+
_sum: "sum",
|
|
114
|
+
_avg: "avg",
|
|
115
|
+
_min: "min",
|
|
116
|
+
_max: "max"
|
|
117
|
+
};
|
|
118
|
+
const NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
119
|
+
"int2",
|
|
120
|
+
"int4",
|
|
121
|
+
"int8",
|
|
122
|
+
"float4",
|
|
123
|
+
"float8",
|
|
124
|
+
"numeric",
|
|
125
|
+
"smallint",
|
|
126
|
+
"integer",
|
|
127
|
+
"bigint",
|
|
128
|
+
"real",
|
|
129
|
+
"double precision"
|
|
130
|
+
]);
|
|
131
|
+
/** Thrown while building IR; surfaces as an `invalid_request` Result. */
|
|
132
|
+
function invalidRequest(message, table) {
|
|
133
|
+
throw new DbException(dbError("invalid_request", message, table ? { table } : {}));
|
|
134
|
+
}
|
|
135
|
+
const FIELD_OPS = /* @__PURE__ */ new Set([
|
|
136
|
+
"eq",
|
|
137
|
+
"neq",
|
|
138
|
+
"in",
|
|
139
|
+
"notIn",
|
|
140
|
+
"isNull",
|
|
141
|
+
"not",
|
|
142
|
+
"gt",
|
|
143
|
+
"gte",
|
|
144
|
+
"lt",
|
|
145
|
+
"lte",
|
|
146
|
+
"like",
|
|
147
|
+
"ilike",
|
|
148
|
+
"contains",
|
|
149
|
+
"startsWith",
|
|
150
|
+
"endsWith",
|
|
151
|
+
"search",
|
|
152
|
+
"hasEvery",
|
|
153
|
+
"hasSome",
|
|
154
|
+
"has",
|
|
155
|
+
"containedBy"
|
|
156
|
+
]);
|
|
157
|
+
function isPlainObject(value) {
|
|
158
|
+
if (typeof value !== "object" || value === null) return false;
|
|
159
|
+
const proto = Object.getPrototypeOf(value);
|
|
160
|
+
return proto === Object.prototype || proto === null;
|
|
161
|
+
}
|
|
162
|
+
function isOpsObject(value) {
|
|
163
|
+
if (!isPlainObject(value)) return false;
|
|
164
|
+
const keys = Object.keys(value);
|
|
165
|
+
return keys.length > 0 && keys.every((key) => FIELD_OPS.has(key));
|
|
166
|
+
}
|
|
167
|
+
function writable(meta, mode) {
|
|
168
|
+
if (meta.generated || meta.identity === "always") return false;
|
|
169
|
+
return mode === "insert" ? meta.insertable !== false : meta.updatable !== false;
|
|
170
|
+
}
|
|
171
|
+
/** Escapes LIKE wildcards so user input matches literally. */
|
|
172
|
+
function escapeLike(value) {
|
|
173
|
+
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
174
|
+
}
|
|
175
|
+
var IrBuilder = class {
|
|
176
|
+
meta;
|
|
177
|
+
constructor(meta) {
|
|
178
|
+
this.meta = meta;
|
|
179
|
+
}
|
|
180
|
+
table(key) {
|
|
181
|
+
const table = this.meta.tables[key];
|
|
182
|
+
if (!table) invalidRequest(`Unknown table "${key}"`);
|
|
183
|
+
return table;
|
|
184
|
+
}
|
|
185
|
+
column(table, name) {
|
|
186
|
+
const column = table.columns[name];
|
|
187
|
+
if (!column) invalidRequest(`Unknown column "${name}" on "${table.key}"`, table.key);
|
|
188
|
+
return column.db;
|
|
189
|
+
}
|
|
190
|
+
where(table, input) {
|
|
191
|
+
if (input === void 0) return void 0;
|
|
192
|
+
if (!isPlainObject(input)) invalidRequest(`"where" on "${table.key}" must be an object`, table.key);
|
|
193
|
+
const items = [];
|
|
194
|
+
for (const [key, value] of Object.entries(input)) {
|
|
195
|
+
if (value === void 0) continue;
|
|
196
|
+
if (key === "AND") {
|
|
197
|
+
const list = Array.isArray(value) ? value : [value];
|
|
198
|
+
items.push(and(...list.map((item) => this.where(table, item))));
|
|
199
|
+
} else if (key === "OR") {
|
|
200
|
+
if (!Array.isArray(value)) invalidRequest(`"OR" on "${table.key}" must be an array`, table.key);
|
|
201
|
+
const branches = value.map((item) => this.where(table, item)).filter((item) => item !== void 0);
|
|
202
|
+
items.push(branches.length === 0 ? not({
|
|
203
|
+
kind: "and",
|
|
204
|
+
items: []
|
|
205
|
+
}) : or(...branches));
|
|
206
|
+
} else if (key === "NOT") {
|
|
207
|
+
const inner = this.where(table, value);
|
|
208
|
+
if (inner) items.push(not(inner));
|
|
209
|
+
} else if (key in table.relations) items.push(this.relationFilter(table, key, value));
|
|
210
|
+
else items.push(this.fieldFilter(table, key, value));
|
|
211
|
+
}
|
|
212
|
+
return and(...items);
|
|
213
|
+
}
|
|
214
|
+
relationFilter(table, name, value) {
|
|
215
|
+
const { relation, target } = relationMeta(this.meta, table, name);
|
|
216
|
+
const make = (quantifier, where) => ({
|
|
217
|
+
kind: "relation",
|
|
218
|
+
name,
|
|
219
|
+
relation,
|
|
220
|
+
target,
|
|
221
|
+
quantifier,
|
|
222
|
+
where: where === null ? void 0 : this.where(target, where)
|
|
223
|
+
});
|
|
224
|
+
if (relation.kind === "many") {
|
|
225
|
+
if (!isPlainObject(value)) invalidRequest(`Filter on to-many relation "${name}" needs some, none or every`, table.key);
|
|
226
|
+
const items = [];
|
|
227
|
+
for (const [quantifier, where] of Object.entries(value)) {
|
|
228
|
+
if (where === void 0) continue;
|
|
229
|
+
if (quantifier !== "some" && quantifier !== "none" && quantifier !== "every") invalidRequest(`Unknown quantifier "${quantifier}" on relation "${name}"`, table.key);
|
|
230
|
+
items.push(make(quantifier, where));
|
|
231
|
+
}
|
|
232
|
+
return and(...items) ?? make("some", {});
|
|
233
|
+
}
|
|
234
|
+
if (value === null) return make("none", null);
|
|
235
|
+
if (isPlainObject(value)) {
|
|
236
|
+
const keys = Object.keys(value);
|
|
237
|
+
if (keys.length > 0 && keys.every((key) => key === "is" || key === "isNot")) {
|
|
238
|
+
const items = [];
|
|
239
|
+
if (value.is !== void 0) items.push(value.is === null ? make("none", null) : make("some", value.is));
|
|
240
|
+
if (value.isNot !== void 0) items.push(value.isNot === null ? make("some", null) : not(make("some", value.isNot)));
|
|
241
|
+
return and(...items) ?? make("some", null);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return make("some", value);
|
|
245
|
+
}
|
|
246
|
+
fieldFilter(table, name, value) {
|
|
247
|
+
const column = this.column(table, name);
|
|
248
|
+
if (value === null) return {
|
|
249
|
+
kind: "column",
|
|
250
|
+
column,
|
|
251
|
+
op: "is",
|
|
252
|
+
value: null
|
|
253
|
+
};
|
|
254
|
+
if (!isOpsObject(value)) return {
|
|
255
|
+
kind: "column",
|
|
256
|
+
column,
|
|
257
|
+
op: "eq",
|
|
258
|
+
value: encodeValue(value)
|
|
259
|
+
};
|
|
260
|
+
const items = [];
|
|
261
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
262
|
+
if (operand === void 0) continue;
|
|
263
|
+
items.push(this.fieldOp(table, name, column, op, operand));
|
|
264
|
+
}
|
|
265
|
+
return and(...items) ?? {
|
|
266
|
+
kind: "and",
|
|
267
|
+
items: []
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
fieldOp(table, name, column, op, operand) {
|
|
271
|
+
const col = (kind, value) => ({
|
|
272
|
+
kind: "column",
|
|
273
|
+
column,
|
|
274
|
+
op: kind,
|
|
275
|
+
value: encodeValue(value)
|
|
276
|
+
});
|
|
277
|
+
const meta = table.columns[name];
|
|
278
|
+
switch (op) {
|
|
279
|
+
case "eq": return operand === null ? col("is", null) : col("eq", operand);
|
|
280
|
+
case "neq": return operand === null ? not(col("is", null)) : col("neq", operand);
|
|
281
|
+
case "in":
|
|
282
|
+
case "notIn": {
|
|
283
|
+
if (!Array.isArray(operand)) invalidRequest(`"${op}" on "${name}" needs an array`, table.key);
|
|
284
|
+
const condition = col("in", operand);
|
|
285
|
+
return op === "in" ? condition : not(condition);
|
|
286
|
+
}
|
|
287
|
+
case "isNull": return operand ? col("is", null) : not(col("is", null));
|
|
288
|
+
case "not": {
|
|
289
|
+
const inner = this.fieldFilter(table, name, operand);
|
|
290
|
+
return not(inner);
|
|
291
|
+
}
|
|
292
|
+
case "gt":
|
|
293
|
+
case "gte":
|
|
294
|
+
case "lt":
|
|
295
|
+
case "lte":
|
|
296
|
+
case "like":
|
|
297
|
+
case "ilike": return col(op, operand);
|
|
298
|
+
case "contains":
|
|
299
|
+
if (meta?.json || meta?.array) return col("contains", operand);
|
|
300
|
+
return col("ilike", `%${escapeLike(String(operand))}%`);
|
|
301
|
+
case "startsWith": return col("like", `${escapeLike(String(operand))}%`);
|
|
302
|
+
case "endsWith": return col("like", `%${escapeLike(String(operand))}`);
|
|
303
|
+
case "search": {
|
|
304
|
+
if (typeof operand === "string") return col("fts", operand);
|
|
305
|
+
const { query, config } = operand;
|
|
306
|
+
return config ? {
|
|
307
|
+
kind: "column",
|
|
308
|
+
column,
|
|
309
|
+
op: "fts",
|
|
310
|
+
value: query,
|
|
311
|
+
config
|
|
312
|
+
} : col("fts", query);
|
|
313
|
+
}
|
|
314
|
+
case "hasEvery": return col("contains", operand);
|
|
315
|
+
case "has": return col("contains", [operand]);
|
|
316
|
+
case "hasSome": return col("overlaps", operand);
|
|
317
|
+
case "containedBy": return col("containedBy", operand);
|
|
318
|
+
default: return invalidRequest(`Unknown operator "${op}" on "${name}"`, table.key);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
selection(table, select, include) {
|
|
322
|
+
const columns = (select ?? Object.keys(table.columns)).map((alias) => this.selectColumn(table, alias));
|
|
323
|
+
const includes = [];
|
|
324
|
+
if (include !== void 0) {
|
|
325
|
+
if (!isPlainObject(include)) invalidRequest(`"include" on "${table.key}" must be an object`, table.key);
|
|
326
|
+
for (const [name, value] of Object.entries(include)) {
|
|
327
|
+
if (value === void 0 || value === false) continue;
|
|
328
|
+
const fn = AGGREGATE_KEYS[name];
|
|
329
|
+
if (name === "_count") includes.push(...this.counts(table, value));
|
|
330
|
+
else if (fn) includes.push(...this.relationAggregates(table, fn, value));
|
|
331
|
+
else includes.push(this.include(table, name, value));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
columns,
|
|
336
|
+
includes
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* `db.x.aggregate(args)`: the grouping columns, `_count` and the measures
|
|
341
|
+
* of each group.
|
|
342
|
+
*/
|
|
343
|
+
aggregation(table, args) {
|
|
344
|
+
const groupBy = args.groupBy ?? [];
|
|
345
|
+
if (!Array.isArray(groupBy)) invalidRequest(`"groupBy" on "${table.key}" must be an array`, table.key);
|
|
346
|
+
const columns = groupBy.map((name) => this.selectColumn(table, String(name)));
|
|
347
|
+
const measures = [];
|
|
348
|
+
for (const [key, fn] of Object.entries(AGGREGATE_KEYS)) {
|
|
349
|
+
const value = args[key];
|
|
350
|
+
if (value === void 0) continue;
|
|
351
|
+
for (const alias of this.measured(table, key, value)) measures.push(this.measure(table, fn, alias, `${key}_${alias}`));
|
|
352
|
+
}
|
|
353
|
+
const count = args._count === true;
|
|
354
|
+
if (!count && measures.length === 0) invalidRequest(`aggregate on "${table.key}" needs _count, _sum, _avg, _min or _max`, table.key);
|
|
355
|
+
return {
|
|
356
|
+
columns,
|
|
357
|
+
includes: [],
|
|
358
|
+
aggregate: {
|
|
359
|
+
count,
|
|
360
|
+
measures
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
relationAggregates(table, fn, value) {
|
|
365
|
+
const key = `_${fn}`;
|
|
366
|
+
if (!isPlainObject(value)) invalidRequest(`"${key}" on "${table.key}" must map relations to columns`, table.key);
|
|
367
|
+
const includes = [];
|
|
368
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
369
|
+
if (entry === void 0 || entry === false) continue;
|
|
370
|
+
const { relation, target } = relationMeta(this.meta, table, name);
|
|
371
|
+
if (relation.kind !== "many") invalidRequest(`"${key}.${name}" on "${table.key}" needs a to-many relation`, table.key);
|
|
372
|
+
const measures = this.measured(target, `${key}.${name}`, entry).map((alias) => this.measure(target, fn, alias, alias));
|
|
373
|
+
if (measures.length === 0) continue;
|
|
374
|
+
includes.push({
|
|
375
|
+
alias: `${key}_${name}`,
|
|
376
|
+
relation,
|
|
377
|
+
target,
|
|
378
|
+
selection: {
|
|
379
|
+
columns: [],
|
|
380
|
+
includes: [],
|
|
381
|
+
aggregate: {
|
|
382
|
+
count: false,
|
|
383
|
+
measures
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
where: void 0,
|
|
387
|
+
orderBy: [],
|
|
388
|
+
limit: void 0,
|
|
389
|
+
required: false,
|
|
390
|
+
aggregate: {
|
|
391
|
+
fn,
|
|
392
|
+
name
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return includes;
|
|
397
|
+
}
|
|
398
|
+
/** The columns switched on in `{ amount: true, tax: true }`. */
|
|
399
|
+
measured(table, path, value) {
|
|
400
|
+
if (!isPlainObject(value)) invalidRequest(`"${path}" on "${table.key}" must map columns to true`, table.key);
|
|
401
|
+
return Object.entries(value).filter(([, on]) => on === true).map(([alias]) => alias);
|
|
402
|
+
}
|
|
403
|
+
measure(table, fn, alias, key) {
|
|
404
|
+
const column = this.column(table, alias);
|
|
405
|
+
const meta = table.columns[alias];
|
|
406
|
+
if ((fn === "sum" || fn === "avg") && !NUMERIC_TYPES.has(meta?.type ?? "")) invalidRequest(`_${fn} needs a numeric column; "${alias}" on "${table.key}" is ${meta?.type}`, table.key);
|
|
407
|
+
const codec = fn === "avg" ? void 0 : meta?.codec;
|
|
408
|
+
if (!codec) return {
|
|
409
|
+
fn,
|
|
410
|
+
key,
|
|
411
|
+
alias,
|
|
412
|
+
column
|
|
413
|
+
};
|
|
414
|
+
return codec === "date" ? {
|
|
415
|
+
fn,
|
|
416
|
+
key,
|
|
417
|
+
alias,
|
|
418
|
+
column,
|
|
419
|
+
codec
|
|
420
|
+
} : {
|
|
421
|
+
fn,
|
|
422
|
+
key,
|
|
423
|
+
alias,
|
|
424
|
+
column,
|
|
425
|
+
cast: "text",
|
|
426
|
+
codec
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/** A selected column, with the cast and codec its metadata asks for. */
|
|
430
|
+
selectColumn(table, alias) {
|
|
431
|
+
const column = this.column(table, alias);
|
|
432
|
+
const codec = table.columns[alias]?.codec;
|
|
433
|
+
if (!codec) return {
|
|
434
|
+
alias,
|
|
435
|
+
column
|
|
436
|
+
};
|
|
437
|
+
return codec === "date" ? {
|
|
438
|
+
alias,
|
|
439
|
+
column,
|
|
440
|
+
codec
|
|
441
|
+
} : {
|
|
442
|
+
alias,
|
|
443
|
+
column,
|
|
444
|
+
cast: "text",
|
|
445
|
+
codec
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
counts(table, value) {
|
|
449
|
+
if (!isPlainObject(value)) invalidRequest(`"_count" on "${table.key}" must map relations to true or { where }`, table.key);
|
|
450
|
+
const counts = [];
|
|
451
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
452
|
+
if (entry === void 0 || entry === false) continue;
|
|
453
|
+
const { relation, target } = relationMeta(this.meta, table, name);
|
|
454
|
+
if (relation.kind !== "many") invalidRequest(`"_count.${name}" on "${table.key}" needs a to-many relation`, table.key);
|
|
455
|
+
const args = isPlainObject(entry) ? entry : {};
|
|
456
|
+
counts.push({
|
|
457
|
+
alias: `_count_${name}`,
|
|
458
|
+
relation,
|
|
459
|
+
target,
|
|
460
|
+
selection: {
|
|
461
|
+
columns: [],
|
|
462
|
+
includes: []
|
|
463
|
+
},
|
|
464
|
+
where: this.where(target, args.where),
|
|
465
|
+
orderBy: [],
|
|
466
|
+
limit: void 0,
|
|
467
|
+
required: false,
|
|
468
|
+
count: name
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return counts;
|
|
472
|
+
}
|
|
473
|
+
include(table, name, value) {
|
|
474
|
+
const { relation, target } = relationMeta(this.meta, table, name);
|
|
475
|
+
const args = value === true ? {} : isPlainObject(value) ? value : {};
|
|
476
|
+
return {
|
|
477
|
+
alias: name,
|
|
478
|
+
relation,
|
|
479
|
+
target,
|
|
480
|
+
selection: this.selection(target, args.select, args.include),
|
|
481
|
+
where: this.where(target, args.where),
|
|
482
|
+
orderBy: this.orderBy(target, args.orderBy),
|
|
483
|
+
limit: typeof args.limit === "number" ? args.limit : void 0,
|
|
484
|
+
required: args.required === true
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
orderBy(table, input) {
|
|
488
|
+
if (input === void 0) return [];
|
|
489
|
+
const list = Array.isArray(input) ? input : [input];
|
|
490
|
+
const terms = [];
|
|
491
|
+
for (const item of list) {
|
|
492
|
+
if (!isPlainObject(item)) invalidRequest(`"orderBy" on "${table.key}" must be an object`, table.key);
|
|
493
|
+
for (const [name, spec] of Object.entries(item)) {
|
|
494
|
+
if (spec === void 0) continue;
|
|
495
|
+
const column = this.column(table, name);
|
|
496
|
+
if (spec === "asc" || spec === "desc") terms.push({
|
|
497
|
+
column,
|
|
498
|
+
direction: spec
|
|
499
|
+
});
|
|
500
|
+
else if (isPlainObject(spec)) {
|
|
501
|
+
const direction = spec.direction === "desc" ? "desc" : "asc";
|
|
502
|
+
const nulls = spec.nulls;
|
|
503
|
+
terms.push(nulls === "first" || nulls === "last" ? {
|
|
504
|
+
column,
|
|
505
|
+
direction,
|
|
506
|
+
nulls
|
|
507
|
+
} : {
|
|
508
|
+
column,
|
|
509
|
+
direction
|
|
510
|
+
});
|
|
511
|
+
} else invalidRequest(`Invalid sort for "${name}" on "${table.key}"`, table.key);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return terms;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Maps an app-cased row to database column names. Rejects columns the
|
|
518
|
+
* database won't accept for `mode`: generated columns, `identity always`
|
|
519
|
+
* columns and read-only view columns.
|
|
520
|
+
*/
|
|
521
|
+
row(table, input, mode = "insert") {
|
|
522
|
+
if (!isPlainObject(input)) invalidRequest(`Row for "${table.key}" must be an object`, table.key);
|
|
523
|
+
const out = {};
|
|
524
|
+
for (const [name, value] of Object.entries(input)) {
|
|
525
|
+
if (value === void 0) continue;
|
|
526
|
+
const column = this.column(table, name);
|
|
527
|
+
const meta = table.columns[name];
|
|
528
|
+
if (meta && !writable(meta, mode)) invalidRequest(`"${name}" on "${table.key}" is read-only${meta.generated ? " (generated)" : meta.identity === "always" ? " (identity always)" : ""} and can't be set in an ${mode}`, table.key);
|
|
529
|
+
out[column] = encodeValue(value);
|
|
530
|
+
}
|
|
531
|
+
return out;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Condition matching exactly one row by the primary key or by the columns
|
|
535
|
+
* of a named unique key. The given columns must be one complete key.
|
|
536
|
+
*/
|
|
537
|
+
uniqueKey(table, input) {
|
|
538
|
+
if (!isPlainObject(input)) invalidRequest(`"where" on "${table.key}" must be an object`, table.key);
|
|
539
|
+
const given = Object.keys(input).filter((key) => input[key] !== void 0);
|
|
540
|
+
const keys = [table.primaryKey, ...Object.values(table.uniqueKeys)];
|
|
541
|
+
const match = keys.find((key) => key.length > 0 && key.length === given.length && key.every((column) => given.includes(column)));
|
|
542
|
+
if (!match) {
|
|
543
|
+
const names = keys.filter((key) => key.length > 0).map((key) => `{ ${key.join(", ")} }`).join(", ");
|
|
544
|
+
invalidRequest(`findUnique on "${table.key}" needs one complete unique key: ${names}`, table.key);
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
kind: "and",
|
|
548
|
+
items: match.map((name) => ({
|
|
549
|
+
kind: "column",
|
|
550
|
+
column: this.column(table, name),
|
|
551
|
+
op: input[name] === null ? "is" : "eq",
|
|
552
|
+
value: encodeValue(input[name])
|
|
553
|
+
}))
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/** Condition matching one primary key value. */
|
|
557
|
+
primaryKey(table, id) {
|
|
558
|
+
const pk = table.primaryKey;
|
|
559
|
+
if (pk.length === 0) invalidRequest(`Table "${table.key}" has no primary key`, table.key);
|
|
560
|
+
if (pk.length === 1) {
|
|
561
|
+
const [name] = pk;
|
|
562
|
+
const value = isPlainObject(id) && name in id ? id[name] : id;
|
|
563
|
+
return {
|
|
564
|
+
kind: "column",
|
|
565
|
+
column: this.column(table, name),
|
|
566
|
+
op: "eq",
|
|
567
|
+
value: encodeValue(value)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (!isPlainObject(id)) invalidRequest(`Table "${table.key}" has a composite key; pass { ${pk.join(", ")} }`, table.key);
|
|
571
|
+
return {
|
|
572
|
+
kind: "and",
|
|
573
|
+
items: pk.map((name) => ({
|
|
574
|
+
kind: "column",
|
|
575
|
+
column: this.column(table, name),
|
|
576
|
+
op: "eq",
|
|
577
|
+
value: encodeValue(id[name])
|
|
578
|
+
}))
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/ir/simplify.ts
|
|
584
|
+
/**
|
|
585
|
+
* Folds constant conditions. `true` means "no filter", `false` means "matches
|
|
586
|
+
* nothing", so executors can skip the request entirely.
|
|
587
|
+
*/
|
|
588
|
+
function simplify(condition) {
|
|
589
|
+
if (condition === void 0) return true;
|
|
590
|
+
switch (condition.kind) {
|
|
591
|
+
case "and": {
|
|
592
|
+
const items = [];
|
|
593
|
+
for (const item of condition.items) {
|
|
594
|
+
const simple = simplify(item);
|
|
595
|
+
if (simple === false) return false;
|
|
596
|
+
if (simple !== true) items.push(simple);
|
|
597
|
+
}
|
|
598
|
+
if (items.length === 0) return true;
|
|
599
|
+
return items.length === 1 && items[0] ? items[0] : {
|
|
600
|
+
kind: "and",
|
|
601
|
+
items
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
case "or": {
|
|
605
|
+
const items = [];
|
|
606
|
+
for (const item of condition.items) {
|
|
607
|
+
const simple = simplify(item);
|
|
608
|
+
if (simple === true) return true;
|
|
609
|
+
if (simple !== false) items.push(simple);
|
|
610
|
+
}
|
|
611
|
+
if (items.length === 0) return false;
|
|
612
|
+
return items.length === 1 && items[0] ? items[0] : {
|
|
613
|
+
kind: "or",
|
|
614
|
+
items
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
case "not": {
|
|
618
|
+
const inner = simplify(condition.item);
|
|
619
|
+
if (typeof inner === "boolean") return !inner;
|
|
620
|
+
if (inner.kind === "not") return inner.item;
|
|
621
|
+
return {
|
|
622
|
+
kind: "not",
|
|
623
|
+
item: inner
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
case "relation": {
|
|
627
|
+
const where = simplify(condition.where);
|
|
628
|
+
if (where === true) {
|
|
629
|
+
if (condition.quantifier === "every") return true;
|
|
630
|
+
return {
|
|
631
|
+
...condition,
|
|
632
|
+
where: void 0
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (where === false) {
|
|
636
|
+
if (condition.quantifier === "some") return false;
|
|
637
|
+
if (condition.quantifier === "none") return true;
|
|
638
|
+
return {
|
|
639
|
+
...condition,
|
|
640
|
+
quantifier: "none",
|
|
641
|
+
where: void 0
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
...condition,
|
|
646
|
+
where
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
case "column":
|
|
650
|
+
if (condition.op === "in" && Array.isArray(condition.value) && condition.value.length === 0) return false;
|
|
651
|
+
return condition;
|
|
652
|
+
default: return condition;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/** Returns the condition, or `undefined` when it folds to `true`. */
|
|
656
|
+
function simplifyOrFalse(condition) {
|
|
657
|
+
const simple = simplify(condition);
|
|
658
|
+
if (simple === false) return { never: true };
|
|
659
|
+
return {
|
|
660
|
+
never: false,
|
|
661
|
+
condition: simple === true ? void 0 : simple
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
export { invalidRequest as a, needsDecoding as c, escapeLike as i, defineSchema as l, simplifyOrFalse as n, decodeRows as o, IrBuilder as r, encodeValue as s, simplify as t, tableMeta as u };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/core/spec-pins.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Versions of evolving specs and conventions the code follows. Moving a pin
|
|
4
|
+
* is a minor release with a changeset (see the standards docs page).
|
|
5
|
+
*/
|
|
6
|
+
declare const SPEC_PINS: {
|
|
7
|
+
readonly otelSemconv: "1.37.0";
|
|
8
|
+
readonly mcp: "2025-06-18";
|
|
9
|
+
readonly openapi: "3.1.1";
|
|
10
|
+
readonly cloudevents: "1.0.2";
|
|
11
|
+
readonly standardWebhooks: "1.0.0";
|
|
12
|
+
readonly sarif: "2.1.0";
|
|
13
|
+
readonly jsonSchema: "2020-12";
|
|
14
|
+
readonly postgrestAggregates: "12";
|
|
15
|
+
readonly serverTiming: "WD-20260407";
|
|
16
|
+
readonly stripeSyncEngine: "0.48.5";
|
|
17
|
+
readonly pgvector: "0.8";
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { SPEC_PINS as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/core/spec-pins.ts
|
|
2
|
+
/**
|
|
3
|
+
* Versions of evolving specs and conventions the code follows. Moving a pin
|
|
4
|
+
* is a minor release with a changeset (see the standards docs page).
|
|
5
|
+
*/
|
|
6
|
+
const SPEC_PINS = {
|
|
7
|
+
otelSemconv: "1.37.0",
|
|
8
|
+
mcp: "2025-06-18",
|
|
9
|
+
openapi: "3.1.1",
|
|
10
|
+
cloudevents: "1.0.2",
|
|
11
|
+
standardWebhooks: "1.0.0",
|
|
12
|
+
sarif: "2.1.0",
|
|
13
|
+
jsonSchema: "2020-12",
|
|
14
|
+
postgrestAggregates: "12",
|
|
15
|
+
serverTiming: "WD-20260407",
|
|
16
|
+
stripeSyncEngine: "0.48.5",
|
|
17
|
+
pgvector: "0.8"
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { SPEC_PINS as t };
|