@lunora/do 0.0.0 → 1.0.0-alpha.1

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 (47) 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 +5599 -0
  5. package/dist/index.d.ts +5599 -0
  6. package/dist/index.mjs +35 -0
  7. package/dist/packem_shared/ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs +313 -0
  8. package/dist/packem_shared/AUTH_METRICS_BUCKET_MS-CiHHYeJi.mjs +84 -0
  9. package/dist/packem_shared/ConflictError-C0STs6bU.mjs +13 -0
  10. package/dist/packem_shared/CountRlsUnsupportedError-28ZvvwKS.mjs +133 -0
  11. package/dist/packem_shared/DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs +237 -0
  12. package/dist/packem_shared/LogBuffer-B_Ezju_N.mjs +37 -0
  13. package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +10 -0
  14. package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-DQkmGiCS.mjs +4009 -0
  15. package/dist/packem_shared/ReactiveCache-ByVzgH3d.mjs +259 -0
  16. package/dist/packem_shared/SCAN_DEP-DLJF8dsj.mjs +19 -0
  17. package/dist/packem_shared/SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs +180 -0
  18. package/dist/packem_shared/SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs +146 -0
  19. package/dist/packem_shared/aggregateTableName-CxNqY1Sl.mjs +64 -0
  20. package/dist/packem_shared/applyCdcChanges-Ctdmxmrv.mjs +103 -0
  21. package/dist/packem_shared/applyOnDelete-CMif2RKw.mjs +165 -0
  22. package/dist/packem_shared/armRestore-BJk53Ro8.mjs +55 -0
  23. package/dist/packem_shared/assertFlatPredicate-DyVYReuT.mjs +160 -0
  24. package/dist/packem_shared/assertReadonly-dDcFE1YZ.mjs +29 -0
  25. package/dist/packem_shared/assertValidClientId-CBZ1zC96.mjs +1745 -0
  26. package/dist/packem_shared/backfillAggregateIndexes-BF5eL7kW.mjs +80 -0
  27. package/dist/packem_shared/buildSecurityAudit-CCAvoFlr.mjs +1 -0
  28. package/dist/packem_shared/buildSeekWhere-lVsNXSLy.mjs +84 -0
  29. package/dist/packem_shared/clearCapturedMail-CPpgl-dX.mjs +104 -0
  30. package/dist/packem_shared/compileWhereSql-CXrhFA3G.mjs +127 -0
  31. package/dist/packem_shared/createSystemReader-8CzSZP9V.mjs +80 -0
  32. package/dist/packem_shared/ctx-db-idempotency-DkC9rP91.mjs +35 -0
  33. package/dist/packem_shared/do-exec-5eQy5cEi.mjs +12 -0
  34. package/dist/packem_shared/do-sql-BCHCWtrD.mjs +87 -0
  35. package/dist/packem_shared/encodePartitionKey-C6blLR5K.mjs +1 -0
  36. package/dist/packem_shared/ensureFunctionMetricsTables-UDNVD7FS.mjs +248 -0
  37. package/dist/packem_shared/exportShardRows-DZEhUeyI.mjs +156 -0
  38. package/dist/packem_shared/ftsTableName-BLEMawrp.mjs +38 -0
  39. package/dist/packem_shared/guardWriter-u3UlnCH5.mjs +128 -0
  40. package/dist/packem_shared/matchesStaticWhere-CFk6adSu.mjs +54 -0
  41. package/dist/packem_shared/rank-CrkEIpF4.mjs +102 -0
  42. package/dist/packem_shared/renderSql-D6eUcn2N.mjs +16 -0
  43. package/dist/packem_shared/runShardMigrations-C3bn5r93.mjs +103 -0
  44. package/dist/packem_shared/runTriggers-5N6_Fx0A.mjs +20 -0
  45. package/dist/packem_shared/security-audit-CucgBice.mjs +158 -0
  46. package/dist/packem_shared/serveRelationFanout-Clr1a05L.mjs +24 -0
  47. package/package.json +41 -17
@@ -0,0 +1,103 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
3
+ import { ConflictError } from './ConflictError-C0STs6bU.mjs';
4
+
5
+ const CDC_LOG_TABLE = "__cdc_log";
6
+ const migrateCdcLog = (sql$1) => {
7
+ runDrizzle(
8
+ sql$1,
9
+ sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(CDC_LOG_TABLE)} (
10
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
11
+ ts REAL NOT NULL,
12
+ ${sql.identifier("table")} TEXT NOT NULL,
13
+ id TEXT NOT NULL,
14
+ op TEXT NOT NULL,
15
+ doc TEXT
16
+ )`
17
+ );
18
+ };
19
+ const appendCdcChange = (sql$1, ts, table, id, op, doc) => {
20
+ const docValue = doc === void 0 ? null : JSON.stringify(doc);
21
+ runDrizzle(
22
+ sql$1,
23
+ sql`INSERT INTO ${sql.identifier(CDC_LOG_TABLE)} (ts, ${sql.identifier("table")}, id, op, doc) VALUES (${ts}, ${table}, ${id}, ${op}, ${docValue})`
24
+ );
25
+ };
26
+ const readCdcChanges = (sql$1, options = {}) => {
27
+ const sinceSeq = options.sinceSeq ?? 0;
28
+ const limit = Math.max(1, Math.min(options.limit ?? 1e3, 1e4));
29
+ const rows = runDrizzle(
30
+ sql$1,
31
+ sql`SELECT seq, ts, ${sql.identifier("table")}, id, op, doc FROM ${sql.identifier(CDC_LOG_TABLE)} WHERE seq > ${sinceSeq} ORDER BY seq ASC LIMIT ${limit}`
32
+ ).toArray();
33
+ const changes = rows.map((row) => {
34
+ const base = { id: row.id, op: row.op, seq: row.seq, table: row.table, ts: row.ts };
35
+ return row.doc === null ? base : { ...base, doc: JSON.parse(row.doc) };
36
+ });
37
+ return { changes, cursor: changes.at(-1)?.seq ?? sinceSeq };
38
+ };
39
+ const trimCdcChanges = (sql$1, throughSeq) => {
40
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(CDC_LOG_TABLE)} WHERE seq <= ${throughSeq}`);
41
+ };
42
+ const readCdcCursor = (sql$1) => {
43
+ const seqRow = runDrizzle(sql$1, sql`SELECT seq FROM sqlite_sequence WHERE name = ${CDC_LOG_TABLE}`).toArray();
44
+ const fromSequence = seqRow[0]?.seq;
45
+ if (typeof fromSequence === "number") {
46
+ return fromSequence;
47
+ }
48
+ const rows = runDrizzle(sql$1, sql`SELECT MAX(seq) AS seq FROM ${sql.identifier(CDC_LOG_TABLE)}`).toArray();
49
+ return rows[0]?.seq ?? 0;
50
+ };
51
+ const minCdcSeq = (sql$1) => {
52
+ const rows = runDrizzle(sql$1, sql`SELECT MIN(seq) AS seq FROM ${sql.identifier(CDC_LOG_TABLE)}`).toArray();
53
+ return rows[0]?.seq ?? void 0;
54
+ };
55
+ const CDC_META_TABLE = "__cdc_meta";
56
+ const migrateCdcMeta = (sql$1) => {
57
+ runDrizzle(sql$1, sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(CDC_META_TABLE)} (id INTEGER PRIMARY KEY CHECK (id = 1), epoch TEXT NOT NULL)`);
58
+ };
59
+ const readCdcEpoch = (sql$1) => {
60
+ migrateCdcMeta(sql$1);
61
+ const rows = runDrizzle(sql$1, sql`SELECT epoch FROM ${sql.identifier(CDC_META_TABLE)} WHERE id = 1`).toArray();
62
+ const existing = rows[0]?.epoch;
63
+ if (typeof existing === "string" && existing.length > 0) {
64
+ return existing;
65
+ }
66
+ const minted = crypto.randomUUID();
67
+ runDrizzle(sql$1, sql`INSERT INTO ${sql.identifier(CDC_META_TABLE)} (id, epoch) VALUES (1, ${minted})`);
68
+ return minted;
69
+ };
70
+ const bumpCdcEpoch = (sql$1) => {
71
+ migrateCdcMeta(sql$1);
72
+ const next = crypto.randomUUID();
73
+ runDrizzle(sql$1, sql`INSERT INTO ${sql.identifier(CDC_META_TABLE)} (id, epoch) VALUES (1, ${next}) ON CONFLICT(id) DO UPDATE SET epoch = excluded.epoch`);
74
+ return next;
75
+ };
76
+ const applyCdcChange = async (writer, change) => {
77
+ if (change.op === "delete") {
78
+ await writer.delete(change.id);
79
+ return;
80
+ }
81
+ const document = change.doc ?? {};
82
+ try {
83
+ await writer.insert(change.table, document, { allowExplicitId: true });
84
+ } catch (error) {
85
+ if (!(error instanceof ConflictError)) {
86
+ throw error;
87
+ }
88
+ const fields = {};
89
+ for (const [key, value] of Object.entries(document)) {
90
+ if (key !== "_id") {
91
+ fields[key] = value;
92
+ }
93
+ }
94
+ await writer.replace(change.id, fields);
95
+ }
96
+ };
97
+ const applyCdcChanges = async (writer, changes) => {
98
+ for (const change of changes) {
99
+ await applyCdcChange(writer, change);
100
+ }
101
+ };
102
+
103
+ export { CDC_LOG_TABLE, CDC_META_TABLE, appendCdcChange, applyCdcChanges, bumpCdcEpoch, migrateCdcLog, migrateCdcMeta, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges };
@@ -0,0 +1,165 @@
1
+ const distinctValues = (rows, field) => {
2
+ const seen = /* @__PURE__ */ new Set();
3
+ for (const row of rows) {
4
+ const value = row[field];
5
+ if (value !== null && value !== void 0) {
6
+ seen.add(value);
7
+ }
8
+ }
9
+ return [...seen];
10
+ };
11
+ const resolveWith = async (options) => {
12
+ const { counter, fetcher, parents, relationBaseWhere, schema, tableName, with: withInput } = options;
13
+ if (parents.length === 0) {
14
+ return;
15
+ }
16
+ const parentDefinition = schema.tables[tableName];
17
+ if (!parentDefinition) {
18
+ throw new Error(`unknown table: ${tableName}`);
19
+ }
20
+ const relationMap = parentDefinition.relationMap ?? {};
21
+ const requireRelation = (name) => {
22
+ const relation = relationMap[name];
23
+ if (!relation) {
24
+ throw new Error(`unknown relation "${name}" on table "${tableName}"`);
25
+ }
26
+ return relation;
27
+ };
28
+ const loadOne = async (name, relation, nested) => {
29
+ const fkValues = distinctValues(parents, relation.field);
30
+ if (fkValues.length === 0) {
31
+ for (const parent of parents) {
32
+ parent[name] = null;
33
+ }
34
+ return;
35
+ }
36
+ const { page } = await fetcher(relation.table, {
37
+ baseWhere: relationBaseWhere?.(relation.table),
38
+ relationBaseWhere,
39
+ where: { [relation.references]: { in: fkValues } },
40
+ with: nested.with
41
+ });
42
+ const byReference = /* @__PURE__ */ new Map();
43
+ for (const child of page) {
44
+ byReference.set(child[relation.references], child);
45
+ }
46
+ for (const parent of parents) {
47
+ parent[name] = byReference.get(parent[relation.field]) ?? null;
48
+ }
49
+ };
50
+ const loadMany = async (name, relation, nested) => {
51
+ const referenceValues = distinctValues(parents, relation.references);
52
+ if (referenceValues.length === 0) {
53
+ for (const parent of parents) {
54
+ parent[name] = [];
55
+ }
56
+ return;
57
+ }
58
+ const fkFilter = { [relation.field]: { in: referenceValues } };
59
+ const where = nested.where ? { AND: [nested.where, fkFilter] } : fkFilter;
60
+ const { page } = await fetcher(relation.table, {
61
+ baseWhere: relationBaseWhere?.(relation.table),
62
+ orderBy: nested.orderBy,
63
+ relationBaseWhere,
64
+ where,
65
+ with: nested.with
66
+ });
67
+ const groups = /* @__PURE__ */ new Map();
68
+ for (const child of page) {
69
+ const key = child[relation.field];
70
+ const group = groups.get(key);
71
+ if (group) {
72
+ group.push(child);
73
+ } else {
74
+ groups.set(key, [child]);
75
+ }
76
+ }
77
+ const cap = typeof nested.limit === "number" ? Math.max(0, Math.floor(nested.limit)) : void 0;
78
+ for (const parent of parents) {
79
+ const group = groups.get(parent[relation.references]) ?? [];
80
+ parent[name] = cap === void 0 ? group : group.slice(0, cap);
81
+ }
82
+ };
83
+ const resolveCounts = async (countInput) => {
84
+ for (const name of Object.keys(countInput)) {
85
+ const relation = requireRelation(name);
86
+ const [whereField, parentField] = relation.kind === "many" ? [relation.field, relation.references] : [relation.references, relation.field];
87
+ const countByValue = /* @__PURE__ */ new Map();
88
+ const policyWhere = relationBaseWhere?.(relation.table);
89
+ for (const value of distinctValues(parents, parentField)) {
90
+ const countWhere = policyWhere ? { AND: [{ [whereField]: value }, policyWhere] } : { [whereField]: value };
91
+ countByValue.set(value, await counter(relation.table, countWhere));
92
+ }
93
+ for (const parent of parents) {
94
+ const counts = parent["_count"] ?? {};
95
+ const parentValue = parent[parentField];
96
+ counts[name] = parentValue === null || parentValue === void 0 ? 0 : countByValue.get(parentValue) ?? 0;
97
+ parent["_count"] = counts;
98
+ }
99
+ }
100
+ };
101
+ for (const [name, value] of Object.entries(withInput)) {
102
+ if (value === void 0 || value === false) {
103
+ continue;
104
+ }
105
+ if (name === "_count") {
106
+ await resolveCounts(value);
107
+ continue;
108
+ }
109
+ const relation = requireRelation(name);
110
+ const nested = value === true ? {} : value;
111
+ await (relation.kind === "one" ? loadOne(name, relation, nested) : loadMany(name, relation, nested));
112
+ }
113
+ };
114
+ const applyRelationOnDelete = async (options, holderTable, relation) => {
115
+ const { deletedId, deletedReference, findHolders, onCascade, onRestrict, onSetNull, tableName } = options;
116
+ const referencedValue = relation.references === "_id" ? deletedId : deletedReference(relation.references);
117
+ if (referencedValue === null || referencedValue === void 0) {
118
+ return;
119
+ }
120
+ const holders = await findHolders(holderTable, relation.field, referencedValue);
121
+ if (holders.length === 0) {
122
+ return;
123
+ }
124
+ if (relation.onDelete === "restrict") {
125
+ onRestrict(`cannot delete "${tableName}" row: "${holderTable}.${relation.field}" still references it`);
126
+ }
127
+ for (const holder of holders) {
128
+ const holderId = holder["_id"];
129
+ if (typeof holderId !== "string") {
130
+ continue;
131
+ }
132
+ await (relation.onDelete === "cascade" ? onCascade(holderTable, holderId) : onSetNull(holderTable, holderId, relation.field));
133
+ }
134
+ };
135
+ const applyOnDelete = async (options) => {
136
+ const { schema, tableName } = options;
137
+ if (!schema.tables[tableName]) {
138
+ throw new Error(`unknown table: ${tableName}`);
139
+ }
140
+ for (const [holderTable, holderDefinition] of Object.entries(schema.tables)) {
141
+ const relations = holderDefinition.relationMap;
142
+ if (!relations) {
143
+ continue;
144
+ }
145
+ for (const relation of Object.values(relations)) {
146
+ if (relation.kind !== "one" || relation.table !== tableName || !relation.onDelete) {
147
+ continue;
148
+ }
149
+ await applyRelationOnDelete(options, holderTable, relation);
150
+ }
151
+ }
152
+ };
153
+ const runRowValidators = (definition, document) => {
154
+ for (const [field, validator] of Object.entries(definition.shape)) {
155
+ if (!(field in document)) {
156
+ continue;
157
+ }
158
+ if (typeof validator.parse !== "function") {
159
+ continue;
160
+ }
161
+ validator.parse(document[field]);
162
+ }
163
+ };
164
+
165
+ export { applyOnDelete, distinctValues, resolveWith, runRowValidators };
@@ -0,0 +1,55 @@
1
+ const pitrUnavailable = () => Object.assign(
2
+ new Error("native PITR is unavailable here (local dev or a non-SQLite Durable Object); use `lunora backup restore` for off-platform recovery"),
3
+ {
4
+ code: "PITR_UNAVAILABLE",
5
+ name: "LunoraError",
6
+ status: 409
7
+ }
8
+ );
9
+ const toTime = (time) => {
10
+ if (typeof time === "number") {
11
+ return time;
12
+ }
13
+ const parsed = Date.parse(time);
14
+ if (Number.isNaN(parsed)) {
15
+ throw Object.assign(new Error(`pitr: invalid time "${time}" — expected epoch-ms or an ISO timestamp`), {
16
+ code: "BAD_REQUEST",
17
+ name: "LunoraError",
18
+ status: 400
19
+ });
20
+ }
21
+ return parsed;
22
+ };
23
+ const readBookmark = async (storage, time) => {
24
+ if (!storage.getCurrentBookmark) {
25
+ throw pitrUnavailable();
26
+ }
27
+ const current = await storage.getCurrentBookmark();
28
+ if (time === void 0 || !storage.getBookmarkForTime) {
29
+ return { current };
30
+ }
31
+ return { current, forTime: await storage.getBookmarkForTime(toTime(time)) };
32
+ };
33
+ const armRestore = async (storage, args) => {
34
+ if (!storage.onNextSessionRestoreBookmark) {
35
+ throw pitrUnavailable();
36
+ }
37
+ let target = args.bookmark;
38
+ if (target === void 0) {
39
+ if (args.time === void 0) {
40
+ throw Object.assign(new Error("pitrRestore: provide a `bookmark` or a `time` to restore to"), {
41
+ code: "BAD_REQUEST",
42
+ name: "LunoraError",
43
+ status: 400
44
+ });
45
+ }
46
+ if (!storage.getBookmarkForTime) {
47
+ throw pitrUnavailable();
48
+ }
49
+ target = await storage.getBookmarkForTime(toTime(args.time));
50
+ }
51
+ const undoBookmark = await storage.onNextSessionRestoreBookmark(target);
52
+ return { restoredTo: target, undoBookmark };
53
+ };
54
+
55
+ export { armRestore, readBookmark };
@@ -0,0 +1,160 @@
1
+ import { distinctValues } from './applyOnDelete-CMif2RKw.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
+
160
+ export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates };
@@ -0,0 +1,29 @@
1
+ const MAX_SQL_ROWS = 1e3;
2
+ const READONLY_LEAD = /^(?:explain\s+(?:query\s+plan\s+)?)?(?:select|with)\b/iu;
3
+ const FORBIDDEN_KEYWORD = /\b(?:alter|attach|create|delete|detach|drop|insert|pragma|reindex|replace|truncate|update|vacuum)\b/iu;
4
+ const LEADING_NOISE = /^(?:\s|--[^\n]*\n?|\/\*[\s\S]*?\*\/)+/u;
5
+ const TRAILING_SEMICOLON = /;\s*$/u;
6
+ const stripLeading = (sql) => sql.replace(LEADING_NOISE, "");
7
+ const sqlError = (message, code) => Object.assign(new Error(message), { code, name: "LunoraError", status: 400 });
8
+ const assertReadonly = (query) => {
9
+ const trimmed = stripLeading(query).trim();
10
+ if (trimmed === "") {
11
+ throw sqlError("the query is empty", "SQL_EMPTY");
12
+ }
13
+ const single = trimmed.replace(TRAILING_SEMICOLON, "");
14
+ if (single.includes(";")) {
15
+ throw sqlError("only a single statement may be run", "SQL_MULTIPLE_STATEMENTS");
16
+ }
17
+ if (!READONLY_LEAD.test(single) || FORBIDDEN_KEYWORD.test(single)) {
18
+ throw sqlError("the SQL editor is read-only — only SELECT / WITH / EXPLAIN queries are allowed", "SQL_NOT_READONLY");
19
+ }
20
+ };
21
+ const runReadonlySql = (sql, query) => {
22
+ assertReadonly(query);
23
+ const all = sql.exec(query).toArray();
24
+ const rows = all.length > MAX_SQL_ROWS ? all.slice(0, MAX_SQL_ROWS) : all;
25
+ const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
26
+ return { columns, rowCount: all.length, rows, truncated: all.length > MAX_SQL_ROWS };
27
+ };
28
+
29
+ export { MAX_SQL_ROWS, assertReadonly, runReadonlySql };