@edge-base/server 0.2.3 → 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.
Files changed (82) hide show
  1. package/admin-build/_app/immutable/chunks/{CzSAxmuj.js → 5RQRbp5q.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{B5Nwfelm.js → BME_U9TJ.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{Dlty5069.js → BYI6CUvd.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{A_3UuvCe.js → BgDzp0i0.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{DRqPU3wD.js → BjWZuf8W.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{Dc1-6Po6.js → C6lpZLE2.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{BxoNtYHK.js → D5GswVnI.js} +3 -3
  8. package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
  9. package/admin-build/_app/immutable/chunks/{nZvorU8i.js → DYaCRWMA.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
  11. package/admin-build/_app/immutable/chunks/{DpVAayDG.js → Dj-E9-FO.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{DCvwWZrm.js → Dj0QUuOf.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{B-_-hJ9o.js → XQM1k9PM.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{Du5vWVa2.js → fYEKMQ-Z.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{CZ0TVkCa.js → g_-Kpxu3.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{DCKcAiQH.js → wCNueVYy.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.CfrmEXPD.js → app.C8ylfBe6.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.Cn2BZ4da.js → 0.CJJ6HZbp.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.Dv4LX_Co.js → 1.B4sI5cB4.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.DPVv3kat.js → 10.D6hvCer6.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.CiCb6Ayu.js → 11.Dx7b8aQ5.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.CIPyeekF.js → 12.Bqmy5KIF.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.Z15Lt36e.js → 13.CC6KpXgS.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.s0l5bAq3.js → 14.yCo1Ix8E.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.UwSSNO76.js → 15.co0UfPlh.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.qiD8i883.js → 16.D0xkPUBW.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.Dy3dcSvu.js → 17.CebNqPeh.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.DeXyPYsO.js → 18.JUoLOZxh.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.CAbuyS6w.js → 19.ND8kmQJe.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.Bec0T7un.js → 20.DYb-q3W8.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.CdVprrv2.js → 22.UOzm8WYV.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.Y8RzVLoF.js → 23.BLgq21om.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.CWhHYFBx.js → 24.DN9usmUs.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.wCBplOVt.js → 25.BddRfAyE.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.Cod_JRFK.js → 26.Dl6XHIeT.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.BO2HVMu9.js → 27.D0iNwALG.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.DxG-FBVQ.js → 28.9dKQmdGi.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.CjGqWGvE.js → 29.wXzfJUXp.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.By3_OmdZ.js → 3.z8ut3jS-.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.M_H7Htpq.js → 30.BtZETNsL.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.DEU18izM.js → 31.CYonj2Jh.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.DeYhKtzJ.js → 4.COtDPQ9b.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.9WLgxhrD.js → 5.CTRCeIhp.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.BdT2i_dd.js → 6.ChHi3QkR.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.CHq0s4K6.js → 7.CCMtr6Ac.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.DuvRw-XZ.js → 8.DpWJ-X_-.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.C2Ub82wn.js → 9.DOkvfmir.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -3
  53. package/src/__tests__/admin-data-routes.test.ts +29 -0
  54. package/src/__tests__/database-do-route-validation.test.ts +105 -0
  55. package/src/__tests__/database-live-route.test.ts +82 -0
  56. package/src/__tests__/do-router.test.ts +116 -0
  57. package/src/__tests__/functions-context.test.ts +84 -0
  58. package/src/__tests__/functions-d1-proxy.test.ts +54 -0
  59. package/src/__tests__/plugin-migration-routing.test.ts +32 -0
  60. package/src/__tests__/provider-aware-sql.test.ts +9 -3
  61. package/src/__tests__/scheduled.test.ts +55 -0
  62. package/src/__tests__/service-key-db-proxy.test.ts +122 -1
  63. package/src/__tests__/sql-route.test.ts +66 -0
  64. package/src/__tests__/table-hook-runtime.test.ts +137 -0
  65. package/src/durable-objects/database-do.ts +36 -45
  66. package/src/index.ts +12 -6
  67. package/src/lib/d1-handler.ts +10 -21
  68. package/src/lib/do-router.ts +135 -3
  69. package/src/lib/functions.ts +4 -3
  70. package/src/lib/internal-transport.ts +28 -12
  71. package/src/lib/plugin-migration-routing.ts +28 -0
  72. package/src/lib/postgres-handler.ts +12 -20
  73. package/src/lib/provider-aware-sql.ts +19 -15
  74. package/src/lib/table-hook-runtime.ts +62 -0
  75. package/src/routes/admin.ts +41 -41
  76. package/src/routes/database-live.ts +110 -12
  77. package/src/routes/sql.ts +22 -17
  78. package/src/routes/tables.ts +42 -29
  79. package/admin-build/_app/immutable/chunks/DiyBpamp.js +0 -1
  80. package/admin-build/_app/immutable/chunks/byv2rTy8.js +0 -1
  81. package/admin-build/_app/immutable/entry/start.l1WvHznQ.js +0 -1
  82. package/admin-build/_app/immutable/nodes/21.DuDYelMY.js +0 -1
@@ -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, sendToDatabaseLiveDO } from './database-live-emitter.js';
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
- databaseLive: {
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, resolved.dbBlock.tables ?? {}, c.env, c.executionCtx);
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, resolved.dbBlock.tables ?? {}, c.env, c.executionCtx);
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, resolved.dbBlock.tables ?? {}, c.env, c.executionCtx);
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, resolved.dbBlock.tables ?? {}, c.env, c.executionCtx);
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, resolved.dbBlock.tables ?? {}, c.env, c.executionCtx);
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);
@@ -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.instance) return false;
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}
@@ -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: id },
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
- id,
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 { getDbDoName, callDO, shouldRouteToD1 } from './do-router.js';
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 doName = getDbDoName(namespace, instanceId);
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
- instanceId,
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 = this.config.databases?.[namespace]?.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, instanceId, tableName, directPath, headers, query, body);
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, instanceId, tableName, directPath, headers, query, body);
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, !!instanceId);
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 = instanceId
200
- ? `/api/db/${namespace}/${instanceId}${directPathWithQuery}`
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, !!instanceId);
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
- tables: Record<string, TableConfig>,
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
- databaseLive: {
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, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
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, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
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, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
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, resolved.dbBlock.tables ?? {}, c.executionCtx);
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, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
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, resolved.dbBlock.tables ?? {}, c.executionCtx);
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, resolved.dbBlock.tables ?? {}, c.executionCtx, query);
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, resolved.dbBlock.tables ?? {}, c.executionCtx);
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 { getD1BindingName, shouldRouteToD1 } from './do-router.js';
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 isDynamicNamespace = !!(
718
- dbBlock?.instance ||
719
- dbBlock?.access?.canCreate ||
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 (!id && (dbBlock?.provider === 'neon' || dbBlock?.provider === 'postgres')) {
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 (!id && shouldRouteToD1(namespace, opts.config)) {
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({ namespace, id, sql: rewriteTaggedTemplateQuery('question'), params }),
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,62 @@
1
+ import type { EdgeBaseConfig, HookCtx } from '@edge-base/shared';
2
+ import { ensureAuthSchema } from './auth-d1.js';
3
+ import { resolveAuthDb, type AuthDb } from './auth-db-adapter.js';
4
+ import { sendToDatabaseLiveDO } from './database-live-emitter.js';
5
+ import { createPushProvider } from './push-provider.js';
6
+ import { getDevicesForUser } from './push-token.js';
7
+ import type { Env } from '../types.js';
8
+
9
+ type PushTokenStore = KVNamespace | { kv: KVNamespace; authDb?: AuthDb | null };
10
+
11
+ async function resolvePushTokenStore(env: Env): Promise<PushTokenStore | null> {
12
+ if (!env.KV) {
13
+ return null;
14
+ }
15
+
16
+ try {
17
+ const authDb = resolveAuthDb(env as unknown as Record<string, unknown>);
18
+ await ensureAuthSchema(authDb);
19
+ return { kv: env.KV, authDb };
20
+ } catch {
21
+ return env.KV;
22
+ }
23
+ }
24
+
25
+ export function buildTableHookRuntimeServices(
26
+ config: EdgeBaseConfig,
27
+ env: Env,
28
+ ): Pick<HookCtx, 'databaseLive' | 'push'> {
29
+ return {
30
+ databaseLive: {
31
+ async broadcast(channel: string, event: string, data: unknown): Promise<void> {
32
+ await sendToDatabaseLiveDO(
33
+ env,
34
+ { channel, event, payload: data ?? {} },
35
+ '/internal/broadcast',
36
+ );
37
+ },
38
+ },
39
+ push: {
40
+ async send(userId: string, payload: { title?: string; body: string }): Promise<void> {
41
+ try {
42
+ const tokenStore = await resolvePushTokenStore(env);
43
+ if (!tokenStore) return;
44
+
45
+ const provider = createPushProvider(config.push, env);
46
+ if (!provider) return;
47
+
48
+ const devices = await getDevicesForUser(tokenStore, userId);
49
+ if (devices.length === 0) return;
50
+
51
+ await Promise.allSettled(
52
+ devices.map((device) =>
53
+ provider.send({ token: device.token, platform: device.platform, payload }),
54
+ ),
55
+ );
56
+ } catch (error) {
57
+ console.warn('[EdgeBase] table hook push.send failed:', error);
58
+ }
59
+ },
60
+ },
61
+ };
62
+ }