@lunora/do 1.0.0-alpha.4 → 1.0.0-alpha.41
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/LICENSE.md +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +5236 -3449
- package/dist/index.d.ts +5236 -3449
- package/dist/index.mjs +34 -25
- package/dist/packem_shared/{ADMIN_FUNCTIONS-Dzdqq5J2.mjs → ADMIN_FUNCTIONS-CnZkbXH_.mjs} +69 -8
- package/dist/packem_shared/{AGGREGATE_SQL_FUNCTION-CFk6adSu.mjs → AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs} +5 -3
- package/dist/packem_shared/{CDC_LOG_TABLE-Ctdmxmrv.mjs → CDC_LOG_TABLE-uwOJxJJZ.mjs} +8 -4
- 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-PTtTiQ7U.mjs → DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs} +11 -3
- package/dist/packem_shared/{DEFAULT_MAX_RELATION_KEYS-DyVYReuT.mjs → DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs} +58 -7
- package/dist/packem_shared/GEO_DEFAULT_PRECISION-BWnsNmpP.mjs +114 -0
- package/dist/packem_shared/{MAX_SQL_ROWS-dDcFE1YZ.mjs → MAX_SQL_ROWS-iFAA8FbD.mjs} +35 -3
- package/dist/packem_shared/NotFoundError-C70b9hLw.mjs +9 -0
- package/dist/packem_shared/{NotUniqueError-B6tzuDmM.mjs → NotUniqueError-DbtlWwcG.mjs} +400 -114
- package/dist/packem_shared/{rank-CrkEIpF4.mjs → RANK_TIEBREAK-CXhdcA1o.mjs} +2 -13
- package/dist/packem_shared/{RLS_UNWRAP_SYMBOL-u3UlnCH5.mjs → RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs} +22 -9
- package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-B4Cpm7Qv.mjs +7653 -0
- package/dist/packem_shared/{ReactiveCache-ByVzgH3d.mjs → ReactiveCache-DnSvbjil.mjs} +4 -30
- package/dist/packem_shared/{SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs → SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs} +16 -27
- package/dist/packem_shared/{SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs → SHARD_REGISTRY_DO_NAME-D99roc-r.mjs} +12 -14
- package/dist/packem_shared/{applyOnDelete-CMif2RKw.mjs → applyOnDelete-BXSq3S70.mjs} +23 -12
- package/dist/packem_shared/{buildSeekWhere-lVsNXSLy.mjs → applySelect-WQY8m62C.mjs} +21 -2
- package/dist/packem_shared/{armRestore-BJk53Ro8.mjs → armRestore-4Px61hHS.mjs} +4 -10
- package/dist/packem_shared/{backfillAggregateIndexes-BbVPvciS.mjs → backfillAggregateIndexes-BGTwynwh.mjs} +4 -3
- package/dist/packem_shared/{compileWhereSql-CXrhFA3G.mjs → compileWhereSql-DE6yfRcQ.mjs} +5 -3
- package/dist/packem_shared/constant-time-equal-BVRWZgES.mjs +12 -0
- package/dist/packem_shared/context-telemetry-CDpyil58.mjs +223 -0
- package/dist/packem_shared/createMetrics-C4I2Yanj.mjs +1 -0
- package/dist/packem_shared/{createSystemReader-8CzSZP9V.mjs → createSystemReader-D12eNH13.mjs} +4 -2
- package/dist/packem_shared/ctx-db-idempotency-BdcNpvY4.mjs +108 -0
- package/dist/packem_shared/ctx-db-shapes-DTeFiHYS.mjs +53 -0
- package/dist/packem_shared/diffExternalSource-CovHfdyo.mjs +44 -0
- package/dist/packem_shared/{do-sql-BCHCWtrD.mjs → do-sql-CGAgiQUz.mjs} +2 -1
- package/dist/packem_shared/{exportShardRows-DZEhUeyI.mjs → exportShardRows-Dy3oFZ26.mjs} +4 -3
- package/dist/packem_shared/isSoftDeleted-CFJmhjFP.mjs +217 -0
- package/dist/packem_shared/json-response-BdbtpOhm.mjs +3 -0
- package/dist/packem_shared/materializeExternalRows-BtEGs1Fv.mjs +45 -0
- package/dist/packem_shared/{runShardMigrations-PabobOjF.mjs → runShardMigrations-BVax6rYu.mjs} +21 -4
- package/dist/packem_shared/selectExpiredIds-C5W29Upb.mjs +18 -0
- package/dist/packem_shared/serialize-sql-BlRUoiQe.mjs +14 -0
- package/dist/packem_shared/{serveRelationFanout-CFBKWJ8Q.mjs → serveRelationFanout-k7HTh3CC.mjs} +4 -7
- package/dist/packem_shared/stableStringify-mC40mZts.mjs +40 -0
- package/dist/packem_shared/stableWireKey-DKuXO7T5.mjs +6 -0
- package/dist/packem_shared/subscriptionListDeltas-CT76bYny.mjs +126 -0
- package/dist/packem_shared/wire-codec-CzQc1pvf.mjs +225 -0
- package/package.json +5 -3
- package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +0 -10
- package/dist/packem_shared/RANK_TIEBREAK-C6blLR5K.mjs +0 -1
- package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-3lZ2yigq.mjs +0 -4003
- package/dist/packem_shared/ctx-db-idempotency-DkC9rP91.mjs +0 -35
|
@@ -1,11 +1,7 @@
|
|
|
1
|
+
import { stableWireKey } from './stableWireKey-DKuXO7T5.mjs';
|
|
1
2
|
import { depKey, SCAN_DEP } from './SCAN_DEP-DLJF8dsj.mjs';
|
|
3
|
+
export { stableStringify } from './stableStringify-mC40mZts.mjs';
|
|
2
4
|
|
|
3
|
-
const compareKeys = (a, b) => {
|
|
4
|
-
if (a < b) {
|
|
5
|
-
return -1;
|
|
6
|
-
}
|
|
7
|
-
return a > b ? 1 : 0;
|
|
8
|
-
};
|
|
9
5
|
const DEFAULT_MAX_ENTRIES = 1e3;
|
|
10
6
|
const DEFAULT_MAX_BYTES = 4 * 1024 * 1024;
|
|
11
7
|
const estimateBytes = (value) => {
|
|
@@ -232,28 +228,6 @@ class ReactiveCache {
|
|
|
232
228
|
}
|
|
233
229
|
}
|
|
234
230
|
}
|
|
235
|
-
const
|
|
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
|
-
const reactiveCacheKey = (functionPath, args, identity) => `${identity ?? "\0anon"}\0${functionPath}:${stableStringify(args)}`;
|
|
231
|
+
const reactiveCacheKey = (functionPath, args, identity) => `${identity ?? "\0anon"}\0${functionPath}:${stableWireKey(args)}`;
|
|
258
232
|
|
|
259
|
-
export { ReactiveCache, reactiveCacheKey,
|
|
233
|
+
export { ReactiveCache, reactiveCacheKey, stableWireKey };
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
155
|
+
return jsonResponse({ error: { code: "EXPIRED", message: "session expired" } }, 404);
|
|
167
156
|
}
|
|
168
|
-
return jsonResponse(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
84
|
+
return jsonResponse({ error: { code: "BAD_REQUEST", message: "missing required query parameter: table" } }, 400);
|
|
87
85
|
}
|
|
88
|
-
return jsonResponse(
|
|
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(
|
|
101
|
+
return jsonResponse({ changed: false, ok: true }, 200);
|
|
104
102
|
}
|
|
105
103
|
set.add(shardKey);
|
|
106
104
|
await this.persist();
|
|
107
|
-
return jsonResponse(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
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 = () =>
|
|
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
|
|
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
|
|
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 './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
|
-
import { D as DOC_COLUMN, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-
|
|
5
|
+
import { D as DOC_COLUMN, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-CGAgiQUz.mjs';
|
|
6
6
|
import { param } from './renderSql-D6eUcn2N.mjs';
|
|
7
|
-
import {
|
|
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
|
|
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 };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { isLunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
3
|
+
const stringifyFieldValue = (value) => {
|
|
4
|
+
if (typeof value === "string") {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
return JSON.stringify(value) ?? String(value);
|
|
9
|
+
} catch {
|
|
10
|
+
return String(value);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const coerceFieldValue = (value) => typeof value === "boolean" || typeof value === "number" || typeof value === "string" ? value : stringifyFieldValue(value);
|
|
14
|
+
const normalizeLogFields = (perCall, bound) => {
|
|
15
|
+
if (perCall === void 0 && bound === void 0) {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
const merged = {};
|
|
19
|
+
if (bound !== void 0) {
|
|
20
|
+
for (const [key, value] of Object.entries(bound)) {
|
|
21
|
+
merged[key] = coerceFieldValue(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (perCall !== void 0) {
|
|
25
|
+
for (const [key, value] of Object.entries(perCall)) {
|
|
26
|
+
merged[key] = coerceFieldValue(value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return Object.keys(merged).length === 0 ? void 0 : merged;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const otlpRandomHex = (bytes) => {
|
|
33
|
+
const buffer = new Uint8Array(bytes);
|
|
34
|
+
crypto.getRandomValues(buffer);
|
|
35
|
+
let hex = "";
|
|
36
|
+
for (const byte of buffer) {
|
|
37
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
38
|
+
}
|
|
39
|
+
return hex;
|
|
40
|
+
};
|
|
41
|
+
const HEX_ONLY = /^[0-9a-f]+$/;
|
|
42
|
+
const parseTraceparent = (header) => {
|
|
43
|
+
if (header === null || header === void 0) {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
const parts = header.trim().toLowerCase().split("-");
|
|
47
|
+
const [version, traceId, parentSpanId, flags] = parts;
|
|
48
|
+
if (parts.length < 4 || version === void 0 || version.length !== 2 || !HEX_ONLY.test(version) || version === "ff" || // Version 00 forbids trailing fields; only a future version may carry them.
|
|
49
|
+
version === "00" && parts.length !== 4 || traceId === void 0 || parentSpanId === void 0 || flags === void 0 || flags.length !== 2 || !HEX_ONLY.test(flags) || traceId.length !== 32 || parentSpanId.length !== 16 || !HEX_ONLY.test(traceId) || !HEX_ONLY.test(parentSpanId) || traceId === "00000000000000000000000000000000" || parentSpanId === "0000000000000000") {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
return { parentSpanId, sampled: (Number.parseInt(flags, 16) & 1) === 1, traceId };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const resolveTraceAnchor = (traceparent) => {
|
|
56
|
+
const inbound = parseTraceparent(traceparent);
|
|
57
|
+
return { rootSpanId: inbound?.parentSpanId ?? otlpRandomHex(8), traceId: inbound?.traceId ?? otlpRandomHex(16) };
|
|
58
|
+
};
|
|
59
|
+
const toErrorType = (error) => {
|
|
60
|
+
if (isLunoraError(error)) {
|
|
61
|
+
return error.code;
|
|
62
|
+
}
|
|
63
|
+
return error instanceof Error ? error.constructor.name : "Error";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const applyCloudflareSpanAttributes = (span, meta) => {
|
|
67
|
+
if (!span.isTraced) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
span.setAttribute("lunora.function_path", meta.functionPath);
|
|
71
|
+
span.setAttribute("lunora.ok", meta.ok);
|
|
72
|
+
span.setAttribute("lunora.duration_ms", meta.durationMs);
|
|
73
|
+
if (meta.shardKey !== void 0) {
|
|
74
|
+
span.setAttribute("lunora.shard_key", meta.shardKey);
|
|
75
|
+
}
|
|
76
|
+
if (meta.userId !== void 0) {
|
|
77
|
+
span.setAttribute("lunora.user_id", meta.userId);
|
|
78
|
+
}
|
|
79
|
+
if (meta.error !== void 0) {
|
|
80
|
+
span.setAttribute("lunora.error.type", meta.error.type);
|
|
81
|
+
span.setAttribute("lunora.error.message", meta.error.message);
|
|
82
|
+
}
|
|
83
|
+
for (const [key, value] of Object.entries(meta.attributes)) {
|
|
84
|
+
if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
85
|
+
span.setAttribute(`lunora.attr.${key}`, value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const createTracer = (deps) => {
|
|
90
|
+
const { anchor, fuseCloudflareSpans, functionPath, record, resolveCloudflareTracing, shardKey, userId } = deps;
|
|
91
|
+
const tracerFor = (parentSpanId) => async (name, function_, attributes) => {
|
|
92
|
+
const spanId = otlpRandomHex(8);
|
|
93
|
+
const startTs = Date.now();
|
|
94
|
+
const normalized = normalizeLogFields(attributes);
|
|
95
|
+
const collected = {};
|
|
96
|
+
const spanHandle = {
|
|
97
|
+
setAttribute: (key, value) => {
|
|
98
|
+
Object.assign(collected, normalizeLogFields({ [key]: value }));
|
|
99
|
+
},
|
|
100
|
+
setAttributes: (fields) => {
|
|
101
|
+
Object.assign(collected, normalizeLogFields(fields));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const runRecorded = async (cfSpan) => {
|
|
105
|
+
let ok = true;
|
|
106
|
+
let error;
|
|
107
|
+
try {
|
|
108
|
+
return await function_(tracerFor(spanId), spanHandle);
|
|
109
|
+
} catch (error_) {
|
|
110
|
+
ok = false;
|
|
111
|
+
error = {
|
|
112
|
+
message: error_ instanceof Error ? error_.message : String(error_),
|
|
113
|
+
// Prefer a LunoraError's stable `code` over the class name
|
|
114
|
+
// so spans group by the same taxonomy the RPC spans use.
|
|
115
|
+
type: toErrorType(error_)
|
|
116
|
+
};
|
|
117
|
+
throw error_;
|
|
118
|
+
} finally {
|
|
119
|
+
const durationMs = Date.now() - startTs;
|
|
120
|
+
const resolvedUserId = userId();
|
|
121
|
+
const merged = { ...normalized, ...collected };
|
|
122
|
+
try {
|
|
123
|
+
record({
|
|
124
|
+
...Object.keys(merged).length === 0 ? {} : { attributes: merged },
|
|
125
|
+
durationMs,
|
|
126
|
+
...error === void 0 ? {} : { error },
|
|
127
|
+
functionPath,
|
|
128
|
+
name,
|
|
129
|
+
ok,
|
|
130
|
+
parentSpanId,
|
|
131
|
+
shardKey,
|
|
132
|
+
spanId,
|
|
133
|
+
startTs,
|
|
134
|
+
traceId: anchor.traceId,
|
|
135
|
+
userId: resolvedUserId
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
if (cfSpan !== void 0) {
|
|
140
|
+
try {
|
|
141
|
+
applyCloudflareSpanAttributes(cfSpan, {
|
|
142
|
+
attributes: merged,
|
|
143
|
+
durationMs,
|
|
144
|
+
error,
|
|
145
|
+
functionPath,
|
|
146
|
+
ok,
|
|
147
|
+
shardKey,
|
|
148
|
+
userId: resolvedUserId
|
|
149
|
+
});
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
if (fuseCloudflareSpans === true && resolveCloudflareTracing !== void 0) {
|
|
156
|
+
const cfTracing = await resolveCloudflareTracing();
|
|
157
|
+
if (cfTracing !== void 0 && typeof cfTracing.enterSpan === "function") {
|
|
158
|
+
return await cfTracing.enterSpan(name, (span) => runRecorded(span));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return await runRecorded();
|
|
162
|
+
};
|
|
163
|
+
return tracerFor(anchor.rootSpanId);
|
|
164
|
+
};
|
|
165
|
+
const createMetrics = (deps) => {
|
|
166
|
+
const { functionPath, record, shardKey } = deps;
|
|
167
|
+
const emit = (kind, name, value, attributes) => {
|
|
168
|
+
if (!Number.isFinite(value)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const normalized = normalizeLogFields(attributes);
|
|
172
|
+
try {
|
|
173
|
+
record({
|
|
174
|
+
...normalized === void 0 ? {} : { attributes: normalized },
|
|
175
|
+
functionPath,
|
|
176
|
+
kind,
|
|
177
|
+
name,
|
|
178
|
+
shardKey,
|
|
179
|
+
ts: Date.now(),
|
|
180
|
+
value
|
|
181
|
+
});
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
count: (name, value = 1, attributes) => {
|
|
187
|
+
emit("counter", name, value, attributes);
|
|
188
|
+
},
|
|
189
|
+
gauge: (name, value, attributes) => {
|
|
190
|
+
emit("gauge", name, value, attributes);
|
|
191
|
+
},
|
|
192
|
+
record: (name, value, attributes) => {
|
|
193
|
+
emit("histogram", name, value, attributes);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
const dispatchRootSpan = (input) => {
|
|
198
|
+
const { anchor, durationMs, failure, functionPath, shardKey, startTs, userId } = input;
|
|
199
|
+
return {
|
|
200
|
+
dispatch: true,
|
|
201
|
+
durationMs,
|
|
202
|
+
...failure === void 0 ? {} : {
|
|
203
|
+
error: {
|
|
204
|
+
message: failure.thrown instanceof Error ? failure.thrown.message : String(failure.thrown),
|
|
205
|
+
type: toErrorType(failure.thrown)
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
functionPath,
|
|
209
|
+
name: functionPath,
|
|
210
|
+
ok: failure === void 0,
|
|
211
|
+
// Empty: this span IS the trace's root locally. The worker's own RPC span
|
|
212
|
+
// sits above it in a full collector-side trace, but it is not in this
|
|
213
|
+
// buffer, so naming it here would dangle.
|
|
214
|
+
parentSpanId: "",
|
|
215
|
+
shardKey,
|
|
216
|
+
spanId: anchor.rootSpanId,
|
|
217
|
+
startTs,
|
|
218
|
+
traceId: anchor.traceId,
|
|
219
|
+
userId
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export { createTracer as a, applyCloudflareSpanAttributes as b, createMetrics as c, dispatchRootSpan as d, normalizeLogFields as n, parseTraceparent as p, resolveTraceAnchor as r };
|