@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,248 @@
1
+ const FUNCTION_METRICS_TABLE = "__lunora_metrics";
2
+ const FUNCTION_METRICS_BUCKETS_TABLE = "__lunora_metrics_buckets";
3
+ const FUNCTION_METRICS_SCANS_TABLE = "__lunora_metrics_scans";
4
+ const FUNCTION_METRICS_INDEX_TABLE = "__lunora_metrics_index";
5
+ const FUNCTION_METRICS_BUCKET_MS = 6e4;
6
+ const FUNCTION_METRICS_BUCKET_RETENTION = 1440;
7
+ const FUNCTION_METRICS_MAX_PATHS = 5e3;
8
+ const FUNCTION_METRICS_READ_LIMIT = 1e3;
9
+ const runSql = (sql, query, ...params) => {
10
+ const runner = sql.exec;
11
+ return runner.call(sql, query, ...params);
12
+ };
13
+ const bucketFloor = (ts) => Math.floor(ts / FUNCTION_METRICS_BUCKET_MS) * FUNCTION_METRICS_BUCKET_MS;
14
+ const dedupeIndexHits = (hits) => {
15
+ const seen = /* @__PURE__ */ new Map();
16
+ for (const hit of hits) {
17
+ seen.set(`${hit.table}\0${hit.index}`, { index: hit.index, table: hit.table });
18
+ }
19
+ return [...seen.values()];
20
+ };
21
+ const ensureFunctionMetricsTables = (sql) => {
22
+ runSql(
23
+ sql,
24
+ `CREATE TABLE IF NOT EXISTS "${FUNCTION_METRICS_TABLE}" (
25
+ path TEXT PRIMARY KEY,
26
+ calls INTEGER NOT NULL DEFAULT 0,
27
+ errors INTEGER NOT NULL DEFAULT 0,
28
+ conflicts INTEGER NOT NULL DEFAULT 0,
29
+ scans INTEGER NOT NULL DEFAULT 0,
30
+ total_duration_ms REAL NOT NULL DEFAULT 0,
31
+ min_duration_ms REAL,
32
+ max_duration_ms REAL NOT NULL DEFAULT 0,
33
+ last_called_at REAL NOT NULL DEFAULT 0,
34
+ last_error_at REAL,
35
+ last_error_message TEXT
36
+ )`
37
+ );
38
+ for (const column of ["scans", "conflicts"]) {
39
+ try {
40
+ runSql(sql, `ALTER TABLE "${FUNCTION_METRICS_TABLE}" ADD COLUMN ${column} INTEGER NOT NULL DEFAULT 0`);
41
+ } catch {
42
+ }
43
+ }
44
+ runSql(
45
+ sql,
46
+ `CREATE TABLE IF NOT EXISTS "${FUNCTION_METRICS_BUCKETS_TABLE}" (
47
+ path TEXT NOT NULL,
48
+ bucket_ms INTEGER NOT NULL,
49
+ calls INTEGER NOT NULL DEFAULT 0,
50
+ errors INTEGER NOT NULL DEFAULT 0,
51
+ PRIMARY KEY (path, bucket_ms)
52
+ )`
53
+ );
54
+ runSql(
55
+ sql,
56
+ `CREATE TABLE IF NOT EXISTS "${FUNCTION_METRICS_SCANS_TABLE}" (
57
+ path TEXT NOT NULL,
58
+ table_name TEXT NOT NULL,
59
+ scans INTEGER NOT NULL DEFAULT 0,
60
+ PRIMARY KEY (path, table_name)
61
+ )`
62
+ );
63
+ runSql(
64
+ sql,
65
+ `CREATE TABLE IF NOT EXISTS "${FUNCTION_METRICS_INDEX_TABLE}" (
66
+ table_name TEXT NOT NULL,
67
+ index_name TEXT NOT NULL,
68
+ reads INTEGER NOT NULL DEFAULT 0,
69
+ PRIMARY KEY (table_name, index_name)
70
+ )`
71
+ );
72
+ };
73
+ const recordFunctionMetric = (sql, input) => {
74
+ ensureFunctionMetricsTables(sql);
75
+ const pathCountRow = runSql(sql, `SELECT COUNT(*) AS n FROM "${FUNCTION_METRICS_TABLE}"`).one();
76
+ if (pathCountRow.n >= FUNCTION_METRICS_MAX_PATHS) {
77
+ const tracked = runSql(sql, `SELECT COUNT(*) AS c FROM "${FUNCTION_METRICS_TABLE}" WHERE path = ?`, input.path).one();
78
+ if (tracked.c === 0) {
79
+ return;
80
+ }
81
+ }
82
+ const scannedTables = input.scannedTables ? [...new Set(input.scannedTables)] : [];
83
+ const scanCount = scannedTables.length;
84
+ const indexHits = dedupeIndexHits(input.indexHits ?? []);
85
+ const errorCount = input.errored ? 1 : 0;
86
+ const conflictCount = input.conflicted ? 1 : 0;
87
+ const lastErrorAt = input.errored ? input.ts : null;
88
+ const lastErrorMessage = input.errored ? input.errorMessage ?? null : null;
89
+ runSql(
90
+ sql,
91
+ `INSERT INTO "${FUNCTION_METRICS_TABLE}"
92
+ (path, calls, errors, conflicts, scans, total_duration_ms, min_duration_ms, max_duration_ms, last_called_at, last_error_at, last_error_message)
93
+ VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
94
+ ON CONFLICT(path) DO UPDATE SET
95
+ calls = calls + 1,
96
+ errors = errors + excluded.errors,
97
+ conflicts = conflicts + excluded.conflicts,
98
+ scans = scans + excluded.scans,
99
+ total_duration_ms = total_duration_ms + excluded.total_duration_ms,
100
+ min_duration_ms = MIN(COALESCE(min_duration_ms, excluded.min_duration_ms), excluded.min_duration_ms),
101
+ max_duration_ms = MAX(max_duration_ms, excluded.max_duration_ms),
102
+ last_called_at = excluded.last_called_at,
103
+ last_error_at = CASE WHEN excluded.last_error_at IS NULL THEN last_error_at ELSE excluded.last_error_at END,
104
+ last_error_message = CASE WHEN excluded.last_error_at IS NULL THEN last_error_message ELSE excluded.last_error_message END`,
105
+ input.path,
106
+ errorCount,
107
+ conflictCount,
108
+ scanCount,
109
+ input.durationMs,
110
+ input.durationMs,
111
+ input.durationMs,
112
+ input.ts,
113
+ lastErrorAt,
114
+ lastErrorMessage
115
+ );
116
+ const bucket = bucketFloor(input.ts);
117
+ runSql(
118
+ sql,
119
+ `INSERT INTO "${FUNCTION_METRICS_BUCKETS_TABLE}" (path, bucket_ms, calls, errors)
120
+ VALUES (?, ?, 1, ?)
121
+ ON CONFLICT(path, bucket_ms) DO UPDATE SET
122
+ calls = calls + 1,
123
+ errors = errors + excluded.errors`,
124
+ input.path,
125
+ bucket,
126
+ errorCount
127
+ );
128
+ runSql(
129
+ sql,
130
+ `DELETE FROM "${FUNCTION_METRICS_BUCKETS_TABLE}"
131
+ WHERE path = ?
132
+ AND bucket_ms <= (
133
+ SELECT MAX(bucket_ms) - ? FROM "${FUNCTION_METRICS_BUCKETS_TABLE}" WHERE path = ?
134
+ )`,
135
+ input.path,
136
+ FUNCTION_METRICS_BUCKET_RETENTION * FUNCTION_METRICS_BUCKET_MS,
137
+ input.path
138
+ );
139
+ for (const table of scannedTables) {
140
+ runSql(
141
+ sql,
142
+ `INSERT INTO "${FUNCTION_METRICS_SCANS_TABLE}" (path, table_name, scans)
143
+ VALUES (?, ?, 1)
144
+ ON CONFLICT(path, table_name) DO UPDATE SET
145
+ scans = scans + 1`,
146
+ input.path,
147
+ table
148
+ );
149
+ }
150
+ for (const hit of indexHits) {
151
+ runSql(
152
+ sql,
153
+ `INSERT INTO "${FUNCTION_METRICS_INDEX_TABLE}" (table_name, index_name, reads)
154
+ VALUES (?, ?, 1)
155
+ ON CONFLICT(table_name, index_name) DO UPDATE SET
156
+ reads = reads + 1`,
157
+ hit.table,
158
+ hit.index
159
+ );
160
+ }
161
+ };
162
+ const readFunctionMetricScans = (sql) => {
163
+ ensureFunctionMetricsTables(sql);
164
+ const rows = runSql(
165
+ sql,
166
+ // Bounded read: cap the materialized rows so a bloated attribution table
167
+ // can't blow up DO memory. Highest-scan rows lead so the dominant
168
+ // attributions survive the cut.
169
+ `SELECT path, table_name, scans FROM "${FUNCTION_METRICS_SCANS_TABLE}" ORDER BY scans DESC, path ASC, table_name ASC LIMIT ${String(FUNCTION_METRICS_READ_LIMIT)}`
170
+ ).toArray();
171
+ const byPath = /* @__PURE__ */ new Map();
172
+ for (const row of rows) {
173
+ const list = byPath.get(row.path);
174
+ const entry = { scans: row.scans, table: row.table_name };
175
+ if (list === void 0) {
176
+ byPath.set(row.path, [entry]);
177
+ } else {
178
+ list.push(entry);
179
+ }
180
+ }
181
+ return byPath;
182
+ };
183
+ const readFunctionMetricIndexHits = (sql) => {
184
+ ensureFunctionMetricsTables(sql);
185
+ const rows = runSql(
186
+ sql,
187
+ `SELECT table_name, index_name, reads FROM "${FUNCTION_METRICS_INDEX_TABLE}" ORDER BY table_name ASC, index_name ASC`
188
+ ).toArray();
189
+ return rows.map((row) => {
190
+ return { index: row.index_name, reads: row.reads, table: row.table_name };
191
+ });
192
+ };
193
+ const mergeScanAttribution = (into, scanned) => {
194
+ for (const table of scanned) {
195
+ const entry = into.find((attribution) => attribution.table === table);
196
+ if (entry === void 0) {
197
+ into.push({ scans: 1, table });
198
+ } else {
199
+ entry.scans += 1;
200
+ }
201
+ }
202
+ into.sort((a, b) => b.scans - a.scans || a.table.localeCompare(b.table));
203
+ return into;
204
+ };
205
+ const readFunctionMetrics = (sql) => {
206
+ ensureFunctionMetricsTables(sql);
207
+ const scansByPath = readFunctionMetricScans(sql);
208
+ const rows = runSql(sql, `SELECT * FROM "${FUNCTION_METRICS_TABLE}" ORDER BY last_called_at DESC LIMIT ${String(FUNCTION_METRICS_READ_LIMIT)}`).toArray();
209
+ return rows.map((row) => {
210
+ return {
211
+ calls: row.calls,
212
+ conflicts: row.conflicts,
213
+ errors: row.errors,
214
+ lastCalledAt: row.last_called_at,
215
+ lastErrorAt: row.last_error_at,
216
+ lastErrorMessage: row.last_error_message,
217
+ maxDurationMs: row.max_duration_ms,
218
+ path: row.path,
219
+ scannedTables: scansByPath.get(row.path) ?? [],
220
+ scans: row.scans,
221
+ totalDurationMs: row.total_duration_ms
222
+ };
223
+ });
224
+ };
225
+ const readFunctionMetricBuckets = (sql, path) => {
226
+ ensureFunctionMetricsTables(sql);
227
+ const rows = path === void 0 ? runSql(
228
+ sql,
229
+ `SELECT path, bucket_ms, calls, errors FROM "${FUNCTION_METRICS_BUCKETS_TABLE}" ORDER BY bucket_ms ASC, path ASC`
230
+ ).toArray() : runSql(
231
+ sql,
232
+ `SELECT path, bucket_ms, calls, errors FROM "${FUNCTION_METRICS_BUCKETS_TABLE}" WHERE path = ? ORDER BY bucket_ms ASC`,
233
+ path
234
+ ).toArray();
235
+ return rows.map((row) => {
236
+ return { bucketMs: row.bucket_ms, calls: row.calls, errors: row.errors, path: row.path };
237
+ });
238
+ };
239
+ const readFunctionMetricsTotals = (sql) => {
240
+ ensureFunctionMetricsTables(sql);
241
+ const row = runSql(
242
+ sql,
243
+ `SELECT SUM(calls) AS requests, SUM(errors) AS errors FROM "${FUNCTION_METRICS_TABLE}"`
244
+ ).one();
245
+ return { errors: row.errors ?? 0, requests: row.requests ?? 0 };
246
+ };
247
+
248
+ export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_MAX_PATHS, FUNCTION_METRICS_READ_LIMIT, FUNCTION_METRICS_SCANS_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, mergeScanAttribution, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetricScans, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric };
@@ -0,0 +1,156 @@
1
+ const DEFAULT_BATCH_SIZE = 200;
2
+ const selectExportTables = (schema, requested) => {
3
+ const isShardLocal = (table) => {
4
+ const definition = schema.tables[table];
5
+ if (!definition) {
6
+ return false;
7
+ }
8
+ return definition.shardMode?.kind !== "global";
9
+ };
10
+ if (requested && requested.length > 0) {
11
+ const filtered = [];
12
+ for (const name of requested) {
13
+ if (isShardLocal(name)) {
14
+ filtered.push(name);
15
+ }
16
+ }
17
+ return filtered;
18
+ }
19
+ const result = [];
20
+ for (const name of Object.keys(schema.tables)) {
21
+ if (isShardLocal(name)) {
22
+ result.push(name);
23
+ }
24
+ }
25
+ return result;
26
+ };
27
+ const exportShardTable = async function* (writer, table, batchSize = DEFAULT_BATCH_SIZE) {
28
+ let cursor = null;
29
+ let done = false;
30
+ while (!done) {
31
+ const page = await writer.findMany(table, { cursor, limit: batchSize });
32
+ for (const record of page.page) {
33
+ yield { doc: record, table };
34
+ }
35
+ cursor = page.continueCursor;
36
+ done = page.isDone || cursor === null;
37
+ }
38
+ };
39
+ const exportShardRows = async function* (writer, schema, args) {
40
+ const tables = selectExportTables(schema, args.tables);
41
+ const batchSize = args.batchSize ?? DEFAULT_BATCH_SIZE;
42
+ for (const table of tables) {
43
+ yield* exportShardTable(writer, table, batchSize);
44
+ }
45
+ };
46
+ const FRAMEWORK_FIELDS = /* @__PURE__ */ new Set(["_creationTime", "_id"]);
47
+ const validateAgainstShape = (definition, payload) => {
48
+ for (const [field, validator] of Object.entries(definition.shape)) {
49
+ const candidate = payload[field];
50
+ if (candidate === void 0 && validator.kind === "optional") {
51
+ continue;
52
+ }
53
+ const parser = validator.parse;
54
+ if (typeof parser !== "function") {
55
+ continue;
56
+ }
57
+ try {
58
+ parser(candidate);
59
+ } catch (error) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ return `field "${field}": ${message}`;
62
+ }
63
+ }
64
+ return void 0;
65
+ };
66
+ const validateImportRow = (schema, table, record) => {
67
+ const definition = schema.tables[table];
68
+ if (!definition) {
69
+ return `unknown table: ${table}`;
70
+ }
71
+ if (definition.shardMode?.kind === "global") {
72
+ return `table "${table}" is a global (.global()) table and is not importable through the shard import path`;
73
+ }
74
+ const payload = Object.fromEntries(Object.entries(record).filter(([key]) => !FRAMEWORK_FIELDS.has(key)));
75
+ for (const key of Object.keys(payload)) {
76
+ if (!(key in definition.shape)) {
77
+ return `unexpected field "${key}": not declared in table "${table}"`;
78
+ }
79
+ }
80
+ return validateAgainstShape(definition, payload);
81
+ };
82
+ const idAlreadyExists = async (writer, explicitId) => {
83
+ try {
84
+ const existing = await writer.get(explicitId);
85
+ return existing !== null;
86
+ } catch {
87
+ return false;
88
+ }
89
+ };
90
+ const importOneRow = async (writer, schema, row, line) => {
91
+ const { doc, table } = row;
92
+ if (typeof table !== "string" || table.length === 0) {
93
+ return { error: { code: "BAD_ROW", line, message: "row is missing `table`", table }, kind: "error" };
94
+ }
95
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
96
+ return { error: { code: "BAD_ROW", line, message: "row is missing or malformed `doc`", table }, kind: "error" };
97
+ }
98
+ const failure = validateImportRow(schema, table, doc);
99
+ if (failure !== void 0) {
100
+ return { error: { code: "VALIDATION_ERROR", line, message: failure, table }, kind: "error" };
101
+ }
102
+ const explicitId = typeof doc["_id"] === "string" ? doc["_id"] : void 0;
103
+ if (explicitId !== void 0 && await idAlreadyExists(writer, explicitId)) {
104
+ return { kind: "conflict" };
105
+ }
106
+ try {
107
+ await writer.insert(table, doc, { allowExplicitId: true });
108
+ return { kind: "inserted", table };
109
+ } catch (error) {
110
+ const code = error.code ?? "INSERT_FAILED";
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ return { error: { code, line, message, table }, kind: "error" };
113
+ }
114
+ };
115
+ const importShardRows = async (writer, schema, args) => {
116
+ const errors = [];
117
+ const inserted = {};
118
+ let conflicts = 0;
119
+ let line = (args.startLine ?? 1) - 1;
120
+ for (const row of args.rows) {
121
+ line += 1;
122
+ const outcome = await importOneRow(writer, schema, row, line);
123
+ if (outcome.kind === "error") {
124
+ errors.push(outcome.error);
125
+ } else if (outcome.kind === "conflict") {
126
+ conflicts += 1;
127
+ } else {
128
+ inserted[outcome.table] = (inserted[outcome.table] ?? 0) + 1;
129
+ }
130
+ }
131
+ return { conflicts, errors, inserted };
132
+ };
133
+ const parseExportShardArgs = (args) => {
134
+ const tables = Array.isArray(args["tables"]) ? args["tables"].filter((entry) => typeof entry === "string") : void 0;
135
+ const batchSize = typeof args["batchSize"] === "number" ? args["batchSize"] : void 0;
136
+ return { batchSize, tables };
137
+ };
138
+ const parseImportShardArgs = (args) => {
139
+ const rawRows = Array.isArray(args["rows"]) ? args["rows"] : [];
140
+ const rows = [];
141
+ for (const entry of rawRows) {
142
+ if (!entry || typeof entry !== "object") {
143
+ continue;
144
+ }
145
+ const candidate = entry;
146
+ if (typeof candidate.table !== "string" || !candidate.doc || typeof candidate.doc !== "object" || Array.isArray(candidate.doc)) {
147
+ rows.push({ doc: candidate.doc ?? {}, table: typeof candidate.table === "string" ? candidate.table : "" });
148
+ continue;
149
+ }
150
+ rows.push({ doc: candidate.doc, table: candidate.table });
151
+ }
152
+ const startLine = typeof args["startLine"] === "number" ? args["startLine"] : void 0;
153
+ return { rows, startLine };
154
+ };
155
+
156
+ export { exportShardRows, exportShardTable, importShardRows, parseExportShardArgs, parseImportShardArgs, selectExportTables, validateImportRow };
@@ -0,0 +1,38 @@
1
+ const ftsTableName = (table, indexName) => `${table}__fts_${indexName}`;
2
+ const tokenizeSearch = (query) => query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
3
+ const buildFtsMatch = (tokens) => tokens.map((token, index) => index === tokens.length - 1 ? `"${token}"*` : `"${token}"`).join(" AND ");
4
+ const stringifySearchText = (value) => {
5
+ if (typeof value === "string") {
6
+ return value;
7
+ }
8
+ if (value === null || value === void 0) {
9
+ return "";
10
+ }
11
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
12
+ return String(value);
13
+ }
14
+ return JSON.stringify(value) ?? "";
15
+ };
16
+ const scoreDocument = (text, tokens) => {
17
+ const documentTokens = tokenizeSearch(text);
18
+ if (documentTokens.length === 0) {
19
+ return 0;
20
+ }
21
+ let score = 0;
22
+ for (const [index, token] of tokens.entries()) {
23
+ const isLast = index === tokens.length - 1;
24
+ let occurrences = 0;
25
+ for (const documentToken of documentTokens) {
26
+ if (isLast ? documentToken.startsWith(token) : documentToken === token) {
27
+ occurrences += 1;
28
+ }
29
+ }
30
+ if (occurrences === 0) {
31
+ return 0;
32
+ }
33
+ score += occurrences;
34
+ }
35
+ return score;
36
+ };
37
+
38
+ export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch };
@@ -0,0 +1,128 @@
1
+ const RLS_UNWRAP_SYMBOL = /* @__PURE__ */ Symbol.for("lunora.ctxdb.rls-unwrap");
2
+ class RlsRequiredError extends Error {
3
+ code = "RLS_REQUIRED";
4
+ status = 403;
5
+ table;
6
+ constructor(table) {
7
+ super(
8
+ `ctx.db access to "${table}" is denied: the schema is marked .rls("required"), so this table is protected. Apply RLS with .use(rls(policies)) in the procedure, or mark the table .public() to opt it out.`
9
+ );
10
+ this.name = "RlsRequiredError";
11
+ this.table = table;
12
+ }
13
+ }
14
+ const guardWriter = (raw, schema, tableOfId) => {
15
+ if (schema.rlsMode !== "required") {
16
+ return raw;
17
+ }
18
+ const base = raw;
19
+ const isProtected = (tableName) => {
20
+ const definition = schema.tables[tableName];
21
+ return definition !== void 0 && definition.isPublic !== true;
22
+ };
23
+ const guardTable = (tableName) => {
24
+ if (isProtected(tableName)) {
25
+ throw new RlsRequiredError(tableName);
26
+ }
27
+ };
28
+ const guardById = async (id, expectedTable) => {
29
+ if (expectedTable !== void 0) {
30
+ guardTable(expectedTable);
31
+ return;
32
+ }
33
+ const tableName = await tableOfId(id);
34
+ if (tableName !== void 0) {
35
+ guardTable(tableName);
36
+ }
37
+ };
38
+ const baseRankBefore = base.rankBefore;
39
+ const guarded = {
40
+ ...raw,
41
+ [RLS_UNWRAP_SYMBOL]: raw,
42
+ aggregate: (tableName, options) => {
43
+ guardTable(tableName);
44
+ return base.aggregate(tableName, options);
45
+ },
46
+ count: (tableName, whereOrArgs) => {
47
+ guardTable(tableName);
48
+ return base.count(tableName, whereOrArgs);
49
+ },
50
+ delete: async (id, expectedTable) => {
51
+ await guardById(id, expectedTable);
52
+ return base.delete(id, expectedTable);
53
+ },
54
+ deleteMany: async (ids, options, expectedTable) => {
55
+ for (const id of ids) {
56
+ await guardById(id, expectedTable);
57
+ }
58
+ return base.deleteMany(ids, options, expectedTable);
59
+ },
60
+ findFirst: (tableName, args) => {
61
+ guardTable(tableName);
62
+ return base.findFirst(tableName, args);
63
+ },
64
+ findFirstOrThrow: (tableName, args) => {
65
+ guardTable(tableName);
66
+ return base.findFirstOrThrow(tableName, args);
67
+ },
68
+ findMany: (tableName, args) => {
69
+ guardTable(tableName);
70
+ return base.findMany(tableName, args);
71
+ },
72
+ get: async (id, expectedTable) => {
73
+ await guardById(id, expectedTable);
74
+ return base.get(id, expectedTable);
75
+ },
76
+ groupBy: (tableName, options) => {
77
+ guardTable(tableName);
78
+ return base.groupBy(tableName, options);
79
+ },
80
+ insert: (tableName, document, options) => {
81
+ guardTable(tableName);
82
+ return base.insert(tableName, document, options);
83
+ },
84
+ insertMany: (tableName, documents, options) => {
85
+ guardTable(tableName);
86
+ return base.insertMany(tableName, documents, options);
87
+ },
88
+ insertManyUnsafe: (tableName, documents, options) => {
89
+ guardTable(tableName);
90
+ return base.insertManyUnsafe(tableName, documents, options);
91
+ },
92
+ patch: async (id, patch, expectedTable) => {
93
+ await guardById(id, expectedTable);
94
+ return base.patch(id, patch, expectedTable);
95
+ },
96
+ patchMany: async (patches, options, expectedTable) => {
97
+ for (const entry of patches) {
98
+ await guardById(entry.id, expectedTable);
99
+ }
100
+ return base.patchMany(patches, options, expectedTable);
101
+ },
102
+ query: (tableName) => {
103
+ guardTable(tableName);
104
+ return base.query(tableName);
105
+ },
106
+ rank: (tableName, indexName, options) => {
107
+ guardTable(tableName);
108
+ return base.rank(tableName, indexName, options);
109
+ },
110
+ rankPage: (tableName, indexName, options) => {
111
+ guardTable(tableName);
112
+ return base.rankPage(tableName, indexName, options);
113
+ },
114
+ replace: async (id, document, expectedTable) => {
115
+ await guardById(id, expectedTable);
116
+ return base.replace(id, document, expectedTable);
117
+ }
118
+ };
119
+ if (baseRankBefore) {
120
+ guarded["rankBefore"] = (tableName, indexName, options) => {
121
+ guardTable(tableName);
122
+ return baseRankBefore(tableName, indexName, options);
123
+ };
124
+ }
125
+ return guarded;
126
+ };
127
+
128
+ export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter };
@@ -0,0 +1,54 @@
1
+ const COUNT_OPTION_KEYS = /* @__PURE__ */ new Set(["baseWhere", "relationBaseWhere", "restrictsCounts", "where"]);
2
+ const matchesStaticWhere = (document, predicate) => {
3
+ for (const [field, expected] of Object.entries(predicate)) {
4
+ const actual = document[field];
5
+ if (expected !== null && typeof expected === "object" && !Array.isArray(expected)) {
6
+ const operatorKeys = Object.keys(expected);
7
+ if (operatorKeys.length === 1 && operatorKeys[0] === "eq") {
8
+ if (actual !== expected.eq) {
9
+ return false;
10
+ }
11
+ continue;
12
+ }
13
+ return false;
14
+ }
15
+ if (actual !== expected) {
16
+ return false;
17
+ }
18
+ }
19
+ return true;
20
+ };
21
+ const normalizeCountArgument = (argument) => {
22
+ if (argument === void 0) {
23
+ return {};
24
+ }
25
+ if (typeof argument !== "object" || Array.isArray(argument)) {
26
+ return { where: argument };
27
+ }
28
+ const keys = Object.keys(argument);
29
+ if (keys.length === 0) {
30
+ return {};
31
+ }
32
+ if (keys.every((key) => COUNT_OPTION_KEYS.has(key))) {
33
+ return argument;
34
+ }
35
+ return { where: argument };
36
+ };
37
+ const AGGREGATE_SQL_FUNCTION = { avg: "AVG", count: "COUNT", max: "MAX", min: "MIN", sum: "SUM" };
38
+ const aggregateSqlFunction = (op) => {
39
+ const sqlFunction = AGGREGATE_SQL_FUNCTION[op];
40
+ if (sqlFunction === void 0) {
41
+ throw new Error(`unknown aggregate op "${op}": expected one of ${Object.keys(AGGREGATE_SQL_FUNCTION).join(", ")}`);
42
+ }
43
+ return sqlFunction;
44
+ };
45
+ const throwingScheduler = {
46
+ runAfter: () => {
47
+ throw new Error("ctx.scheduler: no scheduler configured for triggers. Pass `scheduler` to the ctx-db factory.");
48
+ },
49
+ runAt: () => {
50
+ throw new Error("ctx.scheduler: no scheduler configured for triggers. Pass `scheduler` to the ctx-db factory.");
51
+ }
52
+ };
53
+
54
+ export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, normalizeCountArgument, throwingScheduler };