@edge-base/server 0.2.3 → 0.2.5
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/{DpVAayDG.js → 6oMK_164.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B5Nwfelm.js → B2TnDKF7.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DCvwWZrm.js → B6MschND.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Du5vWVa2.js → B94PilAN.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dc1-6Po6.js → BEW7Ez_g.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dlty5069.js → BoOooyH6.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CzSAxmuj.js → BqTb6Mxk.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DCKcAiQH.js → BvHnF5tV.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B-_-hJ9o.js → CaVKAiCe.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DRqPU3wD.js → Cdm5zBRA.js} +1 -1
- package/admin-build/_app/immutable/chunks/{byv2rTy8.js → CrOZMmdF.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DiyBpamp.js → Cw6OYcq-.js} +1 -1
- package/admin-build/_app/immutable/chunks/{A_3UuvCe.js → D2j3I1VQ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BxoNtYHK.js → DPdQ7z0T.js} +3 -3
- package/admin-build/_app/immutable/chunks/{nZvorU8i.js → J2Gw0SMu.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CZ0TVkCa.js → pUxw8jfq.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.CfrmEXPD.js → app.D3flihMw.js} +2 -2
- package/admin-build/_app/immutable/entry/start.Cl6sLxnz.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.Cn2BZ4da.js → 0.CdczqZLK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.Dv4LX_Co.js → 1.DxcSsEqS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.DPVv3kat.js → 10.DuAd4aIm.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.CiCb6Ayu.js → 11.0jgHQL92.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.CIPyeekF.js → 12.CKNPqmyy.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.Z15Lt36e.js → 13.B1p2POXS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.s0l5bAq3.js → 14.Bb-REBND.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.UwSSNO76.js → 15.1uBFCX0X.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.qiD8i883.js → 16.BR7WwQrS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.Dy3dcSvu.js → 17.Cm57KKXV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.DeXyPYsO.js → 18.CoiwfAuQ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.CAbuyS6w.js → 19.B8ZdLlXj.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.Bec0T7un.js → 20.DnHeFlTv.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.CJFaf0Ia.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.CdVprrv2.js → 22.CItETFzy.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.Y8RzVLoF.js → 23.CWSGMcKJ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CWhHYFBx.js → 24.CWbEqNMB.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.wCBplOVt.js → 25.DRkLEhKi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.Cod_JRFK.js → 26.BRxO8AYH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.BO2HVMu9.js → 27.BLs-nVHz.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.DxG-FBVQ.js → 28.G79qkdBK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.CjGqWGvE.js → 29.BOcI6g0N.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.By3_OmdZ.js → 3.B6q-7qr8.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.M_H7Htpq.js → 30.DAIC7dKd.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.DEU18izM.js → 31.pl0XXjXF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.DeYhKtzJ.js → 4.DOdvVlZj.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.9WLgxhrD.js → 5.BW_zlgye.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BdT2i_dd.js → 6.Dxy1CAI2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.CHq0s4K6.js → 7.BG98w_o7.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.DuvRw-XZ.js → 8.DoG5R2rG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.C2Ub82wn.js → 9.Dmxf6zAC.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__/database-do-route-validation.test.ts +108 -0
- package/src/__tests__/database-live-route.test.ts +82 -0
- package/src/__tests__/do-router.test.ts +116 -0
- package/src/__tests__/functions-context.test.ts +84 -0
- package/src/__tests__/functions-d1-proxy.test.ts +54 -0
- package/src/__tests__/meta-route-registration.test.ts +20 -15
- package/src/__tests__/plugin-migration-routing.test.ts +32 -0
- package/src/__tests__/provider-aware-sql.test.ts +9 -3
- package/src/__tests__/room-auth-state-loss.test.ts +122 -0
- package/src/__tests__/room-handler-context.test.ts +4 -4
- package/src/__tests__/room-rate-limit-scopes.test.ts +38 -0
- package/src/__tests__/runtime-startup.test.ts +49 -0
- 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 +66 -0
- package/src/__tests__/table-hook-runtime.test.ts +137 -0
- package/src/durable-objects/database-do.ts +50 -45
- package/src/durable-objects/database-live-do.ts +15 -0
- package/src/durable-objects/room-runtime-base.ts +387 -129
- package/src/durable-objects/rooms-do.ts +31 -24
- package/src/index.ts +334 -282
- package/src/lib/d1-handler.ts +10 -21
- package/src/lib/do-router.ts +135 -3
- package/src/lib/functions.ts +4 -3
- package/src/lib/internal-transport.ts +28 -12
- package/src/lib/plugin-migration-routing.ts +28 -0
- package/src/lib/postgres-handler.ts +12 -20
- package/src/lib/provider-aware-sql.ts +19 -15
- package/src/lib/runtime-startup.ts +53 -0
- package/src/lib/table-hook-runtime.ts +62 -0
- package/src/routes/admin.ts +41 -41
- package/src/routes/database-live.ts +110 -12
- package/src/routes/sql.ts +22 -17
- package/src/routes/tables.ts +42 -29
- package/admin-build/_app/immutable/entry/start.l1WvHznQ.js +0 -1
- package/admin-build/_app/immutable/nodes/21.DuDYelMY.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);
|
|
@@ -448,7 +437,7 @@ async function handleList(
|
|
|
448
437
|
|
|
449
438
|
// Apply onEnrich hook
|
|
450
439
|
if (tableHooks?.onEnrich) {
|
|
451
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
440
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
452
441
|
for (let i = 0; i < items.length; i++) {
|
|
453
442
|
try {
|
|
454
443
|
const enriched = await tableHooks.onEnrich(auth, items[i], hookCtx);
|
|
@@ -612,7 +601,7 @@ async function handleGet(
|
|
|
612
601
|
|
|
613
602
|
// Apply onEnrich hook
|
|
614
603
|
if (tableHooks?.onEnrich) {
|
|
615
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
604
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
616
605
|
try {
|
|
617
606
|
const enriched = await tableHooks.onEnrich(auth, row, hookCtx);
|
|
618
607
|
if (enriched && typeof enriched === 'object') return c.json({ ...row, ...enriched });
|
|
@@ -678,7 +667,7 @@ async function handleInsert(
|
|
|
678
667
|
}
|
|
679
668
|
|
|
680
669
|
// Run beforeInsert hook
|
|
681
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
670
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
682
671
|
if (tableHooks?.beforeInsert) {
|
|
683
672
|
try {
|
|
684
673
|
const transformed = await tableHooks.beforeInsert(auth, body, hookCtx);
|
|
@@ -845,7 +834,7 @@ async function handleUpdate(
|
|
|
845
834
|
}
|
|
846
835
|
|
|
847
836
|
// Run beforeUpdate hook
|
|
848
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
837
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
849
838
|
if (tableHooks?.beforeUpdate) {
|
|
850
839
|
try {
|
|
851
840
|
const transformed = await tableHooks.beforeUpdate(auth, existingRow, body, hookCtx);
|
|
@@ -962,7 +951,7 @@ async function handleDelete(
|
|
|
962
951
|
}
|
|
963
952
|
|
|
964
953
|
// Run beforeDelete hook
|
|
965
|
-
const hookCtx = buildHookCtx(resolved.db,
|
|
954
|
+
const hookCtx = buildHookCtx(resolved.db, c.env, c.executionCtx);
|
|
966
955
|
if (tableHooks?.beforeDelete) {
|
|
967
956
|
try {
|
|
968
957
|
await tableHooks.beforeDelete(auth, existingRow, hookCtx);
|
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}
|
package/src/lib/functions.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
ScheduleTrigger,
|
|
26
26
|
HttpTrigger,
|
|
27
27
|
} from '@edge-base/shared';
|
|
28
|
-
import { getD1BindingName } from './do-router.js';
|
|
28
|
+
import { getD1BindingName, normalizeDbInstanceId } from './do-router.js';
|
|
29
29
|
import { D1AuthDb, type AuthDb } from './auth-db-adapter.js';
|
|
30
30
|
import { ensureAuthSchema } from './auth-d1.js';
|
|
31
31
|
import type { Env } from '../types.js';
|
|
@@ -822,6 +822,7 @@ export function buildAdminDbProxy(options: BuildAdminDbProxyOptions): FunctionAd
|
|
|
822
822
|
);
|
|
823
823
|
|
|
824
824
|
return (namespace: string, id?: string): DbRef => {
|
|
825
|
+
const normalizedId = normalizeDbInstanceId(id);
|
|
825
826
|
// Create a per-DbRef transport with explicit dbContext so that
|
|
826
827
|
// path parsing is unambiguous even when instanceId === 'tables'.
|
|
827
828
|
const transport = new InternalHttpTransport({
|
|
@@ -832,13 +833,13 @@ export function buildAdminDbProxy(options: BuildAdminDbProxyOptions): FunctionAd
|
|
|
832
833
|
env: options.env,
|
|
833
834
|
executionCtx: options.executionCtx,
|
|
834
835
|
preferDirectDo: options.preferDirectDo,
|
|
835
|
-
dbContext: { namespace, instanceId:
|
|
836
|
+
dbContext: { namespace, instanceId: normalizedId },
|
|
836
837
|
});
|
|
837
838
|
const dbApi = new DefaultDbApi(transport);
|
|
838
839
|
return new DbRef(
|
|
839
840
|
dbApi,
|
|
840
841
|
namespace,
|
|
841
|
-
|
|
842
|
+
normalizedId,
|
|
842
843
|
undefined, // databaseLiveClient — not available server-side
|
|
843
844
|
undefined, // filterMatchFn
|
|
844
845
|
httpClient, // enables table().sql`...` tagged template
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { HttpTransport } from '@edge-base/core';
|
|
10
10
|
import type { EdgeBaseConfig } from '@edge-base/shared';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
callDO,
|
|
13
|
+
formatDbTargetValidationIssue,
|
|
14
|
+
getDbDoName,
|
|
15
|
+
resolveDbTarget,
|
|
16
|
+
shouldRouteToD1,
|
|
17
|
+
} from './do-router.js';
|
|
12
18
|
import { handleD1Request } from './d1-handler.js';
|
|
13
19
|
import { handlePgRequest } from './postgres-handler.js';
|
|
14
20
|
import { buildInternalHandlerContext } from './internal-request.js';
|
|
@@ -115,7 +121,12 @@ export class InternalHttpTransport implements HttpTransport {
|
|
|
115
121
|
options?: { query?: Record<string, string>; body?: unknown },
|
|
116
122
|
): Promise<T> {
|
|
117
123
|
const { namespace, instanceId, tableName, directPath } = parsePath(path, this.dbContext);
|
|
118
|
-
const
|
|
124
|
+
const target = resolveDbTarget(this.config, namespace, instanceId);
|
|
125
|
+
if (!target.ok) {
|
|
126
|
+
throw new Error(formatDbTargetValidationIssue(target.issue, namespace));
|
|
127
|
+
}
|
|
128
|
+
const { instanceId: normalizedInstanceId } = target.value;
|
|
129
|
+
const doName = getDbDoName(namespace, normalizedInstanceId);
|
|
119
130
|
|
|
120
131
|
// Build internal headers
|
|
121
132
|
const headers: Record<string, string> = {
|
|
@@ -141,7 +152,7 @@ export class InternalHttpTransport implements HttpTransport {
|
|
|
141
152
|
const res = await this.routeRequest(
|
|
142
153
|
method as 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT',
|
|
143
154
|
namespace,
|
|
144
|
-
|
|
155
|
+
normalizedInstanceId,
|
|
145
156
|
tableName,
|
|
146
157
|
directPath,
|
|
147
158
|
doName,
|
|
@@ -174,36 +185,41 @@ export class InternalHttpTransport implements HttpTransport {
|
|
|
174
185
|
query: URLSearchParams,
|
|
175
186
|
body?: Record<string, unknown>,
|
|
176
187
|
): Promise<Response> {
|
|
188
|
+
const target = resolveDbTarget(this.config, namespace, instanceId);
|
|
189
|
+
if (!target.ok) {
|
|
190
|
+
throw new Error(formatDbTargetValidationIssue(target.issue, namespace));
|
|
191
|
+
}
|
|
192
|
+
const { dbBlock, dynamic, instanceId: normalizedInstanceId } = target.value;
|
|
177
193
|
const queryString = Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
|
|
178
194
|
const directPathWithQuery = `${directPath}${queryString}`;
|
|
179
|
-
const provider =
|
|
195
|
+
const provider = dbBlock.provider;
|
|
180
196
|
const httpMethod = method === 'PUT' ? 'PATCH' : method; // normalize PUT → PATCH
|
|
181
197
|
|
|
182
198
|
// 1. D1 route
|
|
183
|
-
if (!this.preferDirectDo && shouldRouteToD1(namespace, this.config) && this.env) {
|
|
184
|
-
return this.requestViaD1Handler(httpMethod, namespace,
|
|
199
|
+
if (!this.preferDirectDo && !dynamic && shouldRouteToD1(namespace, this.config) && this.env) {
|
|
200
|
+
return this.requestViaD1Handler(httpMethod, namespace, normalizedInstanceId, tableName, directPath, headers, query, body);
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
// 2. PostgreSQL route
|
|
188
|
-
if ((provider === 'neon' || provider === 'postgres') && this.env) {
|
|
189
|
-
return this.requestViaPgHandler(httpMethod, namespace,
|
|
204
|
+
if (!dynamic && (provider === 'neon' || provider === 'postgres') && this.env) {
|
|
205
|
+
return this.requestViaPgHandler(httpMethod, namespace, normalizedInstanceId, tableName, directPath, headers, query, body);
|
|
190
206
|
}
|
|
191
207
|
|
|
192
208
|
// 3. Direct DO route
|
|
193
209
|
if (this.env) {
|
|
194
|
-
return this.requestViaDirectDo(httpMethod, doName, directPathWithQuery, headers, body,
|
|
210
|
+
return this.requestViaDirectDo(httpMethod, doName, directPathWithQuery, headers, body, dynamic);
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
// 4. Worker HTTP fallback
|
|
198
214
|
if (this.workerUrl) {
|
|
199
|
-
const apiPath =
|
|
200
|
-
? `/api/db/${namespace}/${
|
|
215
|
+
const apiPath = normalizedInstanceId
|
|
216
|
+
? `/api/db/${namespace}/${normalizedInstanceId}${directPathWithQuery}`
|
|
201
217
|
: `/api/db/${namespace}${directPathWithQuery}`;
|
|
202
218
|
return this.requestViaWorker(httpMethod, apiPath, headers, body);
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
// 5. Fallback: direct DO
|
|
206
|
-
return this.requestViaDirectDo(httpMethod, doName, directPathWithQuery, headers, body,
|
|
222
|
+
return this.requestViaDirectDo(httpMethod, doName, directPathWithQuery, headers, body, dynamic);
|
|
207
223
|
}
|
|
208
224
|
|
|
209
225
|
private async requestViaWorker(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const PLUGIN_MIGRATION_PATH_PREFIXES = [
|
|
2
|
+
'/api/auth',
|
|
3
|
+
'/api/db',
|
|
4
|
+
'/api/functions',
|
|
5
|
+
'/api/sql',
|
|
6
|
+
'/api/storage',
|
|
7
|
+
'/admin/api',
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export function shouldRunPluginMigrationsForRequestPath(path: string): boolean {
|
|
11
|
+
if (!path.startsWith('/')) {
|
|
12
|
+
path = `/${path}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
path === '/admin/api/backup'
|
|
17
|
+
|| path.startsWith('/admin/api/backup/')
|
|
18
|
+
|| path.startsWith('/admin/api/data/backup/')
|
|
19
|
+
|| path === '/internal/backup'
|
|
20
|
+
|| path.startsWith('/internal/backup/')
|
|
21
|
+
) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return PLUGIN_MIGRATION_PATH_PREFIXES.some(
|
|
26
|
+
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import { isTrustedInternalContext } from './internal-request.js';
|
|
52
52
|
import { executeDbTriggers } from './functions.js';
|
|
53
53
|
import { parseUpdateBody } from './op-parser.js';
|
|
54
|
+
import { buildTableHookRuntimeServices } from './table-hook-runtime.js';
|
|
54
55
|
|
|
55
56
|
// ─── Types ───
|
|
56
57
|
|
|
@@ -232,13 +233,14 @@ async function evalInsertRule(
|
|
|
232
233
|
|
|
233
234
|
function buildHookCtx(
|
|
234
235
|
connectionString: string,
|
|
235
|
-
|
|
236
|
+
env: Env,
|
|
236
237
|
executionCtx?: ExecutionContext,
|
|
237
238
|
queryExecutor?: PostgresExecutor,
|
|
238
239
|
): HookCtx {
|
|
239
240
|
const query =
|
|
240
241
|
queryExecutor ??
|
|
241
242
|
((sql: string, params: unknown[] = []) => executePostgresQuery(connectionString, sql, params));
|
|
243
|
+
const runtimeServices = buildTableHookRuntimeServices(parseConfig(env), env);
|
|
242
244
|
|
|
243
245
|
return {
|
|
244
246
|
db: {
|
|
@@ -279,17 +281,7 @@ function buildHookCtx(
|
|
|
279
281
|
return result.rows.length > 0;
|
|
280
282
|
},
|
|
281
283
|
},
|
|
282
|
-
|
|
283
|
-
async broadcast(_channel: string, _event: string, _data: unknown): Promise<void> {
|
|
284
|
-
// HookCtx broadcast — not implemented for PostgreSQL provider (no direct env access)
|
|
285
|
-
// Use database-live subscription from client SDK instead
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
push: {
|
|
289
|
-
async send(_userId: string, _payload: { title?: string; body: string }): Promise<void> {
|
|
290
|
-
// Push notifications — same mechanism as DO (via Worker env)
|
|
291
|
-
},
|
|
292
|
-
},
|
|
284
|
+
...runtimeServices,
|
|
293
285
|
waitUntil(promise: Promise<unknown>): void {
|
|
294
286
|
if (executionCtx) {
|
|
295
287
|
executionCtx.waitUntil(promise);
|
|
@@ -358,7 +350,7 @@ async function handleList(
|
|
|
358
350
|
|
|
359
351
|
// Apply onEnrich hook
|
|
360
352
|
if (tableHooks?.onEnrich) {
|
|
361
|
-
const hookCtx = buildHookCtx(resolved.connectionString,
|
|
353
|
+
const hookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
|
|
362
354
|
for (let i = 0; i < items.length; i++) {
|
|
363
355
|
try {
|
|
364
356
|
const enriched = await tableHooks.onEnrich(auth, items[i], hookCtx);
|
|
@@ -507,7 +499,7 @@ async function handleGet(
|
|
|
507
499
|
|
|
508
500
|
// Apply onEnrich hook
|
|
509
501
|
if (tableHooks?.onEnrich) {
|
|
510
|
-
const hookCtx = buildHookCtx(resolved.connectionString,
|
|
502
|
+
const hookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
|
|
511
503
|
try {
|
|
512
504
|
const enriched = await tableHooks.onEnrich(auth, row, hookCtx);
|
|
513
505
|
if (enriched && typeof enriched === 'object') return c.json({ ...row, ...enriched });
|
|
@@ -558,7 +550,7 @@ async function handleInsert(
|
|
|
558
550
|
}
|
|
559
551
|
|
|
560
552
|
// Run beforeInsert hook
|
|
561
|
-
const requestHookCtx = buildHookCtx(resolved.connectionString,
|
|
553
|
+
const requestHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
|
|
562
554
|
if (tableHooks?.beforeInsert) {
|
|
563
555
|
try {
|
|
564
556
|
const transformed = await tableHooks.beforeInsert(auth, body, requestHookCtx);
|
|
@@ -612,7 +604,7 @@ async function handleInsert(
|
|
|
612
604
|
// Run afterInsert hook (fire-and-forget)
|
|
613
605
|
if (tableHooks?.afterInsert) {
|
|
614
606
|
const hook = tableHooks.afterInsert;
|
|
615
|
-
const backgroundHookCtx = buildHookCtx(resolved.connectionString,
|
|
607
|
+
const backgroundHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx);
|
|
616
608
|
backgroundHookCtx.waitUntil(Promise.resolve(hook(inserted, backgroundHookCtx)).catch(() => {}));
|
|
617
609
|
}
|
|
618
610
|
|
|
@@ -703,7 +695,7 @@ async function handleUpdate(
|
|
|
703
695
|
}
|
|
704
696
|
|
|
705
697
|
// Run beforeUpdate hook
|
|
706
|
-
const requestHookCtx = buildHookCtx(resolved.connectionString,
|
|
698
|
+
const requestHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
|
|
707
699
|
if (tableHooks?.beforeUpdate) {
|
|
708
700
|
try {
|
|
709
701
|
const transformed = await tableHooks.beforeUpdate(auth, existingRow, body, requestHookCtx);
|
|
@@ -740,7 +732,7 @@ async function handleUpdate(
|
|
|
740
732
|
// Run afterUpdate hook (fire-and-forget)
|
|
741
733
|
if (tableHooks?.afterUpdate) {
|
|
742
734
|
const hook = tableHooks.afterUpdate;
|
|
743
|
-
const backgroundHookCtx = buildHookCtx(resolved.connectionString,
|
|
735
|
+
const backgroundHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx);
|
|
744
736
|
backgroundHookCtx.waitUntil(
|
|
745
737
|
Promise.resolve(hook(existingRow, updated, backgroundHookCtx)).catch(() => {}),
|
|
746
738
|
);
|
|
@@ -805,7 +797,7 @@ async function handleDelete(
|
|
|
805
797
|
}
|
|
806
798
|
|
|
807
799
|
// Run beforeDelete hook
|
|
808
|
-
const requestHookCtx = buildHookCtx(resolved.connectionString,
|
|
800
|
+
const requestHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx, query);
|
|
809
801
|
if (tableHooks?.beforeDelete) {
|
|
810
802
|
try {
|
|
811
803
|
await tableHooks.beforeDelete(auth, existingRow, requestHookCtx);
|
|
@@ -826,7 +818,7 @@ async function handleDelete(
|
|
|
826
818
|
// Run afterDelete hook (fire-and-forget)
|
|
827
819
|
if (tableHooks?.afterDelete) {
|
|
828
820
|
const hook = tableHooks.afterDelete;
|
|
829
|
-
const backgroundHookCtx = buildHookCtx(resolved.connectionString,
|
|
821
|
+
const backgroundHookCtx = buildHookCtx(resolved.connectionString, c.env, c.executionCtx);
|
|
830
822
|
backgroundHookCtx.waitUntil(
|
|
831
823
|
Promise.resolve(hook(existingRow, backgroundHookCtx)).catch(() => {}),
|
|
832
824
|
);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { EdgeBaseConfig } from '@edge-base/shared';
|
|
2
2
|
import { executeD1Sql } from './d1-sql.js';
|
|
3
3
|
import { executeDoSql } from './do-sql.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
formatDbTargetValidationIssue,
|
|
6
|
+
getD1BindingName,
|
|
7
|
+
resolveDbTarget,
|
|
8
|
+
shouldRouteToD1,
|
|
9
|
+
} from './do-router.js';
|
|
5
10
|
import {
|
|
6
11
|
ensureLocalDevPostgresSchema,
|
|
7
12
|
getLocalDevPostgresExecOptions,
|
|
@@ -710,23 +715,17 @@ export async function executeProviderAwareSql(
|
|
|
710
715
|
query: string,
|
|
711
716
|
params: unknown[] = [],
|
|
712
717
|
): Promise<ProviderAwareSqlResult> {
|
|
713
|
-
const dbBlock = opts.config.databases?.[namespace];
|
|
714
718
|
const usesTaggedTemplateMarkers = hasTaggedTemplateSqlMarkers(query);
|
|
715
719
|
const rewriteTaggedTemplateQuery = (style: 'postgres' | 'question') =>
|
|
716
720
|
usesTaggedTemplateMarkers ? replaceTaggedTemplateSqlMarkers(query, style, params.length) : query;
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
dbBlock?.access?.access
|
|
721
|
-
);
|
|
722
|
-
if (isDynamicNamespace && !id) {
|
|
723
|
-
throw new Error(
|
|
724
|
-
`admin.sqlProviderAware() requires an id for dynamic namespace '${namespace}'.`,
|
|
725
|
-
);
|
|
721
|
+
const target = resolveDbTarget(opts.config, namespace, id);
|
|
722
|
+
if (!target.ok) {
|
|
723
|
+
throw new Error(formatDbTargetValidationIssue(target.issue, namespace));
|
|
726
724
|
}
|
|
725
|
+
const { dbBlock, instanceId } = target.value;
|
|
727
726
|
|
|
728
727
|
if (opts.env) {
|
|
729
|
-
if (!
|
|
728
|
+
if (!instanceId && (dbBlock.provider === 'neon' || dbBlock.provider === 'postgres')) {
|
|
730
729
|
const bindingName = getProviderBindingName(namespace);
|
|
731
730
|
const envRecord = opts.env as unknown as Record<string, unknown>;
|
|
732
731
|
const hyperdrive = envRecord[bindingName] as { connectionString?: string } | undefined;
|
|
@@ -759,7 +758,7 @@ export async function executeProviderAwareSql(
|
|
|
759
758
|
);
|
|
760
759
|
}
|
|
761
760
|
|
|
762
|
-
if (!
|
|
761
|
+
if (!instanceId && shouldRouteToD1(namespace, opts.config)) {
|
|
763
762
|
const bindingName = getD1BindingName(namespace);
|
|
764
763
|
const d1 = (opts.env as unknown as Record<string, unknown>)[bindingName] as
|
|
765
764
|
| D1Database
|
|
@@ -780,7 +779,7 @@ export async function executeProviderAwareSql(
|
|
|
780
779
|
const rows = await executeDoSql({
|
|
781
780
|
databaseNamespace: opts.databaseNamespace,
|
|
782
781
|
namespace,
|
|
783
|
-
id,
|
|
782
|
+
id: instanceId,
|
|
784
783
|
query: rewriteTaggedTemplateQuery('question'),
|
|
785
784
|
params,
|
|
786
785
|
internal: true,
|
|
@@ -800,7 +799,12 @@ export async function executeProviderAwareSql(
|
|
|
800
799
|
'Content-Type': 'application/json',
|
|
801
800
|
'X-EdgeBase-Service-Key': opts.serviceKey,
|
|
802
801
|
},
|
|
803
|
-
body: JSON.stringify({
|
|
802
|
+
body: JSON.stringify({
|
|
803
|
+
namespace,
|
|
804
|
+
id: instanceId,
|
|
805
|
+
sql: rewriteTaggedTemplateQuery('question'),
|
|
806
|
+
params,
|
|
807
|
+
}),
|
|
804
808
|
});
|
|
805
809
|
if (!res.ok) {
|
|
806
810
|
const err = (await res.json().catch(() => ({ message: 'SQL execution failed' }))) as {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Compile-time constant — injected by wrangler [define] in wrangler.test.toml
|
|
2
|
+
declare const EDGEBASE_TEST_BUILD: boolean | undefined;
|
|
3
|
+
|
|
4
|
+
let startupPromise: Promise<void> | null = null;
|
|
5
|
+
|
|
6
|
+
async function detectWorkersTestRuntime(): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
await import('cloudflare:test');
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function ensureServerStartup(): Promise<void> {
|
|
16
|
+
if (startupPromise) {
|
|
17
|
+
return startupPromise;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
startupPromise = (async () => {
|
|
21
|
+
const [{ resolveStartupConfig }, generatedConfigModule, { initFunctionRegistry }, doRouterModule] = await Promise.all([
|
|
22
|
+
import('./startup-config.js'),
|
|
23
|
+
import('../generated-config.js'),
|
|
24
|
+
import('../_functions-registry.js'),
|
|
25
|
+
import('./do-router.js'),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const processEnv = typeof process !== 'undefined' ? process.env : undefined;
|
|
30
|
+
const isTestBuild = typeof EDGEBASE_TEST_BUILD !== 'undefined';
|
|
31
|
+
const preferTestConfig = await detectWorkersTestRuntime() || isTestBuild;
|
|
32
|
+
const existingConfig = doRouterModule.parseConfig();
|
|
33
|
+
const resolvedConfig = await resolveStartupConfig(
|
|
34
|
+
generatedConfigModule.default,
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
async () => import('../../edgebase.test.config.ts' as any),
|
|
37
|
+
processEnv,
|
|
38
|
+
{ preferTestConfig },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (resolvedConfig && Object.keys(existingConfig).length === 0) {
|
|
42
|
+
doRouterModule.setConfig(resolvedConfig);
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('[EdgeBase] Failed to initialize config at startup:', err);
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
initFunctionRegistry();
|
|
50
|
+
})();
|
|
51
|
+
|
|
52
|
+
return startupPromise;
|
|
53
|
+
}
|