@lunora/do 1.0.0-alpha.3 → 1.0.0-alpha.30

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 (51) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/index.d.mts +1375 -289
  4. package/dist/index.d.ts +1375 -289
  5. package/dist/index.mjs +35 -30
  6. package/dist/packem_shared/{ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs → ADMIN_FUNCTIONS-CAHLZMj8.mjs} +62 -8
  7. package/dist/packem_shared/{matchesStaticWhere-CFk6adSu.mjs → AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs} +5 -3
  8. package/dist/packem_shared/{applyCdcChanges-Ctdmxmrv.mjs → CDC_LOG_TABLE-DjJEHiM2.mjs} +7 -3
  9. package/dist/packem_shared/{ConflictError-C0STs6bU.mjs → ConflictError-CLoq37xH.mjs} +4 -5
  10. package/dist/packem_shared/{CountRlsUnsupportedError-28ZvvwKS.mjs → CountRlsUnsupportedError-BGxj0pgS.mjs} +4 -4
  11. package/dist/packem_shared/{DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs → DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs} +11 -3
  12. package/dist/packem_shared/{assertFlatPredicate-DyVYReuT.mjs → DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs} +58 -7
  13. package/dist/packem_shared/{assertReadonly-dDcFE1YZ.mjs → MAX_SQL_ROWS-D57CJaT9.mjs} +3 -1
  14. package/dist/packem_shared/NotFoundError-C70b9hLw.mjs +9 -0
  15. package/dist/packem_shared/{assertValidClientId-CBZ1zC96.mjs → NotUniqueError-BigrdT_W.mjs} +241 -90
  16. package/dist/packem_shared/{rank-CrkEIpF4.mjs → RANK_TIEBREAK-CXhdcA1o.mjs} +2 -13
  17. package/dist/packem_shared/{guardWriter-u3UlnCH5.mjs → RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs} +22 -9
  18. package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-DQkmGiCS.mjs → ROOT_DO_SIZE_WARN_BYTES-BA8QOChj.mjs} +2878 -326
  19. package/dist/packem_shared/{ReactiveCache-ByVzgH3d.mjs → ReactiveCache-BYlSGY0N.mjs} +1 -28
  20. package/dist/packem_shared/{SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs → SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs} +16 -27
  21. package/dist/packem_shared/{SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs → SHARD_REGISTRY_DO_NAME-D99roc-r.mjs} +12 -14
  22. package/dist/packem_shared/{applyOnDelete-CMif2RKw.mjs → applyOnDelete-BXSq3S70.mjs} +23 -12
  23. package/dist/packem_shared/{buildSeekWhere-lVsNXSLy.mjs → applySelect-WQY8m62C.mjs} +21 -2
  24. package/dist/packem_shared/{armRestore-BJk53Ro8.mjs → armRestore-4Px61hHS.mjs} +4 -10
  25. package/dist/packem_shared/{backfillAggregateIndexes-BF5eL7kW.mjs → backfillAggregateIndexes-DDoT-UUI.mjs} +3 -2
  26. package/dist/packem_shared/{compileWhereSql-CXrhFA3G.mjs → compileWhereSql-DE6yfRcQ.mjs} +5 -3
  27. package/dist/packem_shared/constant-time-equal-BVRWZgES.mjs +12 -0
  28. package/dist/packem_shared/{createSystemReader-8CzSZP9V.mjs → createSystemReader-D12eNH13.mjs} +4 -2
  29. package/dist/packem_shared/ctx-db-idempotency-BdcNpvY4.mjs +108 -0
  30. package/dist/packem_shared/ctx-db-shapes-Cz9dHyh1.mjs +53 -0
  31. package/dist/packem_shared/diffExternalSource-Cx9HUPJj.mjs +44 -0
  32. package/dist/packem_shared/{exportShardRows-DZEhUeyI.mjs → exportShardRows-Dy3oFZ26.mjs} +4 -3
  33. package/dist/packem_shared/isSourceDue-CYkt7Ru8.mjs +41 -0
  34. package/dist/packem_shared/json-response-BdbtpOhm.mjs +3 -0
  35. package/dist/packem_shared/materializeExternalRows-CTqZisSC.mjs +23 -0
  36. package/dist/packem_shared/{runShardMigrations-C3bn5r93.mjs → runShardMigrations-BGx4v2B6.mjs} +6 -4
  37. package/dist/packem_shared/serialize-sql-BlRUoiQe.mjs +14 -0
  38. package/dist/packem_shared/{serveRelationFanout-Clr1a05L.mjs → serveRelationFanout-BgaNg3Hu.mjs} +4 -7
  39. package/dist/packem_shared/stableStringify-MydiuScU.mjs +40 -0
  40. package/dist/packem_shared/subscription-delivery-CWigSEr3.mjs +348 -0
  41. package/dist/packem_shared/subscriptionListDeltas-DRLvFlM1.mjs +1 -0
  42. package/package.json +5 -3
  43. package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +0 -10
  44. package/dist/packem_shared/ctx-db-idempotency-DkC9rP91.mjs +0 -35
  45. package/dist/packem_shared/encodePartitionKey-C6blLR5K.mjs +0 -1
  46. /package/dist/packem_shared/{AUTH_METRICS_BUCKET_MS-CiHHYeJi.mjs → AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs} +0 -0
  47. /package/dist/packem_shared/{ensureFunctionMetricsTables-UDNVD7FS.mjs → FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs} +0 -0
  48. /package/dist/packem_shared/{clearCapturedMail-CPpgl-dX.mjs → MAIL_RETENTION-CPpgl-dX.mjs} +0 -0
  49. /package/dist/packem_shared/{buildSecurityAudit-CCAvoFlr.mjs → MIN_ADMIN_TOKEN_LENGTH-CCAvoFlr.mjs} +0 -0
  50. /package/dist/packem_shared/{ftsTableName-BLEMawrp.mjs → buildFtsMatch-BLEMawrp.mjs} +0 -0
  51. /package/dist/packem_shared/{runTriggers-5N6_Fx0A.mjs → hasTrigger-5N6_Fx0A.mjs} +0 -0
@@ -1,11 +1,6 @@
1
+ import { stableStringify } from './stableStringify-MydiuScU.mjs';
1
2
  import { depKey, SCAN_DEP } from './SCAN_DEP-DLJF8dsj.mjs';
2
3
 
3
- const compareKeys = (a, b) => {
4
- if (a < b) {
5
- return -1;
6
- }
7
- return a > b ? 1 : 0;
8
- };
9
4
  const DEFAULT_MAX_ENTRIES = 1e3;
10
5
  const DEFAULT_MAX_BYTES = 4 * 1024 * 1024;
11
6
  const estimateBytes = (value) => {
@@ -232,28 +227,6 @@ class ReactiveCache {
232
227
  }
233
228
  }
234
229
  }
235
- const stableStringify = (value) => {
236
- if (value === void 0) {
237
- return "null";
238
- }
239
- if (value === null || typeof value !== "object") {
240
- return JSON.stringify(value);
241
- }
242
- if (Array.isArray(value)) {
243
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
244
- }
245
- const record = value;
246
- const keys = Object.keys(record).toSorted(compareKeys);
247
- const parts = [];
248
- for (const key of keys) {
249
- const raw = record[key];
250
- if (raw === void 0) {
251
- continue;
252
- }
253
- parts.push(`${JSON.stringify(key)}:${stableStringify(raw)}`);
254
- }
255
- return `{${parts.join(",")}}`;
256
- };
257
230
  const reactiveCacheKey = (functionPath, args, identity) => `${identity ?? "\0anon"}\0${functionPath}:${stableStringify(args)}`;
258
231
 
259
232
  export { ReactiveCache, reactiveCacheKey, stableStringify };
@@ -1,3 +1,6 @@
1
+ import { c as constantTimeEqual } from './constant-time-equal-BVRWZgES.mjs';
2
+ import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
3
+
1
4
  const SESSION_DO_TTL_DEFAULT = 7 * 24 * 60 * 60;
2
5
  const SESSION_DO_TTL_MAX = 90 * 24 * 60 * 60;
3
6
  const SESSION_GC_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -7,20 +10,6 @@ const SESSION_TOKEN_PATTERN = /^[\w-]+$/;
7
10
  const MIN_TOKEN_LENGTH = 32;
8
11
  const MAX_TOKEN_LENGTH = 256;
9
12
  const MAX_USER_ID_LENGTH = 256;
10
- const jsonResponse = (status, body) => Response.json(body, {
11
- headers: { "content-type": "application/json" },
12
- status
13
- });
14
- const constantTimeEqual = (a, b) => {
15
- const max = Math.max(a.length, b.length);
16
- let diff = a.length ^ b.length;
17
- for (let index = 0; index < max; index += 1) {
18
- const charA = index < a.length ? a.charCodeAt(index) : 0;
19
- const charB = index < b.length ? b.charCodeAt(index) : 0;
20
- diff |= charA ^ charB;
21
- }
22
- return diff === 0;
23
- };
24
13
  const isAuthorized = (request, env) => {
25
14
  const expected = env.SESSION_DO_SECRET;
26
15
  if (typeof expected !== "string" || expected.length === 0) {
@@ -68,7 +57,7 @@ class SessionDO {
68
57
  async fetch(request) {
69
58
  const env = this.env ?? {};
70
59
  if (!isAuthorized(request, env)) {
71
- return jsonResponse(401, { error: { code: "UNAUTHORIZED", message: "missing or invalid SessionDO secret" } });
60
+ return jsonResponse({ error: { code: "UNAUTHORIZED", message: "missing or invalid SessionDO secret" } }, 401);
72
61
  }
73
62
  const url = new URL(request.url);
74
63
  if (request.method === "POST" && url.pathname === "/create") {
@@ -80,7 +69,7 @@ class SessionDO {
80
69
  if (request.method === "DELETE" && url.pathname === "/revoke") {
81
70
  return this.handleRevoke(request);
82
71
  }
83
- return jsonResponse(404, { error: { code: "NOT_FOUND", message: "no such session route" } });
72
+ return jsonResponse({ error: { code: "NOT_FOUND", message: "no such session route" } }, 404);
84
73
  }
85
74
  /**
86
75
  * Sweep expired session records. Lazy expiry-on-read ({@link handleGet})
@@ -116,25 +105,25 @@ class SessionDO {
116
105
  try {
117
106
  body = await request.json();
118
107
  } catch {
119
- return jsonResponse(400, { error: "invalid_request" });
108
+ return jsonResponse({ error: "invalid_request" }, 400);
120
109
  }
121
110
  const token = validateToken(body.token);
122
111
  if (token === void 0) {
123
- return jsonResponse(400, { error: "invalid_request" });
112
+ return jsonResponse({ error: "invalid_request" }, 400);
124
113
  }
125
114
  const { userId } = body;
126
115
  if (typeof userId !== "string" || userId.length === 0 || userId.length > MAX_USER_ID_LENGTH) {
127
- return jsonResponse(400, { error: "invalid_request" });
116
+ return jsonResponse({ error: "invalid_request" }, 400);
128
117
  }
129
118
  const ttlSeconds = resolveTtlSeconds(body.ttlSeconds);
130
119
  if (ttlSeconds === void 0) {
131
- return jsonResponse(400, { error: "invalid_request" });
120
+ return jsonResponse({ error: "invalid_request" }, 400);
132
121
  }
133
122
  const now = Date.now();
134
123
  const record = { createdAt: now, expiresAt: now + ttlSeconds * 1e3, userId };
135
124
  await this.state.storage.put(`s:${token}`, record);
136
125
  await this.armGcAlarm();
137
- return jsonResponse(201, { token, ...record });
126
+ return jsonResponse({ token, ...record }, 201);
138
127
  }
139
128
  /**
140
129
  * Ensure a GC alarm is pending. Only sets one when none is currently
@@ -155,25 +144,25 @@ class SessionDO {
155
144
  async handleGet(request) {
156
145
  const token = request.headers.get(SESSION_TOKEN_HEADER);
157
146
  if (!token) {
158
- return jsonResponse(400, { error: { code: "INVALID_INPUT", message: "token required" } });
147
+ return jsonResponse({ error: { code: "INVALID_INPUT", message: "token required" } }, 400);
159
148
  }
160
149
  const record = await this.state.storage.get(`s:${token}`);
161
150
  if (!record) {
162
- return jsonResponse(404, { error: { code: "NOT_FOUND", message: "session not found" } });
151
+ return jsonResponse({ error: { code: "NOT_FOUND", message: "session not found" } }, 404);
163
152
  }
164
153
  if (record.expiresAt < Date.now()) {
165
154
  await this.state.storage.delete(`s:${token}`);
166
- return jsonResponse(404, { error: { code: "EXPIRED", message: "session expired" } });
155
+ return jsonResponse({ error: { code: "EXPIRED", message: "session expired" } }, 404);
167
156
  }
168
- return jsonResponse(200, { token, ...record });
157
+ return jsonResponse({ token, ...record }, 200);
169
158
  }
170
159
  async handleRevoke(request) {
171
160
  const token = request.headers.get(SESSION_TOKEN_HEADER);
172
161
  if (!token) {
173
- return jsonResponse(400, { error: { code: "INVALID_INPUT", message: "token required" } });
162
+ return jsonResponse({ error: { code: "INVALID_INPUT", message: "token required" } }, 400);
174
163
  }
175
164
  await this.state.storage.delete(`s:${token}`);
176
- return jsonResponse(200, { ok: true });
165
+ return jsonResponse({ ok: true }, 200);
177
166
  }
178
167
  }
179
168
 
@@ -1,22 +1,20 @@
1
+ import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
2
+
1
3
  const SHARD_REGISTRY_DO_NAME = "__lunora_shard_registry__";
2
4
  const STORAGE_KEY = "__tables__";
3
- const jsonResponse = (status, body) => Response.json(body, {
4
- headers: { "content-type": "application/json" },
5
- status
6
- });
7
5
  const readTableShardBody = async (request) => {
8
6
  let body;
9
7
  try {
10
8
  body = await request.json();
11
9
  } catch {
12
- return { kind: "error", response: jsonResponse(400, { error: { code: "BAD_REQUEST", message: "invalid JSON body" } }) };
10
+ return { kind: "error", response: jsonResponse({ error: { code: "BAD_REQUEST", message: "invalid JSON body" } }, 400) };
13
11
  }
14
12
  const table = typeof body.table === "string" ? body.table.trim() : "";
15
13
  const shardKey = typeof body.shardKey === "string" ? body.shardKey.trim() : "";
16
14
  if (!table || !shardKey) {
17
15
  return {
18
16
  kind: "error",
19
- response: jsonResponse(400, { error: { code: "BAD_REQUEST", message: "table and shardKey required" } })
17
+ response: jsonResponse({ error: { code: "BAD_REQUEST", message: "table and shardKey required" } }, 400)
20
18
  };
21
19
  }
22
20
  return { kind: "ok", value: { shardKey, table } };
@@ -56,7 +54,7 @@ class ShardRegistryDO {
56
54
  if (request.method === "GET" && url.pathname === "/snapshot") {
57
55
  return this.handleSnapshot();
58
56
  }
59
- return jsonResponse(404, { error: { code: "NOT_FOUND", message: `unknown shard-registry route ${request.method} ${url.pathname}` } });
57
+ return jsonResponse({ error: { code: "NOT_FOUND", message: `unknown shard-registry route ${request.method} ${url.pathname}` } }, 404);
60
58
  }
61
59
  /**
62
60
  * Load the persisted snapshot exactly once. `blockConcurrencyWhile`
@@ -83,9 +81,9 @@ class ShardRegistryDO {
83
81
  handleList(url) {
84
82
  const table = url.searchParams.get("table");
85
83
  if (!table) {
86
- return jsonResponse(400, { error: { code: "BAD_REQUEST", message: "missing required query parameter: table" } });
84
+ return jsonResponse({ error: { code: "BAD_REQUEST", message: "missing required query parameter: table" } }, 400);
87
85
  }
88
- return jsonResponse(200, { shardKeys: [...this.tables.get(table) ?? []] });
86
+ return jsonResponse({ shardKeys: [...this.tables.get(table) ?? []] }, 200);
89
87
  }
90
88
  async handleRegister(request) {
91
89
  const parsed = await readTableShardBody(request);
@@ -100,11 +98,11 @@ class ShardRegistryDO {
100
98
  this.tables.set(table, set);
101
99
  }
102
100
  if (set.has(shardKey)) {
103
- return jsonResponse(200, { changed: false, ok: true });
101
+ return jsonResponse({ changed: false, ok: true }, 200);
104
102
  }
105
103
  set.add(shardKey);
106
104
  await this.persist();
107
- return jsonResponse(200, { changed: true, ok: true });
105
+ return jsonResponse({ changed: true, ok: true }, 200);
108
106
  });
109
107
  }
110
108
  handleSnapshot() {
@@ -112,7 +110,7 @@ class ShardRegistryDO {
112
110
  for (const [table, set] of this.tables) {
113
111
  out[table] = [...set];
114
112
  }
115
- return jsonResponse(200, { tables: out });
113
+ return jsonResponse({ tables: out }, 200);
116
114
  }
117
115
  async handleUnregister(request) {
118
116
  const parsed = await readTableShardBody(request);
@@ -123,14 +121,14 @@ class ShardRegistryDO {
123
121
  return this.state.blockConcurrencyWhile(async () => {
124
122
  const set = this.tables.get(table);
125
123
  if (!set?.has(shardKey)) {
126
- return jsonResponse(200, { changed: false, ok: true });
124
+ return jsonResponse({ changed: false, ok: true }, 200);
127
125
  }
128
126
  set.delete(shardKey);
129
127
  if (set.size === 0) {
130
128
  this.tables.delete(table);
131
129
  }
132
130
  await this.persist();
133
- return jsonResponse(200, { changed: true, ok: true });
131
+ return jsonResponse({ changed: true, ok: true }, 200);
134
132
  });
135
133
  }
136
134
  /** Serialize the in-memory map to a single JSON-safe object and put. */
@@ -1,3 +1,16 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { applySelect } from './applySelect-WQY8m62C.mjs';
3
+
4
+ const projectChildren = (documents, nested) => nested.select ? applySelect(documents, nested.select, nested.with) : documents;
5
+ const fanOutScalarCounts = async (counter, tableName, whereField, values, policyWhere) => {
6
+ const pairs = await Promise.all(
7
+ values.map(async (value) => {
8
+ const countWhere = policyWhere ? { AND: [{ [whereField]: value }, policyWhere] } : { [whereField]: value };
9
+ return [value, await counter(tableName, countWhere)];
10
+ })
11
+ );
12
+ return new Map(pairs);
13
+ };
1
14
  const distinctValues = (rows, field) => {
2
15
  const seen = /* @__PURE__ */ new Set();
3
16
  for (const row of rows) {
@@ -9,19 +22,19 @@ const distinctValues = (rows, field) => {
9
22
  return [...seen];
10
23
  };
11
24
  const resolveWith = async (options) => {
12
- const { counter, fetcher, parents, relationBaseWhere, schema, tableName, with: withInput } = options;
25
+ const { groupedCounter, fetcher, parents, relationBaseWhere, schema, tableName, with: withInput } = options;
13
26
  if (parents.length === 0) {
14
27
  return;
15
28
  }
16
29
  const parentDefinition = schema.tables[tableName];
17
30
  if (!parentDefinition) {
18
- throw new Error(`unknown table: ${tableName}`);
31
+ throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
19
32
  }
20
33
  const relationMap = parentDefinition.relationMap ?? {};
21
34
  const requireRelation = (name) => {
22
35
  const relation = relationMap[name];
23
36
  if (!relation) {
24
- throw new Error(`unknown relation "${name}" on table "${tableName}"`);
37
+ throw new LunoraError("INTERNAL", `unknown relation "${name}" on table "${tableName}"`);
25
38
  }
26
39
  return relation;
27
40
  };
@@ -44,7 +57,8 @@ const resolveWith = async (options) => {
44
57
  byReference.set(child[relation.references], child);
45
58
  }
46
59
  for (const parent of parents) {
47
- parent[name] = byReference.get(parent[relation.field]) ?? null;
60
+ const child = byReference.get(parent[relation.field]);
61
+ parent[name] = child ? projectChildren([child], nested)[0] ?? null : null;
48
62
  }
49
63
  };
50
64
  const loadMany = async (name, relation, nested) => {
@@ -77,19 +91,16 @@ const resolveWith = async (options) => {
77
91
  const cap = typeof nested.limit === "number" ? Math.max(0, Math.floor(nested.limit)) : void 0;
78
92
  for (const parent of parents) {
79
93
  const group = groups.get(parent[relation.references]) ?? [];
80
- parent[name] = cap === void 0 ? group : group.slice(0, cap);
94
+ parent[name] = projectChildren(cap === void 0 ? group : group.slice(0, cap), nested);
81
95
  }
82
96
  };
83
97
  const resolveCounts = async (countInput) => {
84
98
  for (const name of Object.keys(countInput)) {
85
99
  const relation = requireRelation(name);
86
100
  const [whereField, parentField] = relation.kind === "many" ? [relation.field, relation.references] : [relation.references, relation.field];
87
- const countByValue = /* @__PURE__ */ new Map();
88
101
  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
- }
102
+ const values = distinctValues(parents, parentField);
103
+ const countByValue = values.length === 0 ? /* @__PURE__ */ new Map() : await groupedCounter(relation.table, whereField, values, policyWhere);
93
104
  for (const parent of parents) {
94
105
  const counts = parent["_count"] ?? {};
95
106
  const parentValue = parent[parentField];
@@ -135,7 +146,7 @@ const applyRelationOnDelete = async (options, holderTable, relation) => {
135
146
  const applyOnDelete = async (options) => {
136
147
  const { schema, tableName } = options;
137
148
  if (!schema.tables[tableName]) {
138
- throw new Error(`unknown table: ${tableName}`);
149
+ throw new LunoraError("INTERNAL", `unknown table: ${tableName}`);
139
150
  }
140
151
  for (const [holderTable, holderDefinition] of Object.entries(schema.tables)) {
141
152
  const relations = holderDefinition.relationMap;
@@ -162,4 +173,4 @@ const runRowValidators = (definition, document) => {
162
173
  }
163
174
  };
164
175
 
165
- export { applyOnDelete, distinctValues, resolveWith, runRowValidators };
176
+ export { applyOnDelete, distinctValues, fanOutScalarCounts, resolveWith, runRowValidators };
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const TIEBREAK_FIELD = "id";
2
4
  const ID_FIELDS = /* @__PURE__ */ new Set(["_id", "id"]);
3
5
  const normalizeOrderKeys = (orderBy) => {
@@ -30,7 +32,7 @@ const encodeCursor = (record, keys) => {
30
32
  values.push(record["_id"]);
31
33
  return toBase64(JSON.stringify(values));
32
34
  };
33
- const invalidCursor = () => Object.assign(new TypeError("invalid cursor"), { code: "BAD_REQUEST", name: "LunoraError", status: 400 });
35
+ const invalidCursor = () => new LunoraError("BAD_REQUEST", "invalid cursor");
34
36
  const decodeCursor = (cursor) => {
35
37
  let decoded;
36
38
  try {
@@ -80,5 +82,22 @@ const buildSeekBeforeWhere = (keys, cursorValues) => {
80
82
  }
81
83
  return { OR: branches };
82
84
  };
85
+ const SELECT_SYSTEM_FIELDS = ["_id", "_creationTime"];
86
+ const applySelect = (page, select, withInput) => {
87
+ if (!select) {
88
+ return page;
89
+ }
90
+ const keep = /* @__PURE__ */ new Set([...select, ...SELECT_SYSTEM_FIELDS, ...withInput ? Object.keys(withInput) : []]);
91
+ return page.map((document) => {
92
+ const projected = {};
93
+ for (const key of keep) {
94
+ if (key in document) {
95
+ projected[key] = document[key];
96
+ }
97
+ }
98
+ return projected;
99
+ });
100
+ };
101
+ const softDeleteScope = (softDeleteMode, includeDeleted) => softDeleteMode && includeDeleted !== true ? { [softDeleteMode.field]: { isNull: true } } : void 0;
83
102
 
84
- export { buildSeekBeforeWhere, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys };
103
+ export { applySelect, buildSeekBeforeWhere, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope };
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const pitrUnavailable = () => Object.assign(
2
4
  new Error("native PITR is unavailable here (local dev or a non-SQLite Durable Object); use `lunora backup restore` for off-platform recovery"),
3
5
  {
@@ -12,11 +14,7 @@ const toTime = (time) => {
12
14
  }
13
15
  const parsed = Date.parse(time);
14
16
  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
- });
17
+ throw new LunoraError("BAD_REQUEST", `pitr: invalid time "${time}" — expected epoch-ms or an ISO timestamp`);
20
18
  }
21
19
  return parsed;
22
20
  };
@@ -37,11 +35,7 @@ const armRestore = async (storage, args) => {
37
35
  let target = args.bookmark;
38
36
  if (target === void 0) {
39
37
  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
- });
38
+ throw new LunoraError("BAD_REQUEST", "pitrRestore: provide a `bookmark` or a `time` to restore to");
45
39
  }
46
40
  if (!storage.getBookmarkForTime) {
47
41
  throw pitrUnavailable();
@@ -1,10 +1,11 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { matchesStaticWhere } from './matchesStaticWhere-CFk6adSu.mjs';
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';
6
6
  import { param } from './renderSql-D6eUcn2N.mjs';
7
- import { s as sortColumnName, m as matchesRankStaticWhere, e as encodePartitionKey, b as serializeSqlValue, r as rankTableName } from './rank-CrkEIpF4.mjs';
7
+ import { sortColumnName, matchesRankStaticWhere, encodePartitionKey, rankTableName } from './RANK_TIEBREAK-CXhdcA1o.mjs';
8
+ import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
8
9
 
9
10
  const backfillAggregateIndex = (sql$1, tableName, index) => {
10
11
  const aggTable = aggregateTableName(tableName, index.name);
@@ -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";
@@ -11,9 +12,10 @@ const isOperatorObject = (value) => {
11
12
  const keys = Object.keys(value);
12
13
  return keys.length > 0 && keys.every((key) => OPERATOR_KEY_SET.has(key));
13
14
  };
15
+ const escapeLikeTerm = (value) => typeof value === "string" ? value.replaceAll(/[\\%_]/g, (character) => `\\${character}`) : value;
14
16
  const compileContains = (reference, value, strategy) => {
15
- const term = sql`${strategy.serialize(value)}`;
16
- return strategy.likeContains ? strategy.likeContains(reference, term) : sql`${reference} LIKE '%' || ${term} || '%'`;
17
+ const term = sql`${strategy.serialize(escapeLikeTerm(value))}`;
18
+ return strategy.likeContains ? strategy.likeContains(reference, term) : sql`${reference} LIKE '%' || ${term} || '%' ESCAPE '\\'`;
17
19
  };
18
20
  const compileComparator = (reference, operator, comparator, value, strategy) => {
19
21
  if (value === null) {
@@ -93,7 +95,7 @@ const STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["AND", "NOT", "OR", RELATION_EX
93
95
  const compileStructuralKey = (key, value, strategy) => {
94
96
  if (key === RELATION_EXISTS_KEY) {
95
97
  if (!strategy.relationExists) {
96
- throw new Error("encountered a relation EXISTS marker without a relationExists strategy hook");
98
+ throw new LunoraError("INTERNAL", "encountered a relation EXISTS marker without a relationExists strategy hook");
97
99
  }
98
100
  return strategy.relationExists(value);
99
101
  }
@@ -0,0 +1,12 @@
1
+ const constantTimeEqual = (a, b) => {
2
+ const max = Math.max(a.length, b.length);
3
+ let diff = a.length ^ b.length;
4
+ for (let index = 0; index < max; index += 1) {
5
+ const charA = index < a.length ? a.charCodeAt(index) : 0;
6
+ const charB = index < b.length ? b.charCodeAt(index) : 0;
7
+ diff |= charA ^ charB;
8
+ }
9
+ return diff === 0;
10
+ };
11
+
12
+ export { constantTimeEqual as c };
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const toScheduledFunctionDoc = (record) => {
2
4
  const doc = {
3
5
  args: record["args"] ?? {},
@@ -40,13 +42,13 @@ const toStorageMetadata = (object) => {
40
42
  const createSystemReader = (options = {}) => {
41
43
  const requireScheduler = () => {
42
44
  if (!options.scheduler) {
43
- throw new Error('ctx.db.system.query("_scheduled_functions"): no scheduler configured. Pass `scheduler` to createShardDO().');
45
+ throw new LunoraError("INTERNAL", 'ctx.db.system.query("_scheduled_functions"): no scheduler configured. Pass `scheduler` to createShardDO().');
44
46
  }
45
47
  return options.scheduler;
46
48
  };
47
49
  const requireStorage = () => {
48
50
  if (!options.storage) {
49
- throw new Error('ctx.db.system.query("_storage"): no storage configured. Pass `storage` to createShardDO().');
51
+ throw new LunoraError("INTERNAL", 'ctx.db.system.query("_storage"): no storage configured. Pass `storage` to createShardDO().');
50
52
  }
51
53
  return options.storage;
52
54
  };
@@ -0,0 +1,108 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
3
+
4
+ const CLIENT_WATERMARK_TABLE = "__client_watermark";
5
+ const migrateClientWatermark = (sql$1) => {
6
+ runDrizzle(
7
+ sql$1,
8
+ sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(CLIENT_WATERMARK_TABLE)} (
9
+ identity TEXT NOT NULL,
10
+ client_id TEXT NOT NULL,
11
+ last_mutation_id INTEGER NOT NULL,
12
+ PRIMARY KEY (identity, client_id)
13
+ )`
14
+ );
15
+ };
16
+ const readClientWatermark = (sql$1, identity, clientId) => {
17
+ const rows = runDrizzle(
18
+ sql$1,
19
+ sql`SELECT last_mutation_id FROM ${sql.identifier(CLIENT_WATERMARK_TABLE)} WHERE identity = ${identity} AND client_id = ${clientId} LIMIT 1`
20
+ ).toArray();
21
+ return rows[0]?.last_mutation_id ?? 0;
22
+ };
23
+ const advanceClientWatermark = (sql$1, identity, clientId, mutationId) => {
24
+ runDrizzle(
25
+ sql$1,
26
+ sql`INSERT INTO ${sql.identifier(CLIENT_WATERMARK_TABLE)} (identity, client_id, last_mutation_id) VALUES (${identity}, ${clientId}, ${mutationId})
27
+ ON CONFLICT(identity, client_id) DO UPDATE SET last_mutation_id = MAX(last_mutation_id, excluded.last_mutation_id)`
28
+ );
29
+ };
30
+
31
+ const GLOBAL_SHAPE_SNAPSHOT_TABLE = "__global_shape_snapshot";
32
+ const migrateGlobalShapeSnapshot = (sql$1) => {
33
+ runDrizzle(
34
+ sql$1,
35
+ sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(GLOBAL_SHAPE_SNAPSHOT_TABLE)} (
36
+ connection_id TEXT NOT NULL,
37
+ sub_id TEXT NOT NULL,
38
+ members TEXT NOT NULL,
39
+ PRIMARY KEY (connection_id, sub_id)
40
+ )`
41
+ );
42
+ };
43
+ const readGlobalShapeSnapshot = (sql$1, connectionId, subId) => {
44
+ const rows = runDrizzle(
45
+ sql$1,
46
+ sql`SELECT members FROM ${sql.identifier(GLOBAL_SHAPE_SNAPSHOT_TABLE)} WHERE connection_id = ${connectionId} AND sub_id = ${subId} LIMIT 1`
47
+ ).toArray();
48
+ const raw = rows[0]?.members;
49
+ if (raw === void 0) {
50
+ return /* @__PURE__ */ new Map();
51
+ }
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ if (parsed === null || typeof parsed !== "object") {
55
+ return /* @__PURE__ */ new Map();
56
+ }
57
+ return new Map(Object.entries(parsed));
58
+ } catch {
59
+ return /* @__PURE__ */ new Map();
60
+ }
61
+ };
62
+ const writeGlobalShapeSnapshot = (sql$1, connectionId, subId, snapshot) => {
63
+ const members = JSON.stringify(Object.fromEntries(snapshot));
64
+ runDrizzle(
65
+ sql$1,
66
+ sql`INSERT INTO ${sql.identifier(GLOBAL_SHAPE_SNAPSHOT_TABLE)} (connection_id, sub_id, members) VALUES (${connectionId}, ${subId}, ${members})
67
+ ON CONFLICT(connection_id, sub_id) DO UPDATE SET members = excluded.members`
68
+ );
69
+ };
70
+ const deleteGlobalShapeSnapshot = (sql$1, connectionId, subId) => {
71
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(GLOBAL_SHAPE_SNAPSHOT_TABLE)} WHERE connection_id = ${connectionId} AND sub_id = ${subId}`);
72
+ };
73
+ const deleteGlobalShapeSnapshotsForConnection = (sql$1, connectionId) => {
74
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(GLOBAL_SHAPE_SNAPSHOT_TABLE)} WHERE connection_id = ${connectionId}`);
75
+ };
76
+
77
+ const IDEMPOTENCY_TABLE = "__idempotency";
78
+ const migrateIdempotency = (sql$1) => {
79
+ runDrizzle(
80
+ sql$1,
81
+ sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(IDEMPOTENCY_TABLE)} (
82
+ identity TEXT NOT NULL,
83
+ mutation_id TEXT NOT NULL,
84
+ result_json TEXT NOT NULL,
85
+ ts REAL NOT NULL,
86
+ PRIMARY KEY (identity, mutation_id)
87
+ )`
88
+ );
89
+ };
90
+ const readIdempotent = (sql$1, identity, mutationId) => {
91
+ const rows = runDrizzle(
92
+ sql$1,
93
+ sql`SELECT result_json, ts FROM ${sql.identifier(IDEMPOTENCY_TABLE)} WHERE identity = ${identity} AND mutation_id = ${mutationId} LIMIT 1`
94
+ ).toArray();
95
+ const row = rows[0];
96
+ return row === void 0 ? void 0 : { resultJson: row.result_json, ts: row.ts };
97
+ };
98
+ const writeIdempotent = (sql$1, identity, mutationId, resultJson, ts) => {
99
+ runDrizzle(
100
+ sql$1,
101
+ sql`INSERT OR IGNORE INTO ${sql.identifier(IDEMPOTENCY_TABLE)} (identity, mutation_id, result_json, ts) VALUES (${identity}, ${mutationId}, ${resultJson}, ${ts})`
102
+ );
103
+ };
104
+ const trimIdempotent = (sql$1, olderThanTs) => {
105
+ runDrizzle(sql$1, sql`DELETE FROM ${sql.identifier(IDEMPOTENCY_TABLE)} WHERE ts < ${olderThanTs}`);
106
+ };
107
+
108
+ export { CLIENT_WATERMARK_TABLE as C, GLOBAL_SHAPE_SNAPSHOT_TABLE as G, IDEMPOTENCY_TABLE as I, migrateIdempotency as a, migrateGlobalShapeSnapshot as b, advanceClientWatermark as c, deleteGlobalShapeSnapshot as d, deleteGlobalShapeSnapshotsForConnection as e, readGlobalShapeSnapshot as f, readIdempotent as g, writeIdempotent as h, migrateClientWatermark as m, readClientWatermark as r, trimIdempotent as t, writeGlobalShapeSnapshot as w };
@@ -0,0 +1,53 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
3
+ import { D as DOC_COLUMN, r as rowToDocument, j as jsonPathSql } from './do-sql-BCHCWtrD.mjs';
4
+ import { compileWhereSql } from './compileWhereSql-DE6yfRcQ.mjs';
5
+ import { s as serializeSqlValue } from './serialize-sql-BlRUoiQe.mjs';
6
+
7
+ const shapeWhereStrategy = { fieldRef: jsonPathSql, serialize: serializeSqlValue };
8
+ const idInClause = (ids) => {
9
+ if (ids.length === 0) {
10
+ return void 0;
11
+ }
12
+ return sql`id IN (${sql.join(
13
+ ids.map((id) => sql`${id}`),
14
+ sql`, `
15
+ )})`;
16
+ };
17
+ const composeWhere = (effectiveWhere, idRestriction) => {
18
+ const conditions = [];
19
+ if (idRestriction) {
20
+ conditions.push(idRestriction);
21
+ }
22
+ const compiled = compileWhereSql(effectiveWhere, shapeWhereStrategy);
23
+ if (compiled) {
24
+ conditions.push(compiled);
25
+ }
26
+ if (conditions.length === 0) {
27
+ return sql``;
28
+ }
29
+ return sql` WHERE ${sql.join(conditions, sql` AND `)}`;
30
+ };
31
+ const selectShapeRows = (sql$1, table, effectiveWhere) => {
32
+ const whereClause = composeWhere(effectiveWhere, void 0);
33
+ const rows = runDrizzle(sql$1, sql`SELECT id, _creationTime, ${sql.identifier(DOC_COLUMN)} FROM ${sql.identifier(table)}${whereClause}`).toArray();
34
+ const result = [];
35
+ for (const row of rows) {
36
+ const doc = rowToDocument(row);
37
+ const { id } = row;
38
+ if (doc !== void 0 && typeof id === "string") {
39
+ result.push({ doc, id });
40
+ }
41
+ }
42
+ return result;
43
+ };
44
+ const selectShapeMemberIds = (sql$1, table, effectiveWhere, ids) => {
45
+ if (ids.length === 0) {
46
+ return /* @__PURE__ */ new Set();
47
+ }
48
+ const whereClause = composeWhere(effectiveWhere, idInClause(ids));
49
+ const rows = runDrizzle(sql$1, sql`SELECT id FROM ${sql.identifier(table)}${whereClause}`).toArray();
50
+ return new Set(rows.map((row) => row.id));
51
+ };
52
+
53
+ export { selectShapeMemberIds as a, selectShapeRows as s };