@lunora/do 0.0.0 → 1.0.0-alpha.10

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.
Files changed (50) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +115 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/index.d.mts +6347 -0
  5. package/dist/index.d.ts +6347 -0
  6. package/dist/index.mjs +37 -0
  7. package/dist/packem_shared/ADMIN_FUNCTIONS-D_UiYJFk.mjs +316 -0
  8. package/dist/packem_shared/AGGREGATE_SQL_FUNCTION-CFk6adSu.mjs +54 -0
  9. package/dist/packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs +84 -0
  10. package/dist/packem_shared/CDC_LOG_TABLE-DSycmnDf.mjs +107 -0
  11. package/dist/packem_shared/ConflictError-C0STs6bU.mjs +13 -0
  12. package/dist/packem_shared/CountRlsUnsupportedError-28ZvvwKS.mjs +133 -0
  13. package/dist/packem_shared/DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs +237 -0
  14. package/dist/packem_shared/DEFAULT_MAX_RELATION_KEYS-DU-Y4-LJ.mjs +209 -0
  15. package/dist/packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs +248 -0
  16. package/dist/packem_shared/LogBuffer-B_Ezju_N.mjs +37 -0
  17. package/dist/packem_shared/MAIL_RETENTION-CPpgl-dX.mjs +104 -0
  18. package/dist/packem_shared/MAX_SQL_ROWS-dDcFE1YZ.mjs +29 -0
  19. package/dist/packem_shared/MIN_ADMIN_TOKEN_LENGTH-CCAvoFlr.mjs +1 -0
  20. package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +10 -0
  21. package/dist/packem_shared/NotUniqueError-DZQtH02h.mjs +1835 -0
  22. package/dist/packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs +91 -0
  23. package/dist/packem_shared/RLS_UNWRAP_SYMBOL-EtGQdC9d.mjs +132 -0
  24. package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-DKwBF3Jp.mjs +4996 -0
  25. package/dist/packem_shared/ReactiveCache-1hDydFyv.mjs +232 -0
  26. package/dist/packem_shared/SCAN_DEP-DLJF8dsj.mjs +19 -0
  27. package/dist/packem_shared/SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs +180 -0
  28. package/dist/packem_shared/SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs +146 -0
  29. package/dist/packem_shared/aggregateTableName-CxNqY1Sl.mjs +64 -0
  30. package/dist/packem_shared/applyOnDelete-BQ-8ZlZ1.mjs +175 -0
  31. package/dist/packem_shared/applySelect-BvZdFUBT.mjs +101 -0
  32. package/dist/packem_shared/armRestore-BJk53Ro8.mjs +55 -0
  33. package/dist/packem_shared/backfillAggregateIndexes-BZsOqDXP.mjs +81 -0
  34. package/dist/packem_shared/buildFtsMatch-BLEMawrp.mjs +38 -0
  35. package/dist/packem_shared/compileWhereSql-CXrhFA3G.mjs +127 -0
  36. package/dist/packem_shared/createSystemReader-8CzSZP9V.mjs +80 -0
  37. package/dist/packem_shared/ctx-db-idempotency-BdcNpvY4.mjs +108 -0
  38. package/dist/packem_shared/ctx-db-shapes-DVoeZpo-.mjs +53 -0
  39. package/dist/packem_shared/do-exec-5eQy5cEi.mjs +12 -0
  40. package/dist/packem_shared/do-sql-BCHCWtrD.mjs +87 -0
  41. package/dist/packem_shared/exportShardRows-DZEhUeyI.mjs +156 -0
  42. package/dist/packem_shared/hasTrigger-5N6_Fx0A.mjs +20 -0
  43. package/dist/packem_shared/renderSql-D6eUcn2N.mjs +16 -0
  44. package/dist/packem_shared/runShardMigrations-nIwoQeOK.mjs +105 -0
  45. package/dist/packem_shared/security-audit-CucgBice.mjs +158 -0
  46. package/dist/packem_shared/serialize-sql-BlRUoiQe.mjs +14 -0
  47. package/dist/packem_shared/serveRelationFanout-C6lDaesn.mjs +24 -0
  48. package/dist/packem_shared/stableStringify-CyHKJXre.mjs +30 -0
  49. package/dist/packem_shared/subscriptionListDeltas-ce84gpwL.mjs +111 -0
  50. package/package.json +41 -17
@@ -0,0 +1,133 @@
1
+ class CountRlsUnsupportedError extends Error {
2
+ code = "COUNT_RLS_UNSUPPORTED";
3
+ name = "LunoraError";
4
+ status = 422;
5
+ constructor(table) {
6
+ super(
7
+ table === void 0 ? "count() is not supported in an RLS-restricted context" : `count() is not supported on table "${table}" inside an RLS-restricted context`
8
+ );
9
+ }
10
+ }
11
+ const mergeWhere = (left, right) => {
12
+ if (!left) {
13
+ return right;
14
+ }
15
+ if (!right) {
16
+ return left;
17
+ }
18
+ return { AND: [left, right] };
19
+ };
20
+ const BOOLEAN_COMBINATORS = /* @__PURE__ */ new Set(["AND", "NOT", "OR"]);
21
+ const NOT_EQ = /* @__PURE__ */ Symbol("not-eq");
22
+ const resolveEqValue = (value) => {
23
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
24
+ const operatorKeys = Object.keys(value);
25
+ if (operatorKeys.length === 1 && operatorKeys[0] === "eq") {
26
+ return value.eq;
27
+ }
28
+ return NOT_EQ;
29
+ }
30
+ return value;
31
+ };
32
+ const parseRequestedEqKeys = (requested, accept) => {
33
+ const resolved = {};
34
+ for (const [key, raw] of Object.entries(requested)) {
35
+ if (BOOLEAN_COMBINATORS.has(key) || !accept(key)) {
36
+ return void 0;
37
+ }
38
+ const value = resolveEqValue(raw);
39
+ if (value === NOT_EQ) {
40
+ return void 0;
41
+ }
42
+ resolved[key] = value;
43
+ }
44
+ return resolved;
45
+ };
46
+ const reconcileStaticWhere = (staticWhere, resolved, requested, crossCheckRequested) => {
47
+ const merged = { ...resolved };
48
+ if (!staticWhere) {
49
+ return merged;
50
+ }
51
+ for (const [key, value] of Object.entries(staticWhere)) {
52
+ if (key in merged) {
53
+ if (merged[key] !== value) {
54
+ return void 0;
55
+ }
56
+ } else if (crossCheckRequested && key in requested) {
57
+ if (requested[key] !== value) {
58
+ return void 0;
59
+ }
60
+ } else {
61
+ merged[key] = value;
62
+ }
63
+ }
64
+ return merged;
65
+ };
66
+ const planAggregateLookup = (index, requestedWhere) => {
67
+ const by = index.by ?? [];
68
+ const requested = requestedWhere ?? {};
69
+ const resolved = parseRequestedEqKeys(requested, (key) => by.includes(key));
70
+ if (resolved === void 0) {
71
+ return void 0;
72
+ }
73
+ for (const key of by) {
74
+ if (!(key in resolved)) {
75
+ return void 0;
76
+ }
77
+ }
78
+ return reconcileStaticWhere(index.where, resolved, requested, true);
79
+ };
80
+ const collectPartialKey = (index, requestedWhere, byFields) => {
81
+ const partial = parseRequestedEqKeys(requestedWhere ?? {}, (key) => byFields.has(key));
82
+ if (partial === void 0) {
83
+ return void 0;
84
+ }
85
+ return reconcileStaticWhere(index.where, partial, {}, false);
86
+ };
87
+ const selectIndexForReducer = (indexes, op, field, requestedWhere) => {
88
+ let best;
89
+ for (const index of indexes) {
90
+ if (index.op !== op) {
91
+ continue;
92
+ }
93
+ if (op !== "count" && index.field !== field) {
94
+ continue;
95
+ }
96
+ const key = planAggregateLookup(index, requestedWhere);
97
+ if (!key) {
98
+ continue;
99
+ }
100
+ if (!best || (index.by?.length ?? 0) > (best.index.by?.length ?? 0)) {
101
+ best = { index, key };
102
+ }
103
+ }
104
+ return best;
105
+ };
106
+ const selectIndexForCount = (indexes, requestedWhere) => selectIndexForReducer(indexes, "count", void 0, requestedWhere);
107
+ const selectIndexForAggregate = (indexes, op, field, requestedWhere) => selectIndexForReducer(indexes, op, field, requestedWhere);
108
+ const selectIndexForGroupBy = (indexes, op, field, by, requestedWhere) => {
109
+ const requestedFields = new Set(by);
110
+ for (const index of indexes) {
111
+ if (index.op !== op) {
112
+ continue;
113
+ }
114
+ if (op !== "count" && index.field !== field) {
115
+ continue;
116
+ }
117
+ const indexBy = index.by ?? [];
118
+ if (indexBy.length !== requestedFields.size) {
119
+ continue;
120
+ }
121
+ if (!indexBy.every((key) => requestedFields.has(key))) {
122
+ continue;
123
+ }
124
+ const partial = collectPartialKey(index, requestedWhere, requestedFields);
125
+ if (partial === void 0) {
126
+ continue;
127
+ }
128
+ return { index, partial };
129
+ }
130
+ return void 0;
131
+ };
132
+
133
+ export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy };
@@ -0,0 +1,237 @@
1
+ const DATA_MIGRATION_STATE_TABLE = "__lunora_migrations";
2
+ const DEFAULT_BATCH_SIZE = 100;
3
+ const STALE_CLAIM_TIMEOUT_MS = 3e4;
4
+ const CLAIM_HEARTBEAT_INTERVAL_MS = 1e4;
5
+ const runSql = (sql, query, ...params) => {
6
+ const runner = sql.exec;
7
+ return runner.call(sql, query, ...params);
8
+ };
9
+ const ensureStateTable = (sql) => {
10
+ runSql(
11
+ sql,
12
+ `CREATE TABLE IF NOT EXISTS "${DATA_MIGRATION_STATE_TABLE}" (
13
+ id TEXT PRIMARY KEY,
14
+ direction TEXT NOT NULL,
15
+ status TEXT NOT NULL,
16
+ cursor TEXT,
17
+ processed INTEGER NOT NULL DEFAULT 0,
18
+ changed INTEGER NOT NULL DEFAULT 0,
19
+ started_at REAL,
20
+ updated_at REAL,
21
+ error TEXT
22
+ )`
23
+ );
24
+ };
25
+ const readState = (sql, id) => {
26
+ const rows = runSql(sql, `SELECT * FROM "${DATA_MIGRATION_STATE_TABLE}" WHERE id = ?`, id).toArray();
27
+ const row = rows[0];
28
+ if (!row) {
29
+ return void 0;
30
+ }
31
+ return {
32
+ changed: row.changed,
33
+ // eslint-disable-next-line unicorn/no-null -- mirrors the SQLite `cursor` column: a missing cursor is NULL, not undefined
34
+ cursor: typeof row.cursor === "string" ? row.cursor : null,
35
+ direction: row.direction === "down" ? "down" : "up",
36
+ processed: row.processed,
37
+ startedAt: typeof row.started_at === "number" ? row.started_at : void 0,
38
+ status: row.status === "completed" || row.status === "failed" ? row.status : "in_progress"
39
+ };
40
+ };
41
+ const deleteState = (sql, id) => {
42
+ runSql(sql, `DELETE FROM "${DATA_MIGRATION_STATE_TABLE}" WHERE id = ?`, id);
43
+ };
44
+ const persistState = (sql, state) => {
45
+ runSql(
46
+ sql,
47
+ `INSERT INTO "${DATA_MIGRATION_STATE_TABLE}"
48
+ (id, direction, status, cursor, processed, changed, started_at, updated_at, error)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
50
+ ON CONFLICT(id) DO UPDATE SET
51
+ direction = excluded.direction,
52
+ status = excluded.status,
53
+ cursor = excluded.cursor,
54
+ processed = excluded.processed,
55
+ changed = excluded.changed,
56
+ updated_at = excluded.updated_at,
57
+ error = excluded.error`,
58
+ state.id,
59
+ state.direction,
60
+ state.status,
61
+ state.cursor,
62
+ state.processed,
63
+ state.changed,
64
+ state.startedAt,
65
+ state.updatedAt,
66
+ state.error
67
+ );
68
+ };
69
+ const claimMigration = (sql, id, direction, now) => {
70
+ runSql(
71
+ sql,
72
+ `INSERT INTO "${DATA_MIGRATION_STATE_TABLE}"
73
+ (id, direction, status, cursor, processed, changed, started_at, updated_at, error)
74
+ VALUES (?, ?, 'in_progress', NULL, 0, 0, ?, ?, NULL)
75
+ ON CONFLICT(id) DO UPDATE SET
76
+ status = 'in_progress',
77
+ updated_at = excluded.updated_at
78
+ WHERE
79
+ "${DATA_MIGRATION_STATE_TABLE}".direction <> excluded.direction
80
+ OR "${DATA_MIGRATION_STATE_TABLE}".status <> 'in_progress'
81
+ OR "${DATA_MIGRATION_STATE_TABLE}".updated_at IS NULL
82
+ OR "${DATA_MIGRATION_STATE_TABLE}".updated_at <= excluded.updated_at - ${String(STALE_CLAIM_TIMEOUT_MS)}`,
83
+ id,
84
+ direction,
85
+ now,
86
+ now
87
+ );
88
+ return runSql(sql, `SELECT changes() AS changed`).one().changed > 0;
89
+ };
90
+ const releaseClaim = (sql, id) => {
91
+ runSql(sql, `UPDATE "${DATA_MIGRATION_STATE_TABLE}" SET updated_at = 0 WHERE id = ? AND status = 'in_progress'`, id);
92
+ };
93
+ const touchClaim = (sql, id, now) => {
94
+ runSql(sql, `UPDATE "${DATA_MIGRATION_STATE_TABLE}" SET updated_at = ? WHERE id = ? AND status = 'in_progress'`, now, id);
95
+ };
96
+ const readMigrationStatus = (sql, id) => {
97
+ const exists = runSql(
98
+ sql,
99
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
100
+ DATA_MIGRATION_STATE_TABLE
101
+ ).toArray();
102
+ if (exists.length === 0) {
103
+ return [];
104
+ }
105
+ const filter = id === void 0 ? " ORDER BY id" : " WHERE id = ?";
106
+ const params = id === void 0 ? [] : [id];
107
+ const rows = runSql(sql, `SELECT * FROM "${DATA_MIGRATION_STATE_TABLE}"${filter}`, ...params).toArray();
108
+ return rows.map((row) => {
109
+ return {
110
+ changed: row.changed,
111
+ cursor: typeof row.cursor === "string" ? row.cursor : null,
112
+ direction: row.direction === "down" ? "down" : "up",
113
+ error: typeof row.error === "string" ? row.error : null,
114
+ id: row.id,
115
+ processed: row.processed,
116
+ startedAt: typeof row.started_at === "number" ? row.started_at : null,
117
+ status: row.status === "completed" || row.status === "failed" ? row.status : "in_progress",
118
+ updatedAt: typeof row.updated_at === "number" ? row.updated_at : null
119
+ };
120
+ });
121
+ };
122
+ const runDataMigration = async (options) => {
123
+ const { migration, sql, writer } = options;
124
+ const direction = options.direction ?? "up";
125
+ const dryRun = options.dryRun ?? false;
126
+ const clock = options.clock ?? (() => Date.now());
127
+ const maxBatches = options.maxBatches ?? Number.POSITIVE_INFINITY;
128
+ const batchSize = options.batchSize ?? migration.batchSize ?? DEFAULT_BATCH_SIZE;
129
+ const transform = direction === "up" ? migration.up : migration.down;
130
+ if (!transform) {
131
+ throw new Error(`data migration "${migration.id}" has no \`${direction}\` transform`);
132
+ }
133
+ let cursor = null;
134
+ let processed = 0;
135
+ let changed = 0;
136
+ let startedAt = clock();
137
+ if (!dryRun) {
138
+ ensureStateTable(sql);
139
+ const existing = readState(sql, migration.id);
140
+ if (existing?.direction === direction && existing.status === "completed") {
141
+ return { changed: existing.changed, cursor: null, direction, dryRun, id: migration.id, processed: existing.processed, status: "completed" };
142
+ }
143
+ if (existing && existing.direction !== direction) {
144
+ deleteState(sql, migration.id);
145
+ }
146
+ const claimed = claimMigration(sql, migration.id, direction, clock());
147
+ if (!claimed) {
148
+ const active = readState(sql, migration.id);
149
+ return {
150
+ changed: active?.changed ?? 0,
151
+ cursor: active?.cursor ?? cursor,
152
+ direction,
153
+ dryRun,
154
+ id: migration.id,
155
+ processed: active?.processed ?? 0,
156
+ status: active?.status ?? "in_progress"
157
+ };
158
+ }
159
+ const resume = existing?.direction === direction ? existing : void 0;
160
+ if (resume) {
161
+ cursor = resume.cursor;
162
+ processed = resume.processed;
163
+ changed = resume.changed;
164
+ startedAt = resume.startedAt ?? startedAt;
165
+ }
166
+ }
167
+ let isDone = false;
168
+ let batches = 0;
169
+ let lastHeartbeatAt = startedAt;
170
+ try {
171
+ while (!isDone && batches < maxBatches) {
172
+ const batch = await writer.findMany(migration.table, { cursor, limit: batchSize });
173
+ for (const document of batch.page) {
174
+ processed += 1;
175
+ const next = transform(document);
176
+ if (next !== void 0) {
177
+ changed += 1;
178
+ if (!dryRun) {
179
+ await writer.replace(String(document["_id"]), { ...next, _creationTime: document["_creationTime"], _id: document["_id"] });
180
+ }
181
+ }
182
+ if (!dryRun) {
183
+ const now = clock();
184
+ if (now - lastHeartbeatAt >= CLAIM_HEARTBEAT_INTERVAL_MS) {
185
+ touchClaim(sql, migration.id, now);
186
+ lastHeartbeatAt = now;
187
+ }
188
+ }
189
+ }
190
+ cursor = batch.continueCursor;
191
+ isDone = batch.isDone;
192
+ batches += 1;
193
+ if (!dryRun) {
194
+ const batchEndedAt = clock();
195
+ persistState(sql, {
196
+ changed,
197
+ // eslint-disable-next-line unicorn/no-null -- bound to the SQLite cursor column: a finished run stores NULL
198
+ cursor: isDone ? null : cursor,
199
+ direction,
200
+ // eslint-disable-next-line unicorn/no-null -- bound to the SQLite error column: a clean batch stores NULL
201
+ error: null,
202
+ id: migration.id,
203
+ processed,
204
+ startedAt,
205
+ status: isDone ? "completed" : "in_progress",
206
+ updatedAt: batchEndedAt
207
+ });
208
+ lastHeartbeatAt = batchEndedAt;
209
+ try {
210
+ await options.onBatch?.({ batches, changed, processed });
211
+ } catch {
212
+ }
213
+ }
214
+ }
215
+ } catch (error) {
216
+ if (!dryRun) {
217
+ persistState(sql, {
218
+ changed,
219
+ cursor,
220
+ direction,
221
+ error: error instanceof Error ? error.message : String(error),
222
+ id: migration.id,
223
+ processed,
224
+ startedAt,
225
+ status: "failed",
226
+ updatedAt: clock()
227
+ });
228
+ }
229
+ throw error;
230
+ }
231
+ if (!dryRun && !isDone) {
232
+ releaseClaim(sql, migration.id);
233
+ }
234
+ return { changed, cursor: isDone ? null : cursor, direction, dryRun, id: migration.id, processed, status: isDone ? "completed" : "in_progress" };
235
+ };
236
+
237
+ export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration };
@@ -0,0 +1,209 @@
1
+ import { distinctValues } from './applyOnDelete-BQ-8ZlZ1.mjs';
2
+
3
+ const RELATION_EXISTS_KEY = "__relationExists";
4
+
5
+ const RELATION_OPERATOR_META = {
6
+ every: { kind: "many", negateChild: true, negated: true },
7
+ is: { kind: "one", negated: false },
8
+ isNot: { kind: "one", negated: true, nullDisjunct: true },
9
+ none: { kind: "many", negated: true },
10
+ some: { kind: "many", negated: false }
11
+ };
12
+ const RELATION_OPERATORS = new Set(Object.keys(RELATION_OPERATOR_META));
13
+ const joinColumns = (relation) => relation.kind === "one" ? { clause: relation.field, project: relation.references } : { clause: relation.references, project: relation.field };
14
+ const DEFAULT_MAX_RELATION_KEYS = 5e3;
15
+ const KEY_OVERFLOW = /* @__PURE__ */ Symbol("relation-key-overflow");
16
+ const branchesOf = (value) => Array.isArray(value) ? value.map((branch) => branch ?? {}) : [];
17
+ const combineAnd = (clauses) => {
18
+ if (clauses.length === 1) {
19
+ const [only] = clauses;
20
+ return only ?? {};
21
+ }
22
+ return clauses.length === 0 ? {} : { AND: clauses };
23
+ };
24
+ const isRelationPredicate = (value) => {
25
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
26
+ return false;
27
+ }
28
+ const keys = Object.keys(value);
29
+ return keys.length > 0 && keys.every((key) => RELATION_OPERATORS.has(key));
30
+ };
31
+ const containsRelationPredicate = (where, schema, tableName) => {
32
+ const relationMap = schema.tables[tableName]?.relationMap ?? {};
33
+ return Object.keys(where).some((key) => {
34
+ const value = where[key];
35
+ if (key === "AND" || key === "OR") {
36
+ return branchesOf(value).some((branch) => containsRelationPredicate(branch, schema, tableName));
37
+ }
38
+ if (key === "NOT") {
39
+ return containsRelationPredicate(value ?? {}, schema, tableName);
40
+ }
41
+ return Boolean(relationMap[key]) && isRelationPredicate(value);
42
+ });
43
+ };
44
+ const assertFlatPredicate = (where, schema, tableName, op) => {
45
+ if (where && containsRelationPredicate(where, schema, tableName)) {
46
+ throw new Error(`relation-crossing predicates are not supported in ${op}() — use them in findMany/findFirst or an RLS read policy`);
47
+ }
48
+ };
49
+ const projectChildKeys = async (relation, childWhere, projectField, context, escalatable) => {
50
+ const resolvedChildWhere = await resolveForTable(childWhere, relation.table, context);
51
+ const { page } = await context.fetcher(relation.table, {
52
+ baseWhere: context.relationBaseWhere?.(relation.table),
53
+ relationBaseWhere: context.relationBaseWhere,
54
+ where: resolvedChildWhere
55
+ });
56
+ const keys = distinctValues(page, projectField);
57
+ if (keys.length > context.maxRelationKeys) {
58
+ if (escalatable) {
59
+ return KEY_OVERFLOW;
60
+ }
61
+ throw new Error(
62
+ `relation predicate on "${relation.table}" matched ${String(keys.length)} rows, exceeding the ${String(context.maxRelationKeys)}-key limit; narrow the predicate (a same-shard EXISTS push-down lifts this cap)`
63
+ );
64
+ }
65
+ return keys;
66
+ };
67
+ const compileOperator = async (operator, relation, childWhere, context, escalatable) => {
68
+ const meta = RELATION_OPERATOR_META[operator];
69
+ if (!meta) {
70
+ throw new Error(`unknown relation operator "${operator}"`);
71
+ }
72
+ const { clause, project } = joinColumns(relation);
73
+ const keys = await projectChildKeys(relation, meta.negateChild ? { NOT: childWhere } : childWhere, project, context, escalatable);
74
+ if (keys === KEY_OVERFLOW) {
75
+ return KEY_OVERFLOW;
76
+ }
77
+ if (!meta.negated) {
78
+ return { [clause]: { in: keys } };
79
+ }
80
+ if (meta.nullDisjunct) {
81
+ return { OR: [{ [clause]: { notIn: keys } }, { [clause]: { isNull: true } }] };
82
+ }
83
+ return { [clause]: { notIn: keys } };
84
+ };
85
+ const buildExistsMarker = async (operator, relation, childWhere, parentTable, context) => {
86
+ const meta = RELATION_OPERATOR_META[operator];
87
+ if (!meta) {
88
+ throw new Error(`unknown relation operator "${operator}"`);
89
+ }
90
+ const base = context.relationBaseWhere?.(relation.table);
91
+ const predicatePart = meta.negateChild ? { NOT: childWhere } : childWhere;
92
+ const merged = base ? { AND: [base, predicatePart] } : predicatePart;
93
+ const resolvedChild = await resolveForTable(merged, relation.table, context);
94
+ const marker = { childWhere: resolvedChild, negated: meta.negated, parentTable, relation };
95
+ return { [RELATION_EXISTS_KEY]: marker };
96
+ };
97
+ const assertCardinality = (operator, name, relation) => {
98
+ const meta = RELATION_OPERATOR_META[operator];
99
+ if (meta && meta.kind !== relation.kind) {
100
+ throw new Error(`relation operator "${operator}" requires a to-${meta.kind} relation, but "${name}" is to-${relation.kind}`);
101
+ }
102
+ };
103
+ const resolveRelationNode = async (name, relation, predicate, parentTable, context) => {
104
+ const clauses = [];
105
+ for (const operator of Object.keys(predicate)) {
106
+ assertCardinality(operator, name, relation);
107
+ const childWhere = predicate[operator] ?? {};
108
+ const pushable = context.canPushExists?.(relation) ?? false;
109
+ if (pushable && context.existsPushMode === "always") {
110
+ clauses.push(await buildExistsMarker(operator, relation, childWhere, parentTable, context));
111
+ continue;
112
+ }
113
+ const semijoin = await compileOperator(operator, relation, childWhere, context, pushable);
114
+ if (semijoin === KEY_OVERFLOW) {
115
+ clauses.push(await buildExistsMarker(operator, relation, childWhere, parentTable, context));
116
+ } else {
117
+ clauses.push(semijoin);
118
+ }
119
+ }
120
+ return combineAnd(clauses);
121
+ };
122
+ const resolveKey = async (key, value, tableName, context) => {
123
+ if (key === "AND" || key === "OR") {
124
+ const resolved = [];
125
+ for (const branch of branchesOf(value)) {
126
+ resolved.push(await resolveForTable(branch, tableName, context));
127
+ }
128
+ return { [key]: resolved };
129
+ }
130
+ if (key === "NOT") {
131
+ return { NOT: await resolveForTable(value ?? {}, tableName, context) };
132
+ }
133
+ const relation = context.schema.tables[tableName]?.relationMap?.[key];
134
+ if (relation && isRelationPredicate(value)) {
135
+ return resolveRelationNode(key, relation, value, tableName, context);
136
+ }
137
+ return { [key]: value };
138
+ };
139
+ const resolveForTable = async (where, tableName, context) => {
140
+ const clauses = [];
141
+ for (const key of Object.keys(where)) {
142
+ clauses.push(await resolveKey(key, where[key], tableName, context));
143
+ }
144
+ return combineAnd(clauses);
145
+ };
146
+ const resolveRelationPredicates = async (where, options) => {
147
+ if (!where || !containsRelationPredicate(where, options.schema, options.tableName)) {
148
+ return where;
149
+ }
150
+ return resolveForTable(where, options.tableName, {
151
+ canPushExists: options.canPushExists,
152
+ existsPushMode: options.existsPushMode ?? "auto",
153
+ fetcher: options.fetcher,
154
+ maxRelationKeys: options.maxRelationKeys ?? DEFAULT_MAX_RELATION_KEYS,
155
+ relationBaseWhere: options.relationBaseWhere,
156
+ schema: options.schema
157
+ });
158
+ };
159
+ const firstShardedHit = (branches, schema, tableName) => {
160
+ for (const branch of branches) {
161
+ const hit = findShardedRelationTarget(branch, schema, tableName);
162
+ if (hit) {
163
+ return hit;
164
+ }
165
+ }
166
+ return void 0;
167
+ };
168
+ const inspectKey = (key, value, schema, tableName) => {
169
+ if (key === "AND" || key === "OR") {
170
+ return firstShardedHit(branchesOf(value), schema, tableName);
171
+ }
172
+ if (key === "NOT") {
173
+ return firstShardedHit([value ?? {}], schema, tableName);
174
+ }
175
+ const relation = schema.tables[tableName]?.relationMap?.[key];
176
+ if (!relation || !isRelationPredicate(value)) {
177
+ return void 0;
178
+ }
179
+ if (schema.tables[relation.table]?.shardMode?.kind === "shardBy") {
180
+ return { relation: key, target: relation.table };
181
+ }
182
+ return firstShardedHit(Object.values(value), schema, relation.table);
183
+ };
184
+ const findShardedRelationTarget = (where, schema, tableName) => {
185
+ for (const key of Object.keys(where)) {
186
+ const hit = inspectKey(key, where[key], schema, tableName);
187
+ if (hit) {
188
+ return hit;
189
+ }
190
+ }
191
+ return void 0;
192
+ };
193
+ const assertShapeShardable = (effectiveWhere, schema, table) => {
194
+ if (!effectiveWhere) {
195
+ return;
196
+ }
197
+ const offending = findShardedRelationTarget(effectiveWhere, schema, table);
198
+ if (!offending) {
199
+ return;
200
+ }
201
+ throw Object.assign(
202
+ new Error(
203
+ `shape on "${table}" joins the sharded table "${offending.target}" via relation "${offending.relation}" — a live shape cannot replicate rows that live in another shard's Durable Object. Fix it by (a) denormalizing the joined columns into "${table}", or (b) moving "${offending.target}" to .global() so it is served through the latency-tiered D1 shape tier.`
204
+ ),
205
+ { code: "SHAPE_CROSS_SHARD_JOIN", name: "LunoraError", status: 400 }
206
+ );
207
+ };
208
+
209
+ export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates };