@lunora/do 1.0.0-alpha.21 → 1.0.0-alpha.23
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/dist/index.d.mts +92 -90
- package/dist/index.d.ts +92 -90
- package/dist/index.mjs +19 -19
- package/dist/packem_shared/{ADMIN_FUNCTIONS-DSUQ5fX9.mjs → ADMIN_FUNCTIONS-BbQdj8h1.mjs} +5 -0
- package/dist/packem_shared/{AGGREGATE_SQL_FUNCTION-CFk6adSu.mjs → AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs} +5 -3
- package/dist/packem_shared/{CDC_LOG_TABLE-DSycmnDf.mjs → CDC_LOG_TABLE-DZSVKRr7.mjs} +1 -1
- package/dist/packem_shared/{ConflictError-C0STs6bU.mjs → ConflictError-CLoq37xH.mjs} +4 -5
- package/dist/packem_shared/{CountRlsUnsupportedError-28ZvvwKS.mjs → CountRlsUnsupportedError-BGxj0pgS.mjs} +4 -4
- package/dist/packem_shared/{DATA_MIGRATION_STATE_TABLE-DfPxn8I0.mjs → DATA_MIGRATION_STATE_TABLE-DB3IYUR3.mjs} +3 -1
- package/dist/packem_shared/{DEFAULT_MAX_RELATION_KEYS-DU-Y4-LJ.mjs → DEFAULT_MAX_RELATION_KEYS-CjM9Y1_G.mjs} +8 -6
- package/dist/packem_shared/NotFoundError-C70b9hLw.mjs +9 -0
- package/dist/packem_shared/{NotUniqueError-Do5h_jiW.mjs → NotUniqueError-D1U5SBFR.mjs} +63 -55
- package/dist/packem_shared/{RLS_UNWRAP_SYMBOL-EtGQdC9d.mjs → RLS_UNWRAP_SYMBOL-DnjkqVgY.mjs} +6 -5
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-BfPNp3Jh.mjs → ROOT_DO_SIZE_WARN_BYTES-BM4TOOvf.mjs} +388 -49
- package/dist/packem_shared/{applyOnDelete-BQ-8ZlZ1.mjs → applyOnDelete-CAwZfp-5.mjs} +4 -3
- package/dist/packem_shared/{backfillAggregateIndexes-BZsOqDXP.mjs → backfillAggregateIndexes-DDoT-UUI.mjs} +1 -1
- package/dist/packem_shared/{compileWhereSql-MW_Lk8nJ.mjs → compileWhereSql-DE6yfRcQ.mjs} +2 -1
- package/dist/packem_shared/{createSystemReader-8CzSZP9V.mjs → createSystemReader-D12eNH13.mjs} +4 -2
- package/dist/packem_shared/{ctx-db-shapes-0RaIOy7J.mjs → ctx-db-shapes-Cz9dHyh1.mjs} +1 -1
- package/dist/packem_shared/{isSourceDue-9mJRJ9Ld.mjs → isSourceDue-BptUN8CR.mjs} +3 -2
- package/dist/packem_shared/{materializeExternalRows-Bj6EXy50.mjs → materializeExternalRows-DNcvoLRT.mjs} +2 -2
- package/dist/packem_shared/{runShardMigrations-nIwoQeOK.mjs → runShardMigrations-DeZr0KZp.mjs} +1 -1
- package/dist/packem_shared/{serveRelationFanout-C5axmGDj.mjs → serveRelationFanout-BhZF9AuB.mjs} +1 -1
- package/package.json +2 -1
- package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +0 -10
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { LunoraError, toErrorBody } from '@lunora/errors';
|
|
1
2
|
import { drizzle } from 'drizzle-orm/durable-sqlite';
|
|
2
3
|
import { e as encodeWire, a as awaitWsDrain, t as trySendFrame, d as decodeWire, s as subscriptionListDeltas, b as sendDeltaFrames } from './subscription-delivery-CK8qga-k.mjs';
|
|
3
4
|
import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-DZEhUeyI.mjs';
|
|
4
5
|
import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
|
|
5
|
-
import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-
|
|
6
|
+
import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-DB3IYUR3.mjs';
|
|
6
7
|
import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
|
|
7
8
|
import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
|
|
8
|
-
import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-
|
|
9
|
+
import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-BbQdj8h1.mjs';
|
|
9
10
|
import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
|
|
10
11
|
import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
|
|
11
12
|
import { readBookmark, armRestore } from './armRestore-BJk53Ro8.mjs';
|
|
@@ -14,21 +15,21 @@ import { stableStringify } from './stableStringify-MydiuScU.mjs';
|
|
|
14
15
|
import { redact, standardRules } from '@visulima/redact';
|
|
15
16
|
import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } from './security-audit-CucgBice.mjs';
|
|
16
17
|
import { runReadonlySql } from './MAX_SQL_ROWS-dDcFE1YZ.mjs';
|
|
17
|
-
import { ConflictError } from './ConflictError-
|
|
18
|
+
import { ConflictError } from './ConflictError-CLoq37xH.mjs';
|
|
18
19
|
import { e as deleteGlobalShapeSnapshotsForConnection, g as readIdempotent, h as writeIdempotent, t as trimIdempotent, r as readClientWatermark, m as migrateClientWatermark, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, f as readGlobalShapeSnapshot, w as writeGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
|
|
19
|
-
import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-
|
|
20
|
-
import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-
|
|
20
|
+
import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-DZSVKRr7.mjs';
|
|
21
|
+
import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
|
|
21
22
|
|
|
22
23
|
const MAX_BATCH_ENTRIES = 500;
|
|
23
24
|
|
|
24
25
|
const AUDIT_LOG_TABLE = "__lunora_audit__";
|
|
25
26
|
const AUDIT_LOG_RETENTION = 1e3;
|
|
26
|
-
const runSql$
|
|
27
|
+
const runSql$3 = (sql, query, ...params) => {
|
|
27
28
|
const runner = sql.exec;
|
|
28
29
|
return runner.call(sql, query, ...params);
|
|
29
30
|
};
|
|
30
31
|
const ensureAuditTable = (sql) => {
|
|
31
|
-
runSql$
|
|
32
|
+
runSql$3(
|
|
32
33
|
sql,
|
|
33
34
|
`CREATE TABLE IF NOT EXISTS "${AUDIT_LOG_TABLE}" (
|
|
34
35
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -42,7 +43,7 @@ const ensureAuditTable = (sql) => {
|
|
|
42
43
|
};
|
|
43
44
|
const appendAuditEntry = (sql, entry) => {
|
|
44
45
|
ensureAuditTable(sql);
|
|
45
|
-
runSql$
|
|
46
|
+
runSql$3(
|
|
46
47
|
sql,
|
|
47
48
|
`INSERT INTO "${AUDIT_LOG_TABLE}" (ts, op, "table", id, detail) VALUES (?, ?, ?, ?, ?)`,
|
|
48
49
|
entry.ts,
|
|
@@ -54,13 +55,13 @@ const appendAuditEntry = (sql, entry) => {
|
|
|
54
55
|
// eslint-disable-next-line unicorn/no-null -- SQL NULL is the correct value for an op with no associated table/id/detail.
|
|
55
56
|
entry.detail === void 0 ? null : JSON.stringify(entry.detail)
|
|
56
57
|
);
|
|
57
|
-
runSql$
|
|
58
|
+
runSql$3(sql, `DELETE FROM "${AUDIT_LOG_TABLE}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${AUDIT_LOG_TABLE}")`, AUDIT_LOG_RETENTION);
|
|
58
59
|
};
|
|
59
60
|
const readAuditLog = (sql, options = {}) => {
|
|
60
61
|
ensureAuditTable(sql);
|
|
61
62
|
const sinceSeq = options.sinceSeq ?? 0;
|
|
62
63
|
const limit = Math.max(1, Math.min(options.limit ?? AUDIT_LOG_RETENTION, 1e4));
|
|
63
|
-
const rows = runSql$
|
|
64
|
+
const rows = runSql$3(
|
|
64
65
|
sql,
|
|
65
66
|
`SELECT seq, ts, op, "table", id, detail FROM "${AUDIT_LOG_TABLE}" WHERE seq > ? ORDER BY seq DESC LIMIT ?`,
|
|
66
67
|
sinceSeq,
|
|
@@ -116,7 +117,7 @@ const buildBatchEntryRequest = (batchRequest, entry) => {
|
|
|
116
117
|
const QUERY_METRICS_TABLE = "__lunora_metrics_queries";
|
|
117
118
|
const QUERY_METRICS_MAX_SQL_LEN = 512;
|
|
118
119
|
const QUERY_METRICS_MAX_STATEMENTS = 500;
|
|
119
|
-
const runSql$
|
|
120
|
+
const runSql$2 = (sql, query, ...params) => {
|
|
120
121
|
const runner = sql.exec;
|
|
121
122
|
return runner.call(sql, query, ...params);
|
|
122
123
|
};
|
|
@@ -128,7 +129,7 @@ const normalizeSql = (sql) => {
|
|
|
128
129
|
return normalized;
|
|
129
130
|
};
|
|
130
131
|
const ensureQueryMetricsTable = (sql) => {
|
|
131
|
-
runSql$
|
|
132
|
+
runSql$2(
|
|
132
133
|
sql,
|
|
133
134
|
`CREATE TABLE IF NOT EXISTS "${QUERY_METRICS_TABLE}" (
|
|
134
135
|
normalized_sql TEXT PRIMARY KEY,
|
|
@@ -145,10 +146,10 @@ const recordQueryMetric = (sql, rawSql, durationMs, rowsRead, rowsWritten) => {
|
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
147
148
|
ensureQueryMetricsTable(sql);
|
|
148
|
-
const countRow = runSql$
|
|
149
|
+
const countRow = runSql$2(sql, `SELECT COUNT(*) AS n FROM "${QUERY_METRICS_TABLE}"`).one();
|
|
149
150
|
const count = countRow.n;
|
|
150
151
|
if (count >= QUERY_METRICS_MAX_STATEMENTS) {
|
|
151
|
-
const existing = runSql$
|
|
152
|
+
const existing = runSql$2(sql, `SELECT COUNT(*) AS c FROM "${QUERY_METRICS_TABLE}" WHERE normalized_sql = ?`, normalized).one();
|
|
152
153
|
if (existing.c === 0) {
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
@@ -160,11 +161,11 @@ const recordQueryMetric = (sql, rawSql, durationMs, rowsRead, rowsWritten) => {
|
|
|
160
161
|
total_duration_ms = total_duration_ms + excluded.total_duration_ms,
|
|
161
162
|
rows_read = rows_read + excluded.rows_read,
|
|
162
163
|
rows_written = rows_written + excluded.rows_written`;
|
|
163
|
-
runSql$
|
|
164
|
+
runSql$2(sql, upsertSql, normalized, durationMs, rowsRead, rowsWritten);
|
|
164
165
|
};
|
|
165
166
|
const readQueryMetrics = (sql) => {
|
|
166
167
|
ensureQueryMetricsTable(sql);
|
|
167
|
-
const rows = runSql$
|
|
168
|
+
const rows = runSql$2(
|
|
168
169
|
sql,
|
|
169
170
|
`SELECT normalized_sql, exec_count, total_duration_ms, rows_read, rows_written FROM "${QUERY_METRICS_TABLE}" ORDER BY total_duration_ms DESC`
|
|
170
171
|
).toArray();
|
|
@@ -179,6 +180,128 @@ const readQueryMetrics = (sql) => {
|
|
|
179
180
|
});
|
|
180
181
|
};
|
|
181
182
|
|
|
183
|
+
const QUEUE_TABLE = "__lunora_queue_messages";
|
|
184
|
+
const QUEUE_RETENTION = 500;
|
|
185
|
+
const MAX_BODY_CHARS = 128 * 1024;
|
|
186
|
+
const runSql$1 = (sql, query, ...params) => {
|
|
187
|
+
const runner = sql.exec;
|
|
188
|
+
return runner.call(sql, query, ...params);
|
|
189
|
+
};
|
|
190
|
+
const orNull = (value) => (
|
|
191
|
+
// eslint-disable-next-line unicorn/no-null -- SQL NULL is the correct value for an absent column.
|
|
192
|
+
value ?? null
|
|
193
|
+
);
|
|
194
|
+
const TRUNCATION_SUFFIX = "… [truncated by the dev queue catcher]";
|
|
195
|
+
const UNSERIALIZABLE_MARKER = "[unserializable message body]";
|
|
196
|
+
const encodeBody = (value) => {
|
|
197
|
+
if (value === void 0) {
|
|
198
|
+
return "null";
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const encoded = JSON.stringify(value);
|
|
202
|
+
if (encoded.length > MAX_BODY_CHARS) {
|
|
203
|
+
return JSON.stringify(`${encoded.slice(0, MAX_BODY_CHARS)}${TRUNCATION_SUFFIX}`);
|
|
204
|
+
}
|
|
205
|
+
return encoded;
|
|
206
|
+
} catch {
|
|
207
|
+
return JSON.stringify(UNSERIALIZABLE_MARKER);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const isLossyBody = (body) => typeof body === "string" && (body === UNSERIALIZABLE_MARKER || body.endsWith(TRUNCATION_SUFFIX));
|
|
211
|
+
const decodeBody = (value) => {
|
|
212
|
+
if (value === null || value === void 0 || value === "") {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(value);
|
|
217
|
+
} catch {
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const ensureQueueTable = (sql) => {
|
|
222
|
+
runSql$1(
|
|
223
|
+
sql,
|
|
224
|
+
`CREATE TABLE IF NOT EXISTS "${QUEUE_TABLE}" (
|
|
225
|
+
id TEXT PRIMARY KEY,
|
|
226
|
+
captured_at INTEGER NOT NULL,
|
|
227
|
+
message_id TEXT NOT NULL,
|
|
228
|
+
queue TEXT NOT NULL,
|
|
229
|
+
export_name TEXT,
|
|
230
|
+
body TEXT NOT NULL,
|
|
231
|
+
attempts INTEGER NOT NULL,
|
|
232
|
+
outcome TEXT NOT NULL,
|
|
233
|
+
error TEXT,
|
|
234
|
+
dead_lettered INTEGER NOT NULL,
|
|
235
|
+
message_ts INTEGER NOT NULL
|
|
236
|
+
)`
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
const recordQueueMessages = (sql, inputs, capturedAt) => {
|
|
240
|
+
ensureQueueTable(sql);
|
|
241
|
+
for (const input of inputs) {
|
|
242
|
+
runSql$1(
|
|
243
|
+
sql,
|
|
244
|
+
`INSERT INTO "${QUEUE_TABLE}" (id, captured_at, message_id, queue, export_name, body, attempts, outcome, error, dead_lettered, message_ts)
|
|
245
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
246
|
+
crypto.randomUUID(),
|
|
247
|
+
capturedAt,
|
|
248
|
+
input.messageId,
|
|
249
|
+
input.queue,
|
|
250
|
+
orNull(input.exportName),
|
|
251
|
+
encodeBody(input.body),
|
|
252
|
+
input.attempts,
|
|
253
|
+
input.outcome,
|
|
254
|
+
orNull(input.error),
|
|
255
|
+
input.deadLettered === true ? 1 : 0,
|
|
256
|
+
input.timestamp
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
runSql$1(
|
|
260
|
+
sql,
|
|
261
|
+
`DELETE FROM "${QUEUE_TABLE}"
|
|
262
|
+
WHERE id NOT IN (
|
|
263
|
+
SELECT id FROM "${QUEUE_TABLE}" ORDER BY captured_at DESC, id DESC LIMIT ?
|
|
264
|
+
)`,
|
|
265
|
+
QUEUE_RETENTION
|
|
266
|
+
);
|
|
267
|
+
return { recorded: inputs.length };
|
|
268
|
+
};
|
|
269
|
+
const rowToEntry = (row) => {
|
|
270
|
+
return {
|
|
271
|
+
attempts: row.attempts,
|
|
272
|
+
body: decodeBody(row.body),
|
|
273
|
+
capturedAt: row.captured_at,
|
|
274
|
+
deadLettered: row.dead_lettered === 1,
|
|
275
|
+
error: row.error ?? void 0,
|
|
276
|
+
exportName: row.export_name ?? void 0,
|
|
277
|
+
id: row.id,
|
|
278
|
+
messageId: row.message_id,
|
|
279
|
+
outcome: row.outcome,
|
|
280
|
+
queue: row.queue,
|
|
281
|
+
timestamp: row.message_ts
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
const readQueueMessages = (sql, options = {}) => {
|
|
285
|
+
ensureQueueTable(sql);
|
|
286
|
+
const limit = Math.min(Math.max(options.limit ?? 100, 1), QUEUE_RETENTION);
|
|
287
|
+
const filterQueue = typeof options.queue === "string" && options.queue.length > 0 ? options.queue : void 0;
|
|
288
|
+
const where = filterQueue === void 0 ? "" : `WHERE queue = ?`;
|
|
289
|
+
const params = filterQueue === void 0 ? [limit] : [filterQueue, limit];
|
|
290
|
+
const rows = runSql$1(sql, `SELECT * FROM "${QUEUE_TABLE}" ${where} ORDER BY captured_at DESC, id DESC LIMIT ?`, ...params).toArray();
|
|
291
|
+
return { entries: rows.map((row) => rowToEntry(row)) };
|
|
292
|
+
};
|
|
293
|
+
const readQueueMessageById = (sql, id) => {
|
|
294
|
+
ensureQueueTable(sql);
|
|
295
|
+
const rows = runSql$1(sql, `SELECT * FROM "${QUEUE_TABLE}" WHERE id = ? LIMIT 1`, id).toArray();
|
|
296
|
+
const row = rows[0];
|
|
297
|
+
return row === void 0 ? void 0 : rowToEntry(row);
|
|
298
|
+
};
|
|
299
|
+
const clearQueueMessages = (sql) => {
|
|
300
|
+
ensureQueueTable(sql);
|
|
301
|
+
runSql$1(sql, `DELETE FROM "${QUEUE_TABLE}"`);
|
|
302
|
+
return { cleared: true };
|
|
303
|
+
};
|
|
304
|
+
|
|
182
305
|
const RELAY_NAME_INFIX = "::relay::";
|
|
183
306
|
const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
|
|
184
307
|
const parseRelayName = (name) => {
|
|
@@ -199,7 +322,7 @@ const shapeRoutingKey = (name, args) => stableStringify({ args: args ?? {}, name
|
|
|
199
322
|
const DEFAULT_PROMOTION_THRESHOLDS = { tDown: 4e3, tUp: 8e3 };
|
|
200
323
|
const nextPromotionState = (current, subscribers, thresholds = DEFAULT_PROMOTION_THRESHOLDS) => {
|
|
201
324
|
if (thresholds.tDown >= thresholds.tUp) {
|
|
202
|
-
throw new
|
|
325
|
+
throw new LunoraError("INTERNAL", `invalid promotion thresholds: tDown (${String(thresholds.tDown)}) must be < tUp (${String(thresholds.tUp)})`);
|
|
203
326
|
}
|
|
204
327
|
if (current === "owned") {
|
|
205
328
|
return subscribers >= thresholds.tUp ? "promoted" : "owned";
|
|
@@ -305,7 +428,7 @@ const envPositiveInt = (env, key, fallback) => {
|
|
|
305
428
|
};
|
|
306
429
|
const RELAY_MULTICAST_IDENTITY = {};
|
|
307
430
|
const assertNeverFrame = (frame) => {
|
|
308
|
-
throw new
|
|
431
|
+
throw new LunoraError("INTERNAL", `unhandled relay frame: ${JSON.stringify(frame)}`);
|
|
309
432
|
};
|
|
310
433
|
const asRelayNamespace = (value) => {
|
|
311
434
|
if (value === null || typeof value !== "object") {
|
|
@@ -1511,6 +1634,84 @@ Verify your email: ${link}`,
|
|
|
1511
1634
|
to: recipient
|
|
1512
1635
|
};
|
|
1513
1636
|
};
|
|
1637
|
+
const parseRecordQueueMessageArgs = (args) => {
|
|
1638
|
+
const bad = (message) => {
|
|
1639
|
+
throw Object.assign(new Error(`recordQueueMessage: ${message}`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
|
|
1640
|
+
};
|
|
1641
|
+
const raw = args["messages"];
|
|
1642
|
+
if (!Array.isArray(raw)) {
|
|
1643
|
+
bad("`messages` must be an array");
|
|
1644
|
+
}
|
|
1645
|
+
const outcomes = /* @__PURE__ */ new Set(["ack", "error", "retry"]);
|
|
1646
|
+
return raw.map((entry, index) => {
|
|
1647
|
+
if (typeof entry !== "object" || entry === null) {
|
|
1648
|
+
bad(`\`messages[${String(index)}]\` must be an object`);
|
|
1649
|
+
}
|
|
1650
|
+
const record = entry;
|
|
1651
|
+
const messageId = typeof record["messageId"] === "string" ? record["messageId"] : "";
|
|
1652
|
+
const queue = typeof record["queue"] === "string" ? record["queue"] : "";
|
|
1653
|
+
const outcome = typeof record["outcome"] === "string" ? record["outcome"] : "";
|
|
1654
|
+
if (messageId === "") {
|
|
1655
|
+
bad(`\`messages[${String(index)}].messageId\` is required`);
|
|
1656
|
+
}
|
|
1657
|
+
if (queue === "") {
|
|
1658
|
+
bad(`\`messages[${String(index)}].queue\` is required`);
|
|
1659
|
+
}
|
|
1660
|
+
if (!outcomes.has(outcome)) {
|
|
1661
|
+
bad(`\`messages[${String(index)}].outcome\` must be one of ack | error | retry`);
|
|
1662
|
+
}
|
|
1663
|
+
const { attempts, timestamp } = record;
|
|
1664
|
+
return {
|
|
1665
|
+
attempts: typeof attempts === "number" && Number.isFinite(attempts) ? attempts : 1,
|
|
1666
|
+
body: record["body"],
|
|
1667
|
+
deadLettered: record["deadLettered"] === true,
|
|
1668
|
+
error: typeof record["error"] === "string" ? record["error"] : void 0,
|
|
1669
|
+
exportName: typeof record["exportName"] === "string" ? record["exportName"] : void 0,
|
|
1670
|
+
messageId,
|
|
1671
|
+
outcome,
|
|
1672
|
+
queue,
|
|
1673
|
+
timestamp: typeof timestamp === "number" && Number.isFinite(timestamp) ? timestamp : 0
|
|
1674
|
+
};
|
|
1675
|
+
});
|
|
1676
|
+
};
|
|
1677
|
+
const MAX_QUEUE_SEND_BATCH = 100;
|
|
1678
|
+
const parseSendQueueMessageArgs = (args) => {
|
|
1679
|
+
const exportName = typeof args["exportName"] === "string" ? args["exportName"].trim() : "";
|
|
1680
|
+
if (exportName === "") {
|
|
1681
|
+
throw Object.assign(new Error("sendQueueMessage: `exportName` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
|
|
1682
|
+
}
|
|
1683
|
+
const delayRaw = args["delaySeconds"];
|
|
1684
|
+
if (delayRaw !== void 0 && (typeof delayRaw !== "number" || !Number.isFinite(delayRaw) || delayRaw < 0)) {
|
|
1685
|
+
throw Object.assign(new Error("sendQueueMessage: `delaySeconds` must be a non-negative number"), {
|
|
1686
|
+
code: "BAD_REQUEST",
|
|
1687
|
+
name: "LunoraError",
|
|
1688
|
+
status: 400
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
const batch = Array.isArray(args["batch"]) ? args["batch"] : void 0;
|
|
1692
|
+
if (batch !== void 0 && (batch.length === 0 || batch.length > MAX_QUEUE_SEND_BATCH)) {
|
|
1693
|
+
throw Object.assign(new Error(`sendQueueMessage: \`batch\` must contain between 1 and ${String(MAX_QUEUE_SEND_BATCH)} messages`), {
|
|
1694
|
+
code: "BAD_REQUEST",
|
|
1695
|
+
name: "LunoraError",
|
|
1696
|
+
status: 400
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
return {
|
|
1700
|
+
batch,
|
|
1701
|
+
body: args["body"],
|
|
1702
|
+
contentType: typeof args["contentType"] === "string" ? args["contentType"] : void 0,
|
|
1703
|
+
delaySeconds: delayRaw,
|
|
1704
|
+
exportName
|
|
1705
|
+
};
|
|
1706
|
+
};
|
|
1707
|
+
const parseReplayQueueMessageArgs = (args) => {
|
|
1708
|
+
const id = typeof args["id"] === "string" ? args["id"].trim() : "";
|
|
1709
|
+
if (id === "") {
|
|
1710
|
+
throw Object.assign(new Error("replayQueueMessage: `id` is required"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
|
|
1711
|
+
}
|
|
1712
|
+
const target = typeof args["target"] === "string" && args["target"].trim() !== "" ? args["target"].trim() : void 0;
|
|
1713
|
+
return { id, target };
|
|
1714
|
+
};
|
|
1514
1715
|
const parseRankBeforeArgs = (args) => {
|
|
1515
1716
|
const table = typeof args["table"] === "string" ? args["table"] : "";
|
|
1516
1717
|
const index = typeof args["index"] === "string" ? args["index"] : "";
|
|
@@ -4009,27 +4210,11 @@ class ShardDO {
|
|
|
4009
4210
|
*/
|
|
4010
4211
|
// eslint-disable-next-line class-methods-use-this -- cohesive DO instance method (groups with the request handlers); kept non-static so subclasses can override the error mapping
|
|
4011
4212
|
errorToResponse(error) {
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
if (error && typeof error === "object" && error.name === "ValidationError") {
|
|
4016
|
-
const message = error instanceof Error ? error.message : "validation failed";
|
|
4017
|
-
return jsonResponse({ error: { code: "VALIDATION_ERROR", message } }, 400);
|
|
4018
|
-
}
|
|
4019
|
-
if (error && typeof error === "object" && error.name === "LunoraError") {
|
|
4020
|
-
const lunoraError = error;
|
|
4021
|
-
const status = typeof lunoraError.status === "number" ? lunoraError.status : 500;
|
|
4022
|
-
const body = {
|
|
4023
|
-
code: lunoraError.code ?? "INTERNAL",
|
|
4024
|
-
message: lunoraError.message ?? "internal error"
|
|
4025
|
-
};
|
|
4026
|
-
if (lunoraError.data !== void 0) {
|
|
4027
|
-
body.data = encodeWire(lunoraError.data);
|
|
4028
|
-
}
|
|
4029
|
-
return jsonResponse({ error: body }, status);
|
|
4213
|
+
const { body, redacted, status } = toErrorBody(error, { encodeData: encodeWire, fallbackCode: "RPC_FAILED", redactedMessage: "internal error" });
|
|
4214
|
+
if (redacted) {
|
|
4215
|
+
console.error("[@lunora/do] internal error:", error);
|
|
4030
4216
|
}
|
|
4031
|
-
|
|
4032
|
-
return jsonResponse({ error: { code: "RPC_FAILED", message: "internal error" } }, 500);
|
|
4217
|
+
return jsonResponse({ error: body }, status);
|
|
4033
4218
|
}
|
|
4034
4219
|
/**
|
|
4035
4220
|
* Batch dispatch (plan 088). Applies each `calls[]` entry through the SAME
|
|
@@ -4201,6 +4386,18 @@ class ShardDO {
|
|
|
4201
4386
|
if (functionPath === ADMIN_FUNCTIONS.sendTestMail) {
|
|
4202
4387
|
return this.handleSendTestMail(args);
|
|
4203
4388
|
}
|
|
4389
|
+
if (functionPath === ADMIN_FUNCTIONS.recordQueueMessage) {
|
|
4390
|
+
return this.handleRecordQueueMessage(args);
|
|
4391
|
+
}
|
|
4392
|
+
if (functionPath === ADMIN_FUNCTIONS.clearQueueMessages) {
|
|
4393
|
+
return this.handleClearQueueMessages();
|
|
4394
|
+
}
|
|
4395
|
+
if (functionPath === ADMIN_FUNCTIONS.sendQueueMessage) {
|
|
4396
|
+
return this.handleSendQueueMessage(args);
|
|
4397
|
+
}
|
|
4398
|
+
if (functionPath === ADMIN_FUNCTIONS.replayQueueMessage) {
|
|
4399
|
+
return this.handleReplayQueueMessage(args);
|
|
4400
|
+
}
|
|
4204
4401
|
if (functionPath === ADMIN_FUNCTIONS.createWorkflowInstance) {
|
|
4205
4402
|
return this.handleCreateWorkflowInstance(args);
|
|
4206
4403
|
}
|
|
@@ -4410,6 +4607,127 @@ class ShardDO {
|
|
|
4410
4607
|
const result = recordCapturedMail(this.state.storage.sql, input, Date.now());
|
|
4411
4608
|
return jsonResponse({ result }, 200);
|
|
4412
4609
|
}
|
|
4610
|
+
/**
|
|
4611
|
+
* Serve `__lunora_admin__:recordQueueMessage` — the capture sink the generated
|
|
4612
|
+
* worker `queue()` handler (via `@lunora/queue`'s `dispatchQueueBatch`) posts
|
|
4613
|
+
* every consumed message batch to. Records it into the reserved
|
|
4614
|
+
* `__lunora_queue_messages` table (bounded, auto-trimmed) so the studio Queues
|
|
4615
|
+
* panel shows one unified consumed-message log across every push consumer. Like
|
|
4616
|
+
* the mail catcher, the gate is the admin token alone — a token holder can
|
|
4617
|
+
* already mutate the shard, so a token-gated capture insert adds no privilege.
|
|
4618
|
+
*/
|
|
4619
|
+
handleRecordQueueMessage(args) {
|
|
4620
|
+
const messages = parseRecordQueueMessageArgs(args);
|
|
4621
|
+
const result = recordQueueMessages(this.state.storage.sql, messages, Date.now());
|
|
4622
|
+
return jsonResponse({ result }, 200);
|
|
4623
|
+
}
|
|
4624
|
+
/** Empty the dev queue consumed-message log (studio "clear log" action). Admin-gated by the caller. */
|
|
4625
|
+
handleClearQueueMessages() {
|
|
4626
|
+
const result = clearQueueMessages(this.state.storage.sql);
|
|
4627
|
+
return jsonResponse({ result }, 200);
|
|
4628
|
+
}
|
|
4629
|
+
/**
|
|
4630
|
+
* Serve `__lunora_admin__:sendQueueMessage` — the studio's "Send test message"
|
|
4631
|
+
* button. Resolves the declared queue's `QUEUE_*` producer binding and calls
|
|
4632
|
+
* `.send(body, { delaySeconds?, contentType? })`, or `.sendBatch(...)` when a
|
|
4633
|
+
* `batch` array is supplied. No SQLite write happens here (the message is only
|
|
4634
|
+
* captured once a consumer processes it), so this only records an audit entry.
|
|
4635
|
+
* Admin-gated by `handleAdminRpc`'s caller.
|
|
4636
|
+
*/
|
|
4637
|
+
async handleSendQueueMessage(args) {
|
|
4638
|
+
const parsed = parseSendQueueMessageArgs(args);
|
|
4639
|
+
const { binding } = this.resolveQueueBinding(parsed.exportName);
|
|
4640
|
+
let sent;
|
|
4641
|
+
if (parsed.batch === void 0) {
|
|
4642
|
+
await binding.send(parsed.body, { contentType: parsed.contentType, delaySeconds: parsed.delaySeconds });
|
|
4643
|
+
sent = 1;
|
|
4644
|
+
} else {
|
|
4645
|
+
await binding.sendBatch(
|
|
4646
|
+
parsed.batch.map((body) => {
|
|
4647
|
+
return { body, contentType: parsed.contentType, delaySeconds: parsed.delaySeconds };
|
|
4648
|
+
})
|
|
4649
|
+
);
|
|
4650
|
+
sent = parsed.batch.length;
|
|
4651
|
+
}
|
|
4652
|
+
this.recordAudit("sendQueueMessage", { detail: { count: sent, exportName: parsed.exportName } });
|
|
4653
|
+
return jsonResponse({ result: { sent } }, 200);
|
|
4654
|
+
}
|
|
4655
|
+
/**
|
|
4656
|
+
* Serve `__lunora_admin__:replayQueueMessage` — the studio's one-click replay /
|
|
4657
|
+
* DLQ redrive. Looks the captured row up by id, resolves the destination export
|
|
4658
|
+
* (explicit `target` → the parent queue when the message was captured off a
|
|
4659
|
+
* dead-letter queue → the queue it was consumed from), and re-enqueues the
|
|
4660
|
+
* stored body onto that producer. Records an audit entry; no SQLite write beyond
|
|
4661
|
+
* that (the replayed message is re-captured when a consumer processes it).
|
|
4662
|
+
* Admin-gated by `handleAdminRpc`'s caller.
|
|
4663
|
+
*/
|
|
4664
|
+
async handleReplayQueueMessage(args) {
|
|
4665
|
+
const parsed = parseReplayQueueMessageArgs(args);
|
|
4666
|
+
const row = readQueueMessageById(this.state.storage.sql, parsed.id);
|
|
4667
|
+
if (row === void 0) {
|
|
4668
|
+
throw Object.assign(new Error(`replayQueueMessage: captured message "${parsed.id}" was not found`), {
|
|
4669
|
+
code: "BAD_REQUEST",
|
|
4670
|
+
name: "LunoraError",
|
|
4671
|
+
status: 404
|
|
4672
|
+
});
|
|
4673
|
+
}
|
|
4674
|
+
if (isLossyBody(row.body)) {
|
|
4675
|
+
throw Object.assign(
|
|
4676
|
+
new Error(`replayQueueMessage: captured message "${parsed.id}" has a truncated or unserializable body and can't be replayed faithfully`),
|
|
4677
|
+
{ code: "BAD_REQUEST", name: "LunoraError", status: 422 }
|
|
4678
|
+
);
|
|
4679
|
+
}
|
|
4680
|
+
const target = parsed.target ?? this.resolveReplayTarget(row.queue) ?? row.exportName;
|
|
4681
|
+
if (typeof target !== "string" || target === "") {
|
|
4682
|
+
throw Object.assign(new Error(`replayQueueMessage: captured message "${parsed.id}" has no declared producer to replay onto (pass \`target\`)`), {
|
|
4683
|
+
code: "BAD_REQUEST",
|
|
4684
|
+
name: "LunoraError",
|
|
4685
|
+
status: 400
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
const { binding } = this.resolveQueueBinding(target);
|
|
4689
|
+
await binding.send(row.body);
|
|
4690
|
+
this.recordAudit("replayQueueMessage", { detail: { messageId: row.messageId, target }, id: parsed.id });
|
|
4691
|
+
return jsonResponse({ result: { sent: 1, target } }, 200);
|
|
4692
|
+
}
|
|
4693
|
+
/**
|
|
4694
|
+
* Resolve a declared queue's runtime producer binding from this shard's `env`.
|
|
4695
|
+
* Looks the `exportName` up in {@link queuesMetadata} (the codegen subclass's
|
|
4696
|
+
* statically-discovered list) to find its generated `QUEUE_*` binding, then
|
|
4697
|
+
* reads `env[binding]` and validates it carries `send`/`sendBatch`. A bad export
|
|
4698
|
+
* name or a missing/malformed binding throws a 400 `LunoraError` so the studio
|
|
4699
|
+
* surfaces an actionable message. Mirrors {@link resolveWorkflowBinding}.
|
|
4700
|
+
*/
|
|
4701
|
+
resolveQueueBinding(exportName) {
|
|
4702
|
+
const metadata = this.queuesMetadata().queues.find((queue) => queue.exportName === exportName);
|
|
4703
|
+
if (!metadata) {
|
|
4704
|
+
throw Object.assign(new Error(`queue "${exportName}" is not declared`), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
|
|
4705
|
+
}
|
|
4706
|
+
const binding = this.env?.[metadata.binding];
|
|
4707
|
+
if (typeof binding !== "object" || binding === null || typeof binding.send !== "function") {
|
|
4708
|
+
throw Object.assign(new Error(`queue binding "${metadata.binding}" is not available on this deployment`), {
|
|
4709
|
+
code: "BAD_REQUEST",
|
|
4710
|
+
name: "LunoraError",
|
|
4711
|
+
status: 400
|
|
4712
|
+
});
|
|
4713
|
+
}
|
|
4714
|
+
return { binding, metadata };
|
|
4715
|
+
}
|
|
4716
|
+
/**
|
|
4717
|
+
* Pick the replay destination export for a captured message's origin queue.
|
|
4718
|
+
* When the message was consumed off a queue that is another queue's dead-letter
|
|
4719
|
+
* queue, prefer that PARENT queue's producer (a DLQ usually has no producer of
|
|
4720
|
+
* its own) so replay redrives onto the original; otherwise re-enqueue onto the
|
|
4721
|
+
* queue the message came from. Returns `undefined` when neither is declared.
|
|
4722
|
+
*/
|
|
4723
|
+
resolveReplayTarget(queueName) {
|
|
4724
|
+
const { queues } = this.queuesMetadata();
|
|
4725
|
+
const parent = queues.find((queue) => queue.deadLetterQueue === queueName);
|
|
4726
|
+
if (parent !== void 0) {
|
|
4727
|
+
return parent.exportName;
|
|
4728
|
+
}
|
|
4729
|
+
return queues.find((queue) => queue.name === queueName)?.exportName;
|
|
4730
|
+
}
|
|
4413
4731
|
/**
|
|
4414
4732
|
* Append one durable audit entry for a state-changing admin op that just
|
|
4415
4733
|
* succeeded, folding the acting user (from `getCurrentUserId`) into `detail`.
|
|
@@ -4831,6 +5149,9 @@ class ShardDO {
|
|
|
4831
5149
|
if (functionPath === ADMIN_FUNCTIONS.getCapturedMail) {
|
|
4832
5150
|
return this.readAdminCapturedMail(sql, args);
|
|
4833
5151
|
}
|
|
5152
|
+
if (functionPath === ADMIN_FUNCTIONS.getQueueMessages) {
|
|
5153
|
+
return this.readAdminQueueMessages(sql, args);
|
|
5154
|
+
}
|
|
4834
5155
|
return void 0;
|
|
4835
5156
|
}
|
|
4836
5157
|
// eslint-disable-next-line class-methods-use-this -- kept an instance method for symmetry with the other `readAdmin*` resolvers
|
|
@@ -4861,6 +5182,28 @@ class ShardDO {
|
|
|
4861
5182
|
}
|
|
4862
5183
|
return { result, tables: /* @__PURE__ */ new Set([MAIL_TABLE]) };
|
|
4863
5184
|
}
|
|
5185
|
+
/**
|
|
5186
|
+
* Resolve a `getQueueMessages` admin read — the dev queue catcher's consumed
|
|
5187
|
+
* message log (`queue-catcher.ts`), newest-first, optionally filtered to one
|
|
5188
|
+
* queue. Best-effort: a SQL failure returns an empty log rather than throwing.
|
|
5189
|
+
* Reported against the {@link QUEUE_TABLE} so this read participates in
|
|
5190
|
+
* table-scoped subscription invalidation, but new captures arrive via the
|
|
5191
|
+
* worker→root-shard `recordQueueMessage` write, which (like the mail catcher)
|
|
5192
|
+
* inserts directly without a `flushChangedTables` — so the panel refreshes on
|
|
5193
|
+
* its poll (`useAutoRefresh`) rather than a live push.
|
|
5194
|
+
*/
|
|
5195
|
+
// eslint-disable-next-line class-methods-use-this -- kept an instance method for symmetry with the other `readAdmin*` resolvers
|
|
5196
|
+
readAdminQueueMessages(sql, args) {
|
|
5197
|
+
const limit = typeof args["limit"] === "number" ? args["limit"] : void 0;
|
|
5198
|
+
const queue = typeof args["queue"] === "string" ? args["queue"] : void 0;
|
|
5199
|
+
let result;
|
|
5200
|
+
try {
|
|
5201
|
+
result = readQueueMessages(sql, { limit, queue });
|
|
5202
|
+
} catch {
|
|
5203
|
+
result = { entries: [] };
|
|
5204
|
+
}
|
|
5205
|
+
return { result, tables: /* @__PURE__ */ new Set([QUEUE_TABLE]) };
|
|
5206
|
+
}
|
|
4864
5207
|
/** Resolve a `readTablePage` admin read, parsing the loosely-typed args into the reader's options. */
|
|
4865
5208
|
readAdminTablePage(sql, args) {
|
|
4866
5209
|
const table = typeof args["table"] === "string" ? args["table"] : "";
|
|
@@ -5007,7 +5350,6 @@ class ShardDO {
|
|
|
5007
5350
|
* 4. On normal completion send `{type:"complete"}`; on throw send
|
|
5008
5351
|
* `{type:"error"}`. Either way drop the controller.
|
|
5009
5352
|
*/
|
|
5010
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity -- the stream lifecycle (ack → chunk pump → complete/error) plus the structured-vs-redacted error branch is the wire protocol and reads clearer inline than split across helpers sharing the controller + socket
|
|
5011
5353
|
async handleStream(ws, id, functionPath, args) {
|
|
5012
5354
|
const iterable = this.executeStream(functionPath, args);
|
|
5013
5355
|
if (!iterable) {
|
|
@@ -5047,16 +5389,13 @@ class ShardDO {
|
|
|
5047
5389
|
ws.send(JSON.stringify({ id, type: "complete" }));
|
|
5048
5390
|
}
|
|
5049
5391
|
} catch (error) {
|
|
5050
|
-
const {
|
|
5051
|
-
|
|
5052
|
-
if (!isStructured) {
|
|
5392
|
+
const { body, redacted } = toErrorBody(error, { fallbackCode: "INTERNAL_SERVER_ERROR", redactedMessage: "internal error" });
|
|
5393
|
+
if (redacted) {
|
|
5053
5394
|
console.error("[@lunora/do] unhandled stream error:", error);
|
|
5054
5395
|
}
|
|
5055
|
-
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
5056
|
-
const message = isStructured ? rawMessage : "internal error";
|
|
5057
5396
|
ws.send(
|
|
5058
5397
|
JSON.stringify({
|
|
5059
|
-
error: { code:
|
|
5398
|
+
error: { code: body.code, message: body.message },
|
|
5060
5399
|
id,
|
|
5061
5400
|
type: "error"
|
|
5062
5401
|
})
|
|
@@ -5338,8 +5677,8 @@ class ShardDO {
|
|
|
5338
5677
|
resolved = this.resolveShape(shape.name, shape.args ?? {}, identity);
|
|
5339
5678
|
} catch (error) {
|
|
5340
5679
|
this.recordShapeError(`shape:seed:${subId}`, error);
|
|
5341
|
-
const
|
|
5342
|
-
return { code, message:
|
|
5680
|
+
const { body } = toErrorBody(error, { fallbackCode: "SHAPE_RESOLVE_FAILED", redactedMessage: "shape resolution failed" });
|
|
5681
|
+
return { code: body.code, message: body.message };
|
|
5343
5682
|
}
|
|
5344
5683
|
if (!resolved) {
|
|
5345
5684
|
return { code: "SHAPE_NOT_FOUND", message: `shape "${shape.name}" not found or not permitted` };
|
|
@@ -5351,8 +5690,8 @@ class ShardDO {
|
|
|
5351
5690
|
return await this.seedOpLogShape(ws, subId, shape, resolved);
|
|
5352
5691
|
} catch (error) {
|
|
5353
5692
|
this.recordShapeError(`shape:seed:${subId}`, error);
|
|
5354
|
-
const
|
|
5355
|
-
return { code, message:
|
|
5693
|
+
const { body } = toErrorBody(error, { fallbackCode: "SHAPE_SEED_FAILED", redactedMessage: "shape seed failed" });
|
|
5694
|
+
return { code: body.code, message: body.message };
|
|
5356
5695
|
}
|
|
5357
5696
|
}
|
|
5358
5697
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { applySelect } from './applySelect-BvZdFUBT.mjs';
|
|
2
3
|
|
|
3
4
|
const projectChildren = (documents, nested) => nested.select ? applySelect(documents, nested.select, nested.with) : documents;
|
|
@@ -27,13 +28,13 @@ const resolveWith = async (options) => {
|
|
|
27
28
|
}
|
|
28
29
|
const parentDefinition = schema.tables[tableName];
|
|
29
30
|
if (!parentDefinition) {
|
|
30
|
-
throw new
|
|
31
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
31
32
|
}
|
|
32
33
|
const relationMap = parentDefinition.relationMap ?? {};
|
|
33
34
|
const requireRelation = (name) => {
|
|
34
35
|
const relation = relationMap[name];
|
|
35
36
|
if (!relation) {
|
|
36
|
-
throw new
|
|
37
|
+
throw new LunoraError("INTERNAL", `unknown relation "${name}" on table "${tableName}"`);
|
|
37
38
|
}
|
|
38
39
|
return relation;
|
|
39
40
|
};
|
|
@@ -145,7 +146,7 @@ const applyRelationOnDelete = async (options, holderTable, relation) => {
|
|
|
145
146
|
const applyOnDelete = async (options) => {
|
|
146
147
|
const { schema, tableName } = options;
|
|
147
148
|
if (!schema.tables[tableName]) {
|
|
148
|
-
throw new
|
|
149
|
+
throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
|
|
149
150
|
}
|
|
150
151
|
for (const [holderTable, holderDefinition] of Object.entries(schema.tables)) {
|
|
151
152
|
const relations = holderDefinition.relationMap;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import { matchesStaticWhere } from './AGGREGATE_SQL_FUNCTION-
|
|
2
|
+
import { matchesStaticWhere } from './AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs';
|
|
3
3
|
import { encodeAggregateKey, foldAggregateTally, aggregateTableName } from './aggregateTableName-CxNqY1Sl.mjs';
|
|
4
4
|
import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
|
|
5
5
|
import { D as DOC_COLUMN, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-BCHCWtrD.mjs';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { sql } from 'drizzle-orm';
|
|
2
3
|
|
|
3
4
|
const RELATION_EXISTS_KEY = "__relationExists";
|
|
@@ -94,7 +95,7 @@ const STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["AND", "NOT", "OR", RELATION_EX
|
|
|
94
95
|
const compileStructuralKey = (key, value, strategy) => {
|
|
95
96
|
if (key === RELATION_EXISTS_KEY) {
|
|
96
97
|
if (!strategy.relationExists) {
|
|
97
|
-
throw new
|
|
98
|
+
throw new LunoraError("INTERNAL", "encountered a relation EXISTS marker without a relationExists strategy hook");
|
|
98
99
|
}
|
|
99
100
|
return strategy.relationExists(value);
|
|
100
101
|
}
|