@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.
Files changed (97) hide show
  1. package/admin-build/_app/immutable/chunks/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
  8. package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
  9. package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
  11. package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.BZxfavhF.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.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.VRVb0wee.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.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.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__/d1-live-broadcast-verification.test.ts +271 -0
  55. package/src/__tests__/database-do-route-validation.test.ts +105 -0
  56. package/src/__tests__/database-live-do.test.ts +50 -0
  57. package/src/__tests__/database-live-emitter.test.ts +116 -1
  58. package/src/__tests__/database-live-route.test.ts +82 -0
  59. package/src/__tests__/do-router.test.ts +116 -0
  60. package/src/__tests__/error-format.test.ts +63 -0
  61. package/src/__tests__/functions-context.test.ts +674 -33
  62. package/src/__tests__/functions-d1-proxy.test.ts +54 -0
  63. package/src/__tests__/plugin-migration-routing.test.ts +32 -0
  64. package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
  65. package/src/__tests__/provider-aware-sql.test.ts +163 -0
  66. package/src/__tests__/room-auth-state-loss.test.ts +124 -0
  67. package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
  68. package/src/__tests__/scheduled.test.ts +55 -0
  69. package/src/__tests__/service-key-db-proxy.test.ts +122 -1
  70. package/src/__tests__/sql-route.test.ts +252 -75
  71. package/src/__tests__/table-hook-runtime.test.ts +137 -0
  72. package/src/durable-objects/database-do.ts +36 -45
  73. package/src/durable-objects/database-live-do.ts +46 -1
  74. package/src/durable-objects/room-runtime-base.ts +26 -2
  75. package/src/durable-objects/rooms-do.ts +1 -1
  76. package/src/index.ts +12 -6
  77. package/src/lib/admin-db-target.ts +30 -74
  78. package/src/lib/d1-handler.ts +55 -35
  79. package/src/lib/database-live-emitter.ts +57 -16
  80. package/src/lib/do-router.ts +135 -3
  81. package/src/lib/functions.ts +215 -143
  82. package/src/lib/internal-transport.ts +28 -12
  83. package/src/lib/plugin-migration-routing.ts +28 -0
  84. package/src/lib/plugin-migrations.ts +38 -38
  85. package/src/lib/postgres-handler.ts +51 -31
  86. package/src/lib/provider-aware-sql.ts +831 -0
  87. package/src/lib/table-hook-runtime.ts +62 -0
  88. package/src/routes/admin.ts +41 -41
  89. package/src/routes/auth.ts +7 -2
  90. package/src/routes/database-live.ts +110 -12
  91. package/src/routes/sql.ts +64 -84
  92. package/src/routes/storage.ts +7 -2
  93. package/src/routes/tables.ts +42 -29
  94. package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
  95. package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
  96. package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
  97. package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +0 -1
@@ -25,8 +25,7 @@ import type {
25
25
  ScheduleTrigger,
26
26
  HttpTrigger,
27
27
  } from '@edge-base/shared';
28
- import { getD1BindingName, shouldRouteToD1 } from './do-router.js';
29
- import { executeDoSql } from './do-sql.js';
28
+ import { getD1BindingName, normalizeDbInstanceId } from './do-router.js';
30
29
  import { D1AuthDb, type AuthDb } from './auth-db-adapter.js';
31
30
  import { ensureAuthSchema } from './auth-d1.js';
32
31
  import type { Env } from '../types.js';
@@ -41,6 +40,7 @@ import { hashPassword } from './password.js';
41
40
  import { generateId } from './uuid.js';
42
41
  import { DbRef, TableRef, DefaultDbApi, HttpClient, ContextManager } from '@edge-base/core';
43
42
  import { InternalHttpTransport } from './internal-transport.js';
43
+ import { executeProviderAwareSql } from './provider-aware-sql.js';
44
44
 
45
45
  // ─── Function Context Types ───
46
46
 
@@ -95,11 +95,21 @@ export interface FunctionAdminContext {
95
95
  /** Admin user management. */
96
96
  auth: AdminAuthContext;
97
97
  /**
98
- * Execute raw SQL with direct D1/DO binding access — no HTTP round-trip.
99
- * Routes directly to D1 binding or Durable Object without network overhead.
98
+ * Execute raw SQL through the fastest provider-aware path available.
99
+ * Uses direct PostgreSQL / Neon, D1, or DO execution when possible,
100
+ * then falls back to the internal HTTP SQL route when needed.
100
101
  *
101
102
  * @example
102
- * const rows = await ctx.admin.sqlWithDirectD1Access('shared', undefined, 'SELECT * FROM posts WHERE status = ?', ['published']);
103
+ * const rows = await ctx.admin.sqlProviderAware('shared', undefined, 'SELECT * FROM posts WHERE status = ?', ['published']);
104
+ */
105
+ sqlProviderAware(
106
+ namespace: string,
107
+ id: string | undefined,
108
+ query: string,
109
+ params?: unknown[],
110
+ ): Promise<unknown[]>;
111
+ /**
112
+ * @deprecated Use `sqlProviderAware()` instead.
103
113
  */
104
114
  sqlWithDirectD1Access(
105
115
  namespace: string,
@@ -136,9 +146,7 @@ export interface FunctionPushProxy {
136
146
  payload: Record<string, unknown>,
137
147
  ): Promise<{ sent: number; failed: number; removed: number }>;
138
148
  /** Get registered device tokens for a user — token values NOT exposed. */
139
- getTokens(
140
- userId: string,
141
- ): Promise<
149
+ getTokens(userId: string): Promise<
142
150
  Array<{
143
151
  deviceId: string;
144
152
  platform: string;
@@ -178,12 +186,30 @@ export interface FunctionPushProxy {
178
186
 
179
187
  /** Storage proxy for App Functions — wraps R2Bucket with convenience methods. */
180
188
  export interface FunctionStorageProxy {
181
- put(key: string, value: ReadableStream | ArrayBuffer | string, options?: { contentType?: string; customMetadata?: Record<string, string> }): Promise<void>;
182
- get(key: string): Promise<{ body: ReadableStream; contentType: string; size: number; customMetadata: Record<string, string> } | null>;
189
+ put(
190
+ key: string,
191
+ value: ReadableStream | ArrayBuffer | string,
192
+ options?: { contentType?: string; customMetadata?: Record<string, string> },
193
+ ): Promise<void>;
194
+ get(key: string): Promise<{
195
+ body: ReadableStream;
196
+ contentType: string;
197
+ size: number;
198
+ customMetadata: Record<string, string>;
199
+ } | null>;
183
200
  delete(key: string): Promise<void>;
184
201
  getSignedUrl(key: string, options?: { expiresIn?: number }): Promise<string>;
185
- list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{ keys: Array<{ key: string; size: number; contentType: string }>; cursor?: string; truncated: boolean }>;
186
- head(key: string): Promise<{ key: string; size: number; contentType: string; customMetadata: Record<string, string> } | null>;
202
+ list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{
203
+ keys: Array<{ key: string; size: number; contentType: string }>;
204
+ cursor?: string;
205
+ truncated: boolean;
206
+ }>;
207
+ head(key: string): Promise<{
208
+ key: string;
209
+ size: number;
210
+ contentType: string;
211
+ customMetadata: Record<string, string>;
212
+ } | null>;
187
213
  }
188
214
 
189
215
  /** KV proxy for App Functions — routes through Worker HTTP. */
@@ -363,13 +389,14 @@ function getRegistryName(key: string, def: FunctionDefinition): string {
363
389
 
364
390
  export function registerFunction(name: string, def: FunctionDefinition): void {
365
391
  if (!def || typeof def !== 'object' || !def.trigger) {
366
- const received = typeof def === 'function'
367
- ? 'a plain function'
368
- : `${typeof def} (${JSON.stringify(def)?.slice(0, 100)})`;
392
+ const received =
393
+ typeof def === 'function'
394
+ ? 'a plain function'
395
+ : `${typeof def} (${JSON.stringify(def)?.slice(0, 100)})`;
369
396
  throw new Error(
370
397
  `registerFunction('${name}'): expected a FunctionDefinition with a 'trigger' property, but received ${received}. ` +
371
- `Functions must use defineFunction() from '@edge-base/shared' and be exported as named HTTP method exports ` +
372
- `(e.g. export const GET = defineFunction(...)). See https://docs.edgebase.dev/functions for details.`,
398
+ `Functions must use defineFunction() from '@edge-base/shared' and be exported as named HTTP method exports ` +
399
+ `(e.g. export const GET = defineFunction(...)). See https://docs.edgebase.dev/functions for details.`,
373
400
  );
374
401
  }
375
402
  functionRegistry.set(buildRegistryKey(name, def), def);
@@ -676,7 +703,12 @@ export function wrapMethodExport(
676
703
  } else if (handler && typeof handler === 'object') {
677
704
  fn = (handler.handler ?? handler) as unknown as (ctx: unknown) => Promise<unknown>;
678
705
  captcha = handler.captcha;
679
- if ('trigger' in handler && handler.trigger && typeof handler.trigger === 'object' && 'path' in handler.trigger) {
706
+ if (
707
+ 'trigger' in handler &&
708
+ handler.trigger &&
709
+ typeof handler.trigger === 'object' &&
710
+ 'path' in handler.trigger
711
+ ) {
680
712
  const triggerPath = handler.trigger.path;
681
713
  path = typeof triggerPath === 'string' ? triggerPath : undefined;
682
714
  }
@@ -707,10 +739,10 @@ interface BuildAdminDbProxyOptions {
707
739
  preferDirectDo?: boolean;
708
740
  }
709
741
 
710
- // ─── Shared SQL executor — D1 direct DO direct → HTTP fallback ───
742
+ // ─── Shared SQL executor — provider-aware direct paths → HTTP fallback ───
711
743
 
712
- export interface SqlWithDirectD1AccessOptions {
713
- env?: unknown;
744
+ export interface SqlProviderAwareOptions {
745
+ env?: Env;
714
746
  config: EdgeBaseConfig;
715
747
  databaseNamespace?: DurableObjectNamespace;
716
748
  workerUrl?: string;
@@ -718,89 +750,49 @@ export interface SqlWithDirectD1AccessOptions {
718
750
  }
719
751
 
720
752
  /**
721
- * Execute raw SQL with the fastest available path:
722
- * 1. D1 direct binding (no network hop)
723
- * 2. Durable Object direct call
724
- * 3. HTTP fallback via workerUrl
753
+ * Execute raw SQL with the fastest provider-aware path:
754
+ * 1. PostgreSQL / Neon direct query path
755
+ * 2. D1 direct binding
756
+ * 3. Durable Object direct call
757
+ * 4. HTTP fallback via workerUrl
725
758
  *
726
- * Shared by buildFunctionContext, auth hooks, and storage hooks.
759
+ * Shared by buildFunctionContext, auth hooks, storage hooks, and plugin migrations.
727
760
  */
728
- export async function executeSqlWithDirectD1Access(
729
- opts: SqlWithDirectD1AccessOptions,
761
+ export async function executeSqlProviderAware(
762
+ opts: SqlProviderAwareOptions,
730
763
  namespace: string,
731
764
  id: string | undefined,
732
765
  query: string,
733
766
  params?: unknown[],
734
767
  ): Promise<unknown[]> {
735
- if (opts.env) {
736
- const dbBlock = opts.config.databases?.[namespace];
737
- const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
738
- if (isDynamicNamespace && !id) {
739
- throw new Error(`admin.sqlWithDirectD1Access() requires an id for dynamic namespace '${namespace}'.`);
740
- }
741
-
742
- if (!id && shouldRouteToD1(namespace, opts.config)) {
743
- const bindingName = getD1BindingName(namespace);
744
- const d1 = (opts.env as Record<string, unknown>)[bindingName] as D1Database | undefined;
745
- if (!d1) {
746
- throw new Error(`D1 binding '${bindingName}' not found.`);
747
- }
748
- try {
749
- const stmt = d1.prepare(query);
750
- const bound = params && params.length > 0 ? stmt.bind(...params) : stmt;
751
- const result = await bound.all();
752
- return (result.results ?? []) as unknown[];
753
- } catch (error) {
754
- const message = error instanceof Error ? error.message : 'SQL execution failed';
755
- throw new Error(message);
756
- }
757
- }
758
-
759
- if (opts.databaseNamespace) {
760
- return executeDoSql({
761
- databaseNamespace: opts.databaseNamespace,
762
- namespace,
763
- id,
764
- query,
765
- params: params ?? [],
766
- internal: true,
767
- });
768
- }
769
- }
770
-
771
- if (opts.workerUrl && opts.serviceKey) {
772
- const res = await fetch(`${opts.workerUrl}/api/sql`, {
773
- method: 'POST',
774
- headers: {
775
- 'Content-Type': 'application/json',
776
- 'X-EdgeBase-Service-Key': opts.serviceKey,
777
- },
778
- body: JSON.stringify({ namespace, id, sql: query, params: params ?? [] }),
779
- });
780
- if (!res.ok) {
781
- const err = (await res.json().catch(() => ({ message: 'SQL execution failed' }))) as { message: string };
782
- throw new Error(err.message);
783
- }
784
- const data = (await res.json()) as { rows?: unknown[]; items?: unknown[]; results?: unknown[] };
785
- if (Array.isArray(data.rows)) return data.rows;
786
- if (Array.isArray(data.items)) return data.items;
787
- if (Array.isArray(data.results)) return data.results;
788
- return [];
789
- }
790
-
791
- throw new Error(
792
- 'admin.sqlWithDirectD1Access() requires env or workerUrl.',
768
+ const result = await executeProviderAwareSql(
769
+ {
770
+ env: opts.env,
771
+ config: opts.config,
772
+ databaseNamespace: opts.databaseNamespace,
773
+ workerUrl: opts.workerUrl,
774
+ serviceKey: opts.serviceKey,
775
+ },
776
+ namespace,
777
+ id,
778
+ query,
779
+ params ?? [],
793
780
  );
781
+ return result.rows;
794
782
  }
795
783
 
784
+ // Backwards-compatible aliases for existing internal callers and public surfaces.
785
+ export const executeSqlWithDirectBindingAccess = executeSqlProviderAware;
786
+ export const executeSqlWithDirectD1Access = executeSqlProviderAware;
787
+
796
788
  /**
797
789
  * Build the admin DB proxy that returns real DbRef/TableRef instances
798
790
  * from @edge-base/core, routed through InternalHttpTransport for
799
791
  * direct D1/PG/DO access (no HTTP round-trip).
800
792
  */
801
793
  export function buildAdminDbProxy(options: BuildAdminDbProxyOptions): FunctionAdminContext['db'] {
802
- // Create HttpClient for sql() tagged template support on TableRef.
803
- // Only available when workerUrl is set (routes through /api/sql endpoint).
794
+ // Create HttpClient fallback for sql() tagged template support on TableRef.
795
+ // Trusted server contexts also receive a direct SQL executor below.
804
796
  let httpClient: HttpClient | undefined;
805
797
  if (options.workerUrl) {
806
798
  httpClient = new HttpClient({
@@ -809,8 +801,28 @@ export function buildAdminDbProxy(options: BuildAdminDbProxyOptions): FunctionAd
809
801
  contextManager: new ContextManager(),
810
802
  });
811
803
  }
804
+ const sqlExecutor = (
805
+ namespace: string,
806
+ id: string | undefined,
807
+ query: string,
808
+ params?: unknown[],
809
+ ) =>
810
+ executeSqlProviderAware(
811
+ {
812
+ env: options.env,
813
+ config: options.config,
814
+ databaseNamespace: options.databaseNamespace,
815
+ workerUrl: options.workerUrl,
816
+ serviceKey: options.serviceKey,
817
+ },
818
+ namespace,
819
+ id,
820
+ query,
821
+ params,
822
+ );
812
823
 
813
824
  return (namespace: string, id?: string): DbRef => {
825
+ const normalizedId = normalizeDbInstanceId(id);
814
826
  // Create a per-DbRef transport with explicit dbContext so that
815
827
  // path parsing is unambiguous even when instanceId === 'tables'.
816
828
  const transport = new InternalHttpTransport({
@@ -821,16 +833,17 @@ export function buildAdminDbProxy(options: BuildAdminDbProxyOptions): FunctionAd
821
833
  env: options.env,
822
834
  executionCtx: options.executionCtx,
823
835
  preferDirectDo: options.preferDirectDo,
824
- dbContext: { namespace, instanceId: id },
836
+ dbContext: { namespace, instanceId: normalizedId },
825
837
  });
826
838
  const dbApi = new DefaultDbApi(transport);
827
839
  return new DbRef(
828
840
  dbApi,
829
841
  namespace,
830
- id,
831
- undefined, // databaseLiveClient — not available server-side
832
- undefined, // filterMatchFn
833
- httpClient, // enables table().sql`...` tagged template
842
+ normalizedId,
843
+ undefined, // databaseLiveClient — not available server-side
844
+ undefined, // filterMatchFn
845
+ httpClient, // enables table().sql`...` tagged template
846
+ sqlExecutor,
834
847
  );
835
848
  };
836
849
  }
@@ -919,8 +932,10 @@ export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthConte
919
932
  // Direct D1 path — works without service key (same as updateUser/deleteUser)
920
933
  if (d1Database) {
921
934
  // Input validation (mirrors routes/admin-auth.ts guards)
922
- if (!data.email || typeof data.email !== 'string') throw new Error('Email and password are required.');
923
- if (!data.password || typeof data.password !== 'string') throw new Error('Email and password are required.');
935
+ if (!data.email || typeof data.email !== 'string')
936
+ throw new Error('Email and password are required.');
937
+ if (!data.password || typeof data.password !== 'string')
938
+ throw new Error('Email and password are required.');
924
939
  const email = data.email.trim().toLowerCase();
925
940
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
926
941
  throw new Error('Invalid email format.');
@@ -1081,6 +1096,25 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
1081
1096
  executionCtx: options.executionCtx,
1082
1097
  preferDirectDo: options.preferDirectDoDb,
1083
1098
  });
1099
+ const sqlProviderAware = (
1100
+ namespace: string,
1101
+ id: string | undefined,
1102
+ query: string,
1103
+ params?: unknown[],
1104
+ ) =>
1105
+ executeSqlProviderAware(
1106
+ {
1107
+ env: options.env,
1108
+ config: options.config,
1109
+ databaseNamespace: options.databaseNamespace,
1110
+ workerUrl: options.workerUrl,
1111
+ serviceKey: options.serviceKey,
1112
+ },
1113
+ namespace,
1114
+ id,
1115
+ query,
1116
+ params,
1117
+ );
1084
1118
 
1085
1119
  // ─── context.admin — AdminEdgeBase-shaped internal proxy ───
1086
1120
  const admin: FunctionAdminContext = {
@@ -1089,18 +1123,9 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
1089
1123
  // ─── context.admin.db(namespace, id) — DB-first tenant access (§5) ───
1090
1124
  db: adminDb,
1091
1125
  auth: adminAuthContext,
1092
- // ─── Direct D1/DO SQL — delegates to shared executor ───
1093
- sqlWithDirectD1Access: (namespace: string, id: string | undefined, query: string, params?: unknown[]) =>
1094
- executeSqlWithDirectD1Access(
1095
- {
1096
- env: options.env,
1097
- config: options.config,
1098
- databaseNamespace: options.databaseNamespace,
1099
- workerUrl: options.workerUrl,
1100
- serviceKey: options.serviceKey,
1101
- },
1102
- namespace, id, query, params,
1103
- ),
1126
+ // ─── Direct provider-aware SQL — delegates to shared executor ───
1127
+ sqlProviderAware,
1128
+ sqlWithDirectD1Access: sqlProviderAware,
1104
1129
  async broadcast(
1105
1130
  channel: string,
1106
1131
  event: string,
@@ -1109,11 +1134,13 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
1109
1134
  if (options.env?.DATABASE_LIVE) {
1110
1135
  const hubId = options.env.DATABASE_LIVE.idFromName('database-live:hub');
1111
1136
  const stub = options.env.DATABASE_LIVE.get(hubId);
1112
- const response = await stub.fetch(new Request('http://do/internal/broadcast', {
1113
- method: 'POST',
1114
- headers: { 'Content-Type': 'application/json' },
1115
- body: JSON.stringify({ channel, event, payload: payload ?? {} }),
1116
- }));
1137
+ const response = await stub.fetch(
1138
+ new Request('http://do/internal/broadcast', {
1139
+ method: 'POST',
1140
+ headers: { 'Content-Type': 'application/json' },
1141
+ body: JSON.stringify({ channel, event, payload: payload ?? {} }),
1142
+ }),
1143
+ );
1117
1144
  if (!response.ok) {
1118
1145
  throw new Error(`client.broadcast() failed: ${response.status}`);
1119
1146
  }
@@ -1161,9 +1188,7 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
1161
1188
  method: 'POST',
1162
1189
  headers: {
1163
1190
  'Content-Type': 'application/json',
1164
- ...(options.serviceKey
1165
- ? { 'X-EdgeBase-Service-Key': options.serviceKey }
1166
- : {}),
1191
+ ...(options.serviceKey ? { 'X-EdgeBase-Service-Key': options.serviceKey } : {}),
1167
1192
  'X-EdgeBase-Call-Depth': String(currentDepth + 1),
1168
1193
  },
1169
1194
  body: JSON.stringify(data ?? {}),
@@ -1221,13 +1246,31 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
1221
1246
 
1222
1247
  // KV / D1 / Vectorize proxies
1223
1248
  kv(namespace: string): FunctionKvProxy {
1224
- return buildFunctionKvProxy(namespace, options.config, options.env, options.workerUrl, options.serviceKey);
1249
+ return buildFunctionKvProxy(
1250
+ namespace,
1251
+ options.config,
1252
+ options.env,
1253
+ options.workerUrl,
1254
+ options.serviceKey,
1255
+ );
1225
1256
  },
1226
1257
  d1(database: string): FunctionD1Proxy {
1227
- return buildFunctionD1Proxy(database, options.config, options.env, options.workerUrl, options.serviceKey);
1258
+ return buildFunctionD1Proxy(
1259
+ database,
1260
+ options.config,
1261
+ options.env,
1262
+ options.workerUrl,
1263
+ options.serviceKey,
1264
+ );
1228
1265
  },
1229
1266
  vector(index: string): FunctionVectorizeProxy {
1230
- return buildFunctionVectorizeProxy(index, options.config, options.env, options.workerUrl, options.serviceKey);
1267
+ return buildFunctionVectorizeProxy(
1268
+ index,
1269
+ options.config,
1270
+ options.env,
1271
+ options.workerUrl,
1272
+ options.serviceKey,
1273
+ );
1231
1274
  },
1232
1275
 
1233
1276
  // Push notification management
@@ -1466,11 +1509,14 @@ export function buildFunctionD1Proxy(
1466
1509
  return {
1467
1510
  async exec<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<T[]> {
1468
1511
  if (config && env) {
1469
- const bindingName = config.d1?.[database]?.binding
1470
- ?? (database === 'auth' ? 'AUTH_DB' : undefined)
1471
- ?? (database === 'control' ? 'CONTROL_DB' : undefined)
1472
- ?? getD1BindingName(database);
1473
- const binding = (env as unknown as Record<string, unknown>)[bindingName] as D1Database | undefined;
1512
+ const bindingName =
1513
+ config.d1?.[database]?.binding ??
1514
+ (database === 'auth' ? 'AUTH_DB' : undefined) ??
1515
+ (database === 'control' ? 'CONTROL_DB' : undefined) ??
1516
+ getD1BindingName(database);
1517
+ const binding = (env as unknown as Record<string, unknown>)[bindingName] as
1518
+ | D1Database
1519
+ | undefined;
1474
1520
  if (!binding) {
1475
1521
  throw new Error(`D1 binding '${bindingName}' not found.`);
1476
1522
  }
@@ -1522,7 +1568,7 @@ export function buildFunctionVectorizeProxy(
1522
1568
  if (values instanceof Float32Array || values instanceof Float64Array) {
1523
1569
  return Array.from(values);
1524
1570
  }
1525
- return Array.isArray(values) ? values as number[] : undefined;
1571
+ return Array.isArray(values) ? (values as number[]) : undefined;
1526
1572
  };
1527
1573
 
1528
1574
  const mapMatches = (
@@ -1533,15 +1579,19 @@ export function buildFunctionVectorizeProxy(
1533
1579
  metadata?: Record<string, unknown>;
1534
1580
  namespace?: string;
1535
1581
  }>,
1536
- ) => matches.map((match) => ({
1537
- id: match.id,
1538
- score: match.score,
1539
- ...(match.values !== undefined ? { values: normalizeValues(match.values) } : {}),
1540
- ...(match.metadata !== undefined ? { metadata: match.metadata } : {}),
1541
- ...(match.namespace ? { namespace: match.namespace } : {}),
1542
- }));
1543
-
1544
- const withNamespace = <T extends { namespace?: string }>(vectors: T[], namespace?: string): T[] => {
1582
+ ) =>
1583
+ matches.map((match) => ({
1584
+ id: match.id,
1585
+ score: match.score,
1586
+ ...(match.values !== undefined ? { values: normalizeValues(match.values) } : {}),
1587
+ ...(match.metadata !== undefined ? { metadata: match.metadata } : {}),
1588
+ ...(match.namespace ? { namespace: match.namespace } : {}),
1589
+ }));
1590
+
1591
+ const withNamespace = <T extends { namespace?: string }>(
1592
+ vectors: T[],
1593
+ namespace?: string,
1594
+ ): T[] => {
1545
1595
  if (!namespace) return vectors;
1546
1596
  return vectors.map((vector) => (vector.namespace ? vector : { ...vector, namespace }));
1547
1597
  };
@@ -1559,7 +1609,10 @@ export function buildFunctionVectorizeProxy(
1559
1609
  if (directBinding) {
1560
1610
  switch (body.action) {
1561
1611
  case 'upsert': {
1562
- const vectors = withNamespace(body.vectors as VectorizeVector[], body.namespace as string | undefined);
1612
+ const vectors = withNamespace(
1613
+ body.vectors as VectorizeVector[],
1614
+ body.namespace as string | undefined,
1615
+ );
1563
1616
  let count = 0;
1564
1617
  let mutationId: string | undefined;
1565
1618
  for (const chunk of chunkArray(vectors, VECTOR_BATCH_LIMIT)) {
@@ -1572,7 +1625,10 @@ export function buildFunctionVectorizeProxy(
1572
1625
  return { count, ...(mutationId ? { mutationId } : {}) };
1573
1626
  }
1574
1627
  case 'insert': {
1575
- const vectors = withNamespace(body.vectors as VectorizeVector[], body.namespace as string | undefined);
1628
+ const vectors = withNamespace(
1629
+ body.vectors as VectorizeVector[],
1630
+ body.namespace as string | undefined,
1631
+ );
1576
1632
  let count = 0;
1577
1633
  let mutationId: string | undefined;
1578
1634
  for (const chunk of chunkArray(vectors, VECTOR_BATCH_LIMIT)) {
@@ -1595,9 +1651,11 @@ export function buildFunctionVectorizeProxy(
1595
1651
  return { matches: mapMatches(result.matches), count: result.count };
1596
1652
  }
1597
1653
  case 'queryById': {
1598
- const queryById = (directBinding as unknown as {
1599
- queryById?: (id: string, opts?: VectorizeQueryOptions) => Promise<VectorizeMatches>;
1600
- }).queryById;
1654
+ const queryById = (
1655
+ directBinding as unknown as {
1656
+ queryById?: (id: string, opts?: VectorizeQueryOptions) => Promise<VectorizeMatches>;
1657
+ }
1658
+ ).queryById;
1601
1659
  if (typeof queryById !== 'function') {
1602
1660
  throw new Error('queryById is not available on this Vectorize binding');
1603
1661
  }
@@ -1612,7 +1670,11 @@ export function buildFunctionVectorizeProxy(
1612
1670
  }
1613
1671
  case 'getByIds': {
1614
1672
  const vectors = (
1615
- await Promise.all(chunkArray(body.ids as string[], VECTOR_BATCH_LIMIT).map((chunk) => directBinding.getByIds(chunk)))
1673
+ await Promise.all(
1674
+ chunkArray(body.ids as string[], VECTOR_BATCH_LIMIT).map((chunk) =>
1675
+ directBinding.getByIds(chunk),
1676
+ ),
1677
+ )
1616
1678
  ).flat();
1617
1679
  return {
1618
1680
  vectors: vectors.map((vector) => ({
@@ -1640,12 +1702,22 @@ export function buildFunctionVectorizeProxy(
1640
1702
  const details = info as unknown as Record<string, unknown>;
1641
1703
  return {
1642
1704
  vectorCount: details.vectorCount ?? details.vectorsCount ?? 0,
1643
- dimensions: details.dimensions ?? (details.config as Record<string, unknown> | undefined)?.dimensions ?? 0,
1644
- metric: details.metric ?? (details.config as Record<string, unknown> | undefined)?.metric ?? 'cosine',
1705
+ dimensions:
1706
+ details.dimensions ??
1707
+ (details.config as Record<string, unknown> | undefined)?.dimensions ??
1708
+ 0,
1709
+ metric:
1710
+ details.metric ??
1711
+ (details.config as Record<string, unknown> | undefined)?.metric ??
1712
+ 'cosine',
1645
1713
  ...('id' in details ? { id: details.id } : {}),
1646
1714
  ...('name' in details ? { name: details.name } : {}),
1647
- ...('processedUpToDatetime' in details ? { processedUpToDatetime: details.processedUpToDatetime } : {}),
1648
- ...('processedUpToMutation' in details ? { processedUpToMutation: details.processedUpToMutation } : {}),
1715
+ ...('processedUpToDatetime' in details
1716
+ ? { processedUpToDatetime: details.processedUpToDatetime }
1717
+ : {}),
1718
+ ...('processedUpToMutation' in details
1719
+ ? { processedUpToMutation: details.processedUpToMutation }
1720
+ : {}),
1649
1721
  };
1650
1722
  }
1651
1723
  }
@@ -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(