@edge-base/server 0.2.2 → 0.2.4
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/admin-build/_app/immutable/chunks/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
- package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
- package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
- package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
- package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
- package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
- package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
- package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
- package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BZxfavhF.js → app.C8ylfBe6.js} +2 -2
- package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.VRVb0wee.js → 20.DYb-q3W8.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.js → 9.DOkvfmir.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -3
- package/src/__tests__/admin-data-routes.test.ts +29 -0
- package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
- package/src/__tests__/database-do-route-validation.test.ts +105 -0
- package/src/__tests__/database-live-do.test.ts +50 -0
- package/src/__tests__/database-live-emitter.test.ts +116 -1
- package/src/__tests__/database-live-route.test.ts +82 -0
- package/src/__tests__/do-router.test.ts +116 -0
- package/src/__tests__/error-format.test.ts +63 -0
- package/src/__tests__/functions-context.test.ts +674 -33
- package/src/__tests__/functions-d1-proxy.test.ts +54 -0
- package/src/__tests__/plugin-migration-routing.test.ts +32 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
- package/src/__tests__/provider-aware-sql.test.ts +163 -0
- package/src/__tests__/room-auth-state-loss.test.ts +124 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
- package/src/__tests__/scheduled.test.ts +55 -0
- package/src/__tests__/service-key-db-proxy.test.ts +122 -1
- package/src/__tests__/sql-route.test.ts +252 -75
- package/src/__tests__/table-hook-runtime.test.ts +137 -0
- package/src/durable-objects/database-do.ts +36 -45
- package/src/durable-objects/database-live-do.ts +46 -1
- package/src/durable-objects/room-runtime-base.ts +26 -2
- package/src/durable-objects/rooms-do.ts +1 -1
- package/src/index.ts +12 -6
- package/src/lib/admin-db-target.ts +30 -74
- package/src/lib/d1-handler.ts +55 -35
- package/src/lib/database-live-emitter.ts +57 -16
- package/src/lib/do-router.ts +135 -3
- package/src/lib/functions.ts +215 -143
- package/src/lib/internal-transport.ts +28 -12
- package/src/lib/plugin-migration-routing.ts +28 -0
- package/src/lib/plugin-migrations.ts +38 -38
- package/src/lib/postgres-handler.ts +51 -31
- package/src/lib/provider-aware-sql.ts +831 -0
- package/src/lib/table-hook-runtime.ts +62 -0
- package/src/routes/admin.ts +41 -41
- package/src/routes/auth.ts +7 -2
- package/src/routes/database-live.ts +110 -12
- package/src/routes/sql.ts +64 -84
- package/src/routes/storage.ts +7 -2
- package/src/routes/tables.ts +42 -29
- package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
- package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
- package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
- package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +0 -1
package/src/lib/d1-handler.ts
CHANGED
|
@@ -36,10 +36,11 @@ import { summarizeValidationErrors, validateInsert, validateUpdate } from './val
|
|
|
36
36
|
import { buildEffectiveSchema } from './schema.js';
|
|
37
37
|
import { generateId } from './uuid.js';
|
|
38
38
|
import { parseUpdateBody } from './op-parser.js';
|
|
39
|
-
import { emitDbLiveEvent, emitDbLiveBatchEvent
|
|
39
|
+
import { emitDbLiveEvent, emitDbLiveBatchEvent } from './database-live-emitter.js';
|
|
40
40
|
import { isTrustedInternalContext } from './internal-request.js';
|
|
41
41
|
import { executeDbTriggers } from './functions.js';
|
|
42
42
|
import { forbiddenError, hookRejectedError, normalizeDatabaseError } from './errors.js';
|
|
43
|
+
import { buildTableHookRuntimeServices } from './table-hook-runtime.js';
|
|
43
44
|
|
|
44
45
|
// ─── Types ───
|
|
45
46
|
|
|
@@ -227,10 +228,11 @@ async function evalInsertRule(
|
|
|
227
228
|
|
|
228
229
|
function buildHookCtx(
|
|
229
230
|
db: D1Database,
|
|
230
|
-
tables: Record<string, TableConfig>,
|
|
231
231
|
env: Env,
|
|
232
232
|
executionCtx?: ExecutionContext,
|
|
233
233
|
): HookCtx {
|
|
234
|
+
const runtimeServices = buildTableHookRuntimeServices(parseConfig(env), env);
|
|
235
|
+
|
|
234
236
|
return {
|
|
235
237
|
db: {
|
|
236
238
|
async get(table: string, id: string): Promise<Record<string, unknown> | null> {
|
|
@@ -266,20 +268,7 @@ function buildHookCtx(
|
|
|
266
268
|
return result.rows.length > 0;
|
|
267
269
|
},
|
|
268
270
|
},
|
|
269
|
-
|
|
270
|
-
async broadcast(channel: string, event: string, data: unknown): Promise<void> {
|
|
271
|
-
await sendToDatabaseLiveDO(
|
|
272
|
-
env,
|
|
273
|
-
{ channel, event, payload: data ?? {} },
|
|
274
|
-
'/internal/broadcast',
|
|
275
|
-
);
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
push: {
|
|
279
|
-
async send(_userId: string, _payload: { title?: string; body: string }): Promise<void> {
|
|
280
|
-
// Push notifications — same mechanism as DO (via Worker env)
|
|
281
|
-
},
|
|
282
|
-
},
|
|
271
|
+
...runtimeServices,
|
|
283
272
|
waitUntil(promise: Promise<unknown>): void {
|
|
284
273
|
if (executionCtx) {
|
|
285
274
|
executionCtx.waitUntil(promise);
|
|
@@ -288,6 +277,18 @@ function buildHookCtx(
|
|
|
288
277
|
};
|
|
289
278
|
}
|
|
290
279
|
|
|
280
|
+
function scheduleDbLive(
|
|
281
|
+
executionCtx: ExecutionContext,
|
|
282
|
+
promise: Promise<void>,
|
|
283
|
+
context: string,
|
|
284
|
+
): void {
|
|
285
|
+
executionCtx.waitUntil(
|
|
286
|
+
promise.catch((error) => {
|
|
287
|
+
console.warn(`[db-live] ${context} failed`, error);
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
291
292
|
// ─── Utility ───
|
|
292
293
|
|
|
293
294
|
function esc(name: string): string {
|
|
@@ -436,7 +437,7 @@ async function handleList(
|
|
|
436
437
|
|
|
437
438
|
// Apply onEnrich hook
|
|
438
439
|
if (tableHooks?.onEnrich) {
|
|
439
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
440
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
440
441
|
for (let i = 0; i < items.length; i++) {
|
|
441
442
|
try {
|
|
442
443
|
const enriched = await tableHooks.onEnrich(auth, items[i], hookCtx);
|
|
@@ -600,7 +601,7 @@ async function handleGet(
|
|
|
600
601
|
|
|
601
602
|
// Apply onEnrich hook
|
|
602
603
|
if (tableHooks?.onEnrich) {
|
|
603
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
604
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
604
605
|
try {
|
|
605
606
|
const enriched = await tableHooks.onEnrich(auth, row, hookCtx);
|
|
606
607
|
if (enriched && typeof enriched === 'object') return c.json({ ...row, ...enriched });
|
|
@@ -666,7 +667,7 @@ async function handleInsert(
|
|
|
666
667
|
}
|
|
667
668
|
|
|
668
669
|
// Run beforeInsert hook
|
|
669
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
670
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
670
671
|
if (tableHooks?.beforeInsert) {
|
|
671
672
|
try {
|
|
672
673
|
const transformed = await tableHooks.beforeInsert(auth, body, hookCtx);
|
|
@@ -748,10 +749,12 @@ async function handleInsert(
|
|
|
748
749
|
hookCtx.waitUntil(Promise.resolve(hook(inserted, hookCtx)).catch(() => {}));
|
|
749
750
|
}
|
|
750
751
|
|
|
751
|
-
// Emit database-live event
|
|
752
|
+
// Emit database-live event in the background so writes stay fast.
|
|
752
753
|
const eventType = isUpsert && isUpdate ? 'modified' : 'added';
|
|
753
|
-
|
|
754
|
+
scheduleDbLive(
|
|
755
|
+
c.executionCtx,
|
|
754
756
|
emitDbLiveEvent(c.env, resolved.namespace, tableName, eventType, String(inserted.id ?? ''), inserted),
|
|
757
|
+
`emit ${eventType} ${resolved.namespace}.${tableName}`,
|
|
755
758
|
);
|
|
756
759
|
c.executionCtx.waitUntil(
|
|
757
760
|
executeDbTriggers(
|
|
@@ -831,7 +834,7 @@ async function handleUpdate(
|
|
|
831
834
|
}
|
|
832
835
|
|
|
833
836
|
// Run beforeUpdate hook
|
|
834
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
837
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
835
838
|
if (tableHooks?.beforeUpdate) {
|
|
836
839
|
try {
|
|
837
840
|
const transformed = await tableHooks.beforeUpdate(auth, existingRow, body, hookCtx);
|
|
@@ -891,9 +894,10 @@ async function handleUpdate(
|
|
|
891
894
|
);
|
|
892
895
|
}
|
|
893
896
|
|
|
894
|
-
|
|
895
|
-
|
|
897
|
+
scheduleDbLive(
|
|
898
|
+
c.executionCtx,
|
|
896
899
|
emitDbLiveEvent(c.env, resolved.namespace, tableName, 'modified', id, updated),
|
|
900
|
+
`emit modified ${resolved.namespace}.${tableName}:${id}`,
|
|
897
901
|
);
|
|
898
902
|
c.executionCtx.waitUntil(
|
|
899
903
|
executeDbTriggers(
|
|
@@ -947,7 +951,7 @@ async function handleDelete(
|
|
|
947
951
|
}
|
|
948
952
|
|
|
949
953
|
// Run beforeDelete hook
|
|
950
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
954
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
951
955
|
if (tableHooks?.beforeDelete) {
|
|
952
956
|
try {
|
|
953
957
|
await tableHooks.beforeDelete(auth, existingRow, hookCtx);
|
|
@@ -969,9 +973,10 @@ async function handleDelete(
|
|
|
969
973
|
);
|
|
970
974
|
}
|
|
971
975
|
|
|
972
|
-
|
|
973
|
-
|
|
976
|
+
scheduleDbLive(
|
|
977
|
+
c.executionCtx,
|
|
974
978
|
emitDbLiveEvent(c.env, resolved.namespace, tableName, 'removed', id, stripInternalFields(existingRow)),
|
|
979
|
+
`emit removed ${resolved.namespace}.${tableName}:${id}`,
|
|
975
980
|
);
|
|
976
981
|
c.executionCtx.waitUntil(
|
|
977
982
|
executeDbTriggers(
|
|
@@ -1203,15 +1208,21 @@ async function handleBatch(
|
|
|
1203
1208
|
// Emit database-live events
|
|
1204
1209
|
if (allChanges.length > 0) {
|
|
1205
1210
|
if (allChanges.length >= 10) {
|
|
1206
|
-
|
|
1211
|
+
scheduleDbLive(
|
|
1212
|
+
c.executionCtx,
|
|
1207
1213
|
emitDbLiveBatchEvent(c.env, resolved.namespace, tableName, allChanges),
|
|
1214
|
+
`emit batch ${resolved.namespace}.${tableName} (${allChanges.length} changes)`,
|
|
1208
1215
|
);
|
|
1209
1216
|
} else {
|
|
1210
|
-
|
|
1211
|
-
c.executionCtx
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1217
|
+
scheduleDbLive(
|
|
1218
|
+
c.executionCtx,
|
|
1219
|
+
Promise.all(
|
|
1220
|
+
allChanges.map((ch) =>
|
|
1221
|
+
emitDbLiveEvent(c.env, resolved.namespace, tableName, ch.type, ch.docId, ch.data),
|
|
1222
|
+
),
|
|
1223
|
+
).then(() => undefined),
|
|
1224
|
+
`emit fan-out ${resolved.namespace}.${tableName} (${allChanges.length} changes)`,
|
|
1225
|
+
);
|
|
1215
1226
|
}
|
|
1216
1227
|
}
|
|
1217
1228
|
|
|
@@ -1306,8 +1317,17 @@ async function handleBatchByFilter(
|
|
|
1306
1317
|
// Emit database-live events
|
|
1307
1318
|
if (succeeded > 0) {
|
|
1308
1319
|
const eventType = body.action === 'delete' ? 'removed' : 'modified';
|
|
1309
|
-
|
|
1310
|
-
|
|
1320
|
+
scheduleDbLive(
|
|
1321
|
+
c.executionCtx,
|
|
1322
|
+
emitDbLiveEvent(
|
|
1323
|
+
c.env,
|
|
1324
|
+
resolved.namespace,
|
|
1325
|
+
tableName,
|
|
1326
|
+
eventType as 'modified' | 'removed',
|
|
1327
|
+
'_bulk',
|
|
1328
|
+
{ action: body.action, count: succeeded },
|
|
1329
|
+
),
|
|
1330
|
+
`emit bulk ${resolved.namespace}.${tableName} (${body.action})`,
|
|
1311
1331
|
);
|
|
1312
1332
|
}
|
|
1313
1333
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared database live event emission helpers.
|
|
3
3
|
*
|
|
4
|
-
* Used by d1-handler.ts and postgres-handler.ts for
|
|
4
|
+
* Used by d1-handler.ts and postgres-handler.ts for background
|
|
5
5
|
* event delivery to DatabaseLiveDO after successful CUD operations.
|
|
6
6
|
*
|
|
7
7
|
* database-do.ts keeps its own internal version (uses DO env).
|
|
@@ -10,6 +10,16 @@ import type { Env } from '../types.js';
|
|
|
10
10
|
|
|
11
11
|
export const DATABASE_LIVE_HUB_DO_NAME = 'database-live:hub';
|
|
12
12
|
|
|
13
|
+
function withDeliveryId(event: Record<string, unknown>): Record<string, unknown> {
|
|
14
|
+
if (typeof event.deliveryId === 'string' && event.deliveryId.length > 0) {
|
|
15
|
+
return event;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
...event,
|
|
19
|
+
deliveryId: crypto.randomUUID(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
export function buildDbLiveChannel(
|
|
14
24
|
namespace: string,
|
|
15
25
|
table: string,
|
|
@@ -34,7 +44,7 @@ export function isDbLiveChannel(channel: string): boolean {
|
|
|
34
44
|
|
|
35
45
|
/**
|
|
36
46
|
* Emit a single CUD event to DatabaseLiveDO.
|
|
37
|
-
* Mirrors database-do.ts emitDbLiveEvent()
|
|
47
|
+
* Mirrors database-do.ts emitDbLiveEvent().
|
|
38
48
|
*/
|
|
39
49
|
export function emitDbLiveEvent(
|
|
40
50
|
env: Env,
|
|
@@ -87,25 +97,56 @@ export function emitDbLiveBatchEvent(
|
|
|
87
97
|
return sendToDatabaseLiveDO(env, event, '/internal/batch-event');
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
async function postToDatabaseLiveDO(
|
|
101
|
+
env: Env,
|
|
102
|
+
event: Record<string, unknown>,
|
|
103
|
+
path = '/internal/event',
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const liveNamespace = env.DATABASE_LIVE;
|
|
106
|
+
if (!liveNamespace) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const doId = liveNamespace.idFromName(DATABASE_LIVE_HUB_DO_NAME);
|
|
111
|
+
const stub = liveNamespace.get(doId);
|
|
112
|
+
const response = await stub.fetch(`http://internal${path}`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
115
|
+
body: JSON.stringify(event),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const detail = await response.text().catch(() => '');
|
|
120
|
+
const suffix = detail ? `: ${detail.slice(0, 200)}` : '';
|
|
121
|
+
throw new Error(`DatabaseLiveDO ${path} failed with ${response.status}${suffix}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
90
125
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
126
|
+
* Send an event to DatabaseLiveDO via stub.fetch().
|
|
127
|
+
* Callers should schedule this with waitUntil() when they want
|
|
128
|
+
* fire-and-forget request semantics without hiding delivery failures.
|
|
93
129
|
*/
|
|
94
|
-
export function sendToDatabaseLiveDO(
|
|
130
|
+
export async function sendToDatabaseLiveDO(
|
|
95
131
|
env: Env,
|
|
96
132
|
event: Record<string, unknown>,
|
|
97
133
|
path = '/internal/event',
|
|
98
134
|
): Promise<void> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
const eventWithDeliveryId = withDeliveryId(event);
|
|
136
|
+
let lastError: unknown;
|
|
137
|
+
|
|
138
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
139
|
+
try {
|
|
140
|
+
await postToDatabaseLiveDO(env, eventWithDeliveryId, path);
|
|
141
|
+
return;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
lastError = error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (lastError instanceof Error) {
|
|
148
|
+
throw lastError;
|
|
110
149
|
}
|
|
150
|
+
|
|
151
|
+
throw new Error('DatabaseLiveDO delivery failed.');
|
|
111
152
|
}
|
package/src/lib/do-router.ts
CHANGED
|
@@ -8,11 +8,36 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Constraint: `id` must NOT contain `:` character.
|
|
10
10
|
*/
|
|
11
|
-
import { materializeConfig, type EdgeBaseConfig } from '@edge-base/shared';
|
|
11
|
+
import { materializeConfig, type EdgeBaseConfig, type DbBlock } from '@edge-base/shared';
|
|
12
12
|
import { counter } from '../middleware/rate-limit.js';
|
|
13
13
|
|
|
14
14
|
const RUNTIME_CONFIG_GLOBAL_KEY = '__EDGEBASE_RUNTIME_CONFIG__';
|
|
15
15
|
|
|
16
|
+
export type DbTargetValidationIssue =
|
|
17
|
+
| 'namespace_not_found'
|
|
18
|
+
| 'instance_id_empty'
|
|
19
|
+
| 'instance_id_invalid'
|
|
20
|
+
| 'instance_id_required'
|
|
21
|
+
| 'instance_id_not_allowed';
|
|
22
|
+
|
|
23
|
+
export type DbTargetResolutionResult =
|
|
24
|
+
| {
|
|
25
|
+
ok: true;
|
|
26
|
+
value: {
|
|
27
|
+
namespace: string;
|
|
28
|
+
instanceId?: string;
|
|
29
|
+
dbBlock: DbBlock;
|
|
30
|
+
dynamic: boolean;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
ok: false;
|
|
35
|
+
issue: DbTargetValidationIssue;
|
|
36
|
+
status: 400 | 404;
|
|
37
|
+
namespace: string;
|
|
38
|
+
instanceId?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
16
41
|
// ─── DO Instance ID Generation (§2) ───
|
|
17
42
|
|
|
18
43
|
/**
|
|
@@ -53,6 +78,105 @@ export function parseDbDoName(doName: string): { namespace: string; id?: string
|
|
|
53
78
|
};
|
|
54
79
|
}
|
|
55
80
|
|
|
81
|
+
export function normalizeDbInstanceId(instanceId: string | null | undefined): string | undefined {
|
|
82
|
+
return typeof instanceId === 'string' ? instanceId : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatDbTargetValidationIssue(
|
|
86
|
+
issue: DbTargetValidationIssue,
|
|
87
|
+
namespace: string,
|
|
88
|
+
options: {
|
|
89
|
+
namespaceLabel?: string;
|
|
90
|
+
instanceIdLabel?: string;
|
|
91
|
+
includeSectionRef?: boolean;
|
|
92
|
+
} = {},
|
|
93
|
+
): string {
|
|
94
|
+
const namespaceLabel = options.namespaceLabel ?? 'Database';
|
|
95
|
+
const instanceIdLabel = options.instanceIdLabel ?? 'instanceId';
|
|
96
|
+
switch (issue) {
|
|
97
|
+
case 'namespace_not_found':
|
|
98
|
+
return `${namespaceLabel} '${namespace}' not found in config`;
|
|
99
|
+
case 'instance_id_empty':
|
|
100
|
+
return `${instanceIdLabel} must not be empty`;
|
|
101
|
+
case 'instance_id_invalid':
|
|
102
|
+
return options.includeSectionRef
|
|
103
|
+
? `${instanceIdLabel} must not contain ':' (§2)`
|
|
104
|
+
: `${instanceIdLabel} must not contain ':'`;
|
|
105
|
+
case 'instance_id_required':
|
|
106
|
+
return `${instanceIdLabel} is required for dynamic namespace '${namespace}'`;
|
|
107
|
+
case 'instance_id_not_allowed':
|
|
108
|
+
return `${instanceIdLabel} is not allowed for single-instance namespace '${namespace}'`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolveDbTarget(
|
|
113
|
+
config: EdgeBaseConfig,
|
|
114
|
+
namespace: string,
|
|
115
|
+
instanceId?: string | null,
|
|
116
|
+
): DbTargetResolutionResult {
|
|
117
|
+
const normalizedInstanceId = normalizeDbInstanceId(instanceId);
|
|
118
|
+
const dbBlock = config.databases?.[namespace];
|
|
119
|
+
if (!dbBlock) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
issue: 'namespace_not_found',
|
|
123
|
+
status: 404,
|
|
124
|
+
namespace,
|
|
125
|
+
instanceId: normalizedInstanceId,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (normalizedInstanceId !== undefined && normalizedInstanceId.trim().length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
issue: 'instance_id_empty',
|
|
133
|
+
status: 400,
|
|
134
|
+
namespace,
|
|
135
|
+
instanceId: normalizedInstanceId,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (normalizedInstanceId?.includes(':')) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
issue: 'instance_id_invalid',
|
|
143
|
+
status: 400,
|
|
144
|
+
namespace,
|
|
145
|
+
instanceId: normalizedInstanceId,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const dynamic = isDynamicDbBlock(dbBlock);
|
|
150
|
+
if (dynamic && normalizedInstanceId === undefined) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
issue: 'instance_id_required',
|
|
154
|
+
status: 400,
|
|
155
|
+
namespace,
|
|
156
|
+
instanceId: normalizedInstanceId,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (!dynamic && normalizedInstanceId !== undefined) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
issue: 'instance_id_not_allowed',
|
|
163
|
+
status: 400,
|
|
164
|
+
namespace,
|
|
165
|
+
instanceId: normalizedInstanceId,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
value: {
|
|
172
|
+
namespace,
|
|
173
|
+
instanceId: normalizedInstanceId,
|
|
174
|
+
dbBlock,
|
|
175
|
+
dynamic,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
56
180
|
// ─── DO Stub Call Helper ───
|
|
57
181
|
|
|
58
182
|
/**
|
|
@@ -258,13 +382,21 @@ export function shouldRouteToD1(namespace: string, config: EdgeBaseConfig): bool
|
|
|
258
382
|
if (dbBlock.provider === 'neon' || dbBlock.provider === 'postgres' || dbBlock.provider === 'do') return false;
|
|
259
383
|
|
|
260
384
|
// Auto-detect: multi-tenant namespaces stay in DO
|
|
261
|
-
if (dbBlock
|
|
262
|
-
if (dbBlock.access?.canCreate || dbBlock.access?.access) return false;
|
|
385
|
+
if (isDynamicDbBlock(dbBlock)) return false;
|
|
263
386
|
|
|
264
387
|
// Default: single-instance → D1
|
|
265
388
|
return true;
|
|
266
389
|
}
|
|
267
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Dynamic DB blocks represent per-instance / multi-tenant storage and therefore
|
|
393
|
+
* require create/access authorization semantics that single-instance DB blocks do not.
|
|
394
|
+
*/
|
|
395
|
+
export function isDynamicDbBlock(dbBlock?: DbBlock): boolean {
|
|
396
|
+
if (!dbBlock) return false;
|
|
397
|
+
return Boolean(dbBlock.instance || dbBlock.access?.canCreate || dbBlock.access?.access);
|
|
398
|
+
}
|
|
399
|
+
|
|
268
400
|
/**
|
|
269
401
|
* Get the D1 binding name for a single-instance namespace.
|
|
270
402
|
* Convention: DB_D1_{NAMESPACE_UPPER}
|