@edge-base/server 0.2.1 → 0.2.3

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 (81) hide show
  1. package/admin-build/_app/immutable/chunks/{DjOEv9M9.js → A_3UuvCe.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{Dqk2TGNU.js → B-_-hJ9o.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{BFs_qStz.js → B5Nwfelm.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{B0QyxC2M.js → BxoNtYHK.js} +3 -3
  5. package/admin-build/_app/immutable/chunks/{BsFiK_FJ.js → CZ0TVkCa.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{k0CIJkw4.js → CzSAxmuj.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{D-x55wdW.js → DCKcAiQH.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{CSGrwS7E.js → DCvwWZrm.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/{BTJcQFEp.js → DRqPU3wD.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/{CqUxCvs_.js → Dc1-6Po6.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{D755Tqat.js → DiyBpamp.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{BcIUK2sk.js → Dlty5069.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{BY07qVPA.js → DpVAayDG.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{BCKr7yKd.js → Du5vWVa2.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{m9QZTyVV.js → byv2rTy8.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{DnLqc9L1.js → nZvorU8i.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.BTsq3_xq.js → app.CfrmEXPD.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.l1WvHznQ.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.BZ00WDYH.js → 0.Cn2BZ4da.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.RzSJ3yyr.js → 1.Dv4LX_Co.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.D-rsiquF.js → 10.DPVv3kat.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.l7-bgtFD.js → 11.CiCb6Ayu.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.Dkq0H7B5.js → 12.CIPyeekF.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.DtK_4oRz.js → 13.Z15Lt36e.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.BKo7-AMx.js → 14.s0l5bAq3.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.CQAj_6lq.js → 15.UwSSNO76.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.XVIG-Ffr.js → 16.qiD8i883.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.g6raZLCM.js → 17.Dy3dcSvu.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.IQz6a3T6.js → 18.DeXyPYsO.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.CAAZ8i8h.js → 19.CAbuyS6w.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.BPcX3KPj.js → 20.Bec0T7un.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.DuDYelMY.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.Br5AG_5Z.js → 22.CdVprrv2.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.KjbrdXoE.js → 23.Y8RzVLoF.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.C3n2-hgw.js → 24.CWhHYFBx.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.SFDSBzHd.js → 25.wCBplOVt.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.D95vui6E.js → 26.Cod_JRFK.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.FgLgdjwB.js → 27.BO2HVMu9.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.B9sYYm1F.js → 28.DxG-FBVQ.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.DyqZ_wbN.js → 29.CjGqWGvE.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.Bzo2yVIO.js → 3.By3_OmdZ.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.c1CiNwiS.js → 30.M_H7Htpq.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.CXty66Vh.js → 31.DEU18izM.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.BgQaXZ27.js → 4.DeYhKtzJ.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.BuJrHvxH.js → 5.9WLgxhrD.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.CkBBC94k.js → 6.BdT2i_dd.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.D2YBvNFM.js → 7.CHq0s4K6.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.D8qQWo_z.js → 8.DuvRw-XZ.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.BLDLX5hV.js → 9.C2Ub82wn.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 -2
  53. package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
  54. package/src/__tests__/database-live-do.test.ts +50 -0
  55. package/src/__tests__/database-live-emitter.test.ts +116 -1
  56. package/src/__tests__/error-format.test.ts +63 -0
  57. package/src/__tests__/functions-context.test.ts +592 -35
  58. package/src/__tests__/meta-export-coverage.test.ts +1 -0
  59. package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
  60. package/src/__tests__/provider-aware-sql.test.ts +157 -0
  61. package/src/__tests__/room-auth-state-loss.test.ts +124 -0
  62. package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
  63. package/src/__tests__/sql-route.test.ts +187 -76
  64. package/src/durable-objects/database-live-do.ts +46 -1
  65. package/src/durable-objects/room-runtime-base.ts +26 -2
  66. package/src/durable-objects/rooms-do.ts +1 -1
  67. package/src/lib/admin-db-target.ts +30 -74
  68. package/src/lib/d1-handler.ts +45 -14
  69. package/src/lib/database-live-emitter.ts +57 -16
  70. package/src/lib/functions.ts +332 -454
  71. package/src/lib/internal-transport.ts +316 -0
  72. package/src/lib/plugin-migrations.ts +39 -39
  73. package/src/lib/postgres-handler.ts +39 -11
  74. package/src/lib/provider-aware-sql.ts +827 -0
  75. package/src/routes/admin.ts +7 -1
  76. package/src/routes/auth.ts +11 -12
  77. package/src/routes/sql.ts +51 -76
  78. package/src/routes/storage.ts +11 -12
  79. package/src/types.ts +2 -0
  80. package/admin-build/_app/immutable/entry/start.zXCirpgY.js +0 -1
  81. package/admin-build/_app/immutable/nodes/21.DoPabrY_.js +0 -1
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Internal HttpTransport implementation for server-side function context.
3
+ *
4
+ * Replaces the old TableProxy by implementing the same HttpTransport interface
5
+ * that @edge-base/core's DefaultDbApi expects. This lets us use the real
6
+ * TableRef/DbRef classes from the core SDK while routing requests directly
7
+ * to D1, PostgreSQL, or DurableObject handlers — no HTTP round-trip.
8
+ */
9
+ import type { HttpTransport } from '@edge-base/core';
10
+ import type { EdgeBaseConfig } from '@edge-base/shared';
11
+ import { getDbDoName, callDO, shouldRouteToD1 } from './do-router.js';
12
+ import { handleD1Request } from './d1-handler.js';
13
+ import { handlePgRequest } from './postgres-handler.js';
14
+ import { buildInternalHandlerContext } from './internal-request.js';
15
+ import type { Env } from '../types.js';
16
+
17
+ export interface InternalTransportOptions {
18
+ databaseNamespace: DurableObjectNamespace;
19
+ config: EdgeBaseConfig;
20
+ workerUrl?: string;
21
+ serviceKey?: string;
22
+ env?: Env;
23
+ executionCtx?: ExecutionContext;
24
+ preferDirectDo?: boolean;
25
+ /**
26
+ * When set, the transport knows this DbRef targets a specific
27
+ * namespace + optional instanceId. This avoids ambiguous path parsing
28
+ * when instanceId happens to be "tables".
29
+ */
30
+ dbContext?: { namespace: string; instanceId?: string };
31
+ }
32
+
33
+ /**
34
+ * Parse a DefaultDbApi path into routing components.
35
+ *
36
+ * Paths follow two patterns:
37
+ * /api/db/{namespace}/tables/{table}[/rest] → static DB
38
+ * /api/db/{namespace}/{instanceId}/tables/{table}[/rest] → dynamic DB
39
+ *
40
+ * Returns namespace, optional instanceId, tableName, and directPath
41
+ * (everything from /tables/... onward, which D1/PG handlers expect).
42
+ */
43
+ /**
44
+ * Parse a DefaultDbApi path, optionally guided by known dbContext.
45
+ *
46
+ * When dbContext is provided (recommended), we know whether this is a
47
+ * static or dynamic DB, so we can unambiguously find the 'tables' keyword
48
+ * even when instanceId === 'tables'.
49
+ *
50
+ * Without dbContext, falls back to heuristic: first 'tables' at index 1
51
+ * means static, at index 2 means dynamic.
52
+ */
53
+ function parsePath(
54
+ path: string,
55
+ dbContext?: { namespace: string; instanceId?: string },
56
+ ): {
57
+ namespace: string;
58
+ instanceId?: string;
59
+ tableName: string;
60
+ directPath: string;
61
+ } {
62
+ // Strip leading /api/db/
63
+ const stripped = path.replace(/^\/api\/db\//, '');
64
+ const segments = stripped.split('/');
65
+
66
+ let tablesIdx: number;
67
+ if (dbContext) {
68
+ // We know the shape: static has 'tables' at index 1, dynamic at index 2
69
+ tablesIdx = dbContext.instanceId ? 2 : 1;
70
+ } else {
71
+ // Heuristic fallback: find first 'tables' keyword
72
+ tablesIdx = segments.indexOf('tables', 1);
73
+ }
74
+ if (tablesIdx < 0 || segments[tablesIdx] !== 'tables') {
75
+ throw new Error(`Invalid DB path: missing 'tables' segment in ${path}`);
76
+ }
77
+
78
+ const namespace = segments[0];
79
+ const instanceId = tablesIdx === 2 ? segments[1] : undefined;
80
+ const rawTableName = segments[tablesIdx + 1];
81
+ // Decode URL-encoded table names (e.g. 'plugin-a%2Fevents' → 'plugin-a/events')
82
+ // Use decoded name consistently in both tableName and directPath so that
83
+ // handler suffix parsing (doPath.replace(`/tables/${tableName}`, '')) matches.
84
+ const tableName = decodeURIComponent(rawTableName);
85
+ const rest = segments.slice(tablesIdx + 2);
86
+ const directPath = `/tables/${tableName}${rest.length ? '/' + rest.join('/') : ''}`;
87
+
88
+ return { namespace, instanceId, tableName, directPath };
89
+ }
90
+
91
+ export class InternalHttpTransport implements HttpTransport {
92
+ private readonly databaseNamespace: DurableObjectNamespace;
93
+ private readonly config: EdgeBaseConfig;
94
+ private readonly workerUrl?: string;
95
+ private readonly serviceKey?: string;
96
+ private readonly env?: Env;
97
+ private readonly executionCtx?: ExecutionContext;
98
+ private readonly preferDirectDo: boolean;
99
+ private readonly dbContext?: { namespace: string; instanceId?: string };
100
+
101
+ constructor(options: InternalTransportOptions) {
102
+ this.databaseNamespace = options.databaseNamespace;
103
+ this.config = options.config;
104
+ this.workerUrl = options.workerUrl;
105
+ this.serviceKey = options.serviceKey;
106
+ this.env = options.env;
107
+ this.executionCtx = options.executionCtx;
108
+ this.preferDirectDo = options.preferDirectDo ?? false;
109
+ this.dbContext = options.dbContext;
110
+ }
111
+
112
+ async request<T>(
113
+ method: string,
114
+ path: string,
115
+ options?: { query?: Record<string, string>; body?: unknown },
116
+ ): Promise<T> {
117
+ const { namespace, instanceId, tableName, directPath } = parsePath(path, this.dbContext);
118
+ const doName = getDbDoName(namespace, instanceId);
119
+
120
+ // Build internal headers
121
+ const headers: Record<string, string> = {
122
+ 'Content-Type': 'application/json',
123
+ 'X-DO-Name': doName,
124
+ 'X-EdgeBase-Internal': 'true',
125
+ };
126
+ if (this.serviceKey) {
127
+ headers['X-EdgeBase-Service-Key'] = this.serviceKey;
128
+ }
129
+
130
+ // Convert query Record to URLSearchParams
131
+ const query = new URLSearchParams();
132
+ if (options?.query) {
133
+ for (const [k, v] of Object.entries(options.query)) {
134
+ if (v !== undefined && v !== '') query.set(k, v);
135
+ }
136
+ }
137
+
138
+ const body = options?.body as Record<string, unknown> | undefined;
139
+
140
+ // Route to the appropriate handler
141
+ const res = await this.routeRequest(
142
+ method as 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT',
143
+ namespace,
144
+ instanceId,
145
+ tableName,
146
+ directPath,
147
+ doName,
148
+ headers,
149
+ query,
150
+ body,
151
+ );
152
+
153
+ if (!res.ok) {
154
+ const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
155
+ throw new Error(String(err.message || `Internal request failed: ${res.status}`));
156
+ }
157
+
158
+ return (await res.json()) as T;
159
+ }
160
+
161
+ async head(_path: string): Promise<boolean> {
162
+ // HEAD is only used by StorageClient.checkFileExists — not relevant for DB ops
163
+ return false;
164
+ }
165
+
166
+ private async routeRequest(
167
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT',
168
+ namespace: string,
169
+ instanceId: string | undefined,
170
+ tableName: string,
171
+ directPath: string,
172
+ doName: string,
173
+ headers: Record<string, string>,
174
+ query: URLSearchParams,
175
+ body?: Record<string, unknown>,
176
+ ): Promise<Response> {
177
+ const queryString = Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
178
+ const directPathWithQuery = `${directPath}${queryString}`;
179
+ const provider = this.config.databases?.[namespace]?.provider;
180
+ const httpMethod = method === 'PUT' ? 'PATCH' : method; // normalize PUT → PATCH
181
+
182
+ // 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);
185
+ }
186
+
187
+ // 2. PostgreSQL route
188
+ if ((provider === 'neon' || provider === 'postgres') && this.env) {
189
+ return this.requestViaPgHandler(httpMethod, namespace, instanceId, tableName, directPath, headers, query, body);
190
+ }
191
+
192
+ // 3. Direct DO route
193
+ if (this.env) {
194
+ return this.requestViaDirectDo(httpMethod, doName, directPathWithQuery, headers, body, !!instanceId);
195
+ }
196
+
197
+ // 4. Worker HTTP fallback
198
+ if (this.workerUrl) {
199
+ const apiPath = instanceId
200
+ ? `/api/db/${namespace}/${instanceId}${directPathWithQuery}`
201
+ : `/api/db/${namespace}${directPathWithQuery}`;
202
+ return this.requestViaWorker(httpMethod, apiPath, headers, body);
203
+ }
204
+
205
+ // 5. Fallback: direct DO
206
+ return this.requestViaDirectDo(httpMethod, doName, directPathWithQuery, headers, body, !!instanceId);
207
+ }
208
+
209
+ private async requestViaWorker(
210
+ method: string,
211
+ path: string,
212
+ headers: Record<string, string>,
213
+ body?: Record<string, unknown>,
214
+ ): Promise<Response> {
215
+ return fetch(`${this.workerUrl}${path}`, {
216
+ method,
217
+ headers,
218
+ body: body === undefined || method === 'GET' || method === 'DELETE'
219
+ ? undefined
220
+ : JSON.stringify(body),
221
+ });
222
+ }
223
+
224
+ private async requestViaD1Handler(
225
+ method: string,
226
+ namespace: string,
227
+ instanceId: string | undefined,
228
+ tableName: string,
229
+ directPath: string,
230
+ headers: Record<string, string>,
231
+ query: URLSearchParams,
232
+ body?: Record<string, unknown>,
233
+ ): Promise<Response> {
234
+ if (!this.env) throw new Error('D1 table proxy requires env.');
235
+
236
+ const queryString = Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
237
+ const url = `http://internal/api/db/${namespace}${instanceId ? `/${instanceId}` : ''}${directPath}${queryString}`;
238
+
239
+ const request = new Request(url, {
240
+ method,
241
+ headers,
242
+ body: body === undefined || method === 'GET' || method === 'DELETE'
243
+ ? undefined
244
+ : JSON.stringify(body),
245
+ });
246
+
247
+ return handleD1Request(
248
+ buildInternalHandlerContext({ env: this.env, request, body, executionCtx: this.executionCtx }),
249
+ namespace,
250
+ tableName,
251
+ directPath,
252
+ );
253
+ }
254
+
255
+ private async requestViaPgHandler(
256
+ method: string,
257
+ namespace: string,
258
+ instanceId: string | undefined,
259
+ tableName: string,
260
+ directPath: string,
261
+ headers: Record<string, string>,
262
+ query: URLSearchParams,
263
+ body?: Record<string, unknown>,
264
+ ): Promise<Response> {
265
+ if (!this.env) throw new Error('PostgreSQL table proxy requires env.');
266
+
267
+ const queryString = Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
268
+ const url = `http://internal/api/db/${namespace}${instanceId ? `/${instanceId}` : ''}${directPath}${queryString}`;
269
+
270
+ const request = new Request(url, {
271
+ method,
272
+ headers,
273
+ body: body === undefined || method === 'GET' || method === 'DELETE'
274
+ ? undefined
275
+ : JSON.stringify(body),
276
+ });
277
+
278
+ return handlePgRequest(
279
+ buildInternalHandlerContext({ env: this.env, request, body, executionCtx: this.executionCtx }),
280
+ namespace,
281
+ tableName,
282
+ directPath,
283
+ );
284
+ }
285
+
286
+ private async requestViaDirectDo(
287
+ method: string,
288
+ doName: string,
289
+ directPathWithQuery: string,
290
+ headers: Record<string, string>,
291
+ body?: Record<string, unknown>,
292
+ isDynamic?: boolean,
293
+ ): Promise<Response> {
294
+ const res = await callDO(this.databaseNamespace, doName, directPathWithQuery, {
295
+ method,
296
+ body,
297
+ headers,
298
+ });
299
+
300
+ // Handle dynamic DO creation
301
+ if (isDynamic && res.status === 201) {
302
+ const createPayload = await res.clone().json().catch(() => null) as
303
+ | { needsCreate?: boolean }
304
+ | null;
305
+ if (createPayload?.needsCreate) {
306
+ return callDO(this.databaseNamespace, doName, directPathWithQuery, {
307
+ method,
308
+ body,
309
+ headers: { ...headers, 'X-DO-Create-Authorized': '1' },
310
+ });
311
+ }
312
+ }
313
+
314
+ return res;
315
+ }
316
+ }
@@ -33,7 +33,7 @@ import {
33
33
  buildFunctionPushProxy,
34
34
  buildAdminAuthContext,
35
35
  } from './functions.js';
36
- import { executeDoSql } from './do-sql.js';
36
+ import { executeProviderAwareSql } from './provider-aware-sql.js';
37
37
  import { resolveRootServiceKey } from './service-key.js';
38
38
 
39
39
  /**
@@ -118,9 +118,9 @@ function arePluginMigrationsCurrentInMemory(plugins: PluginInstance[]): boolean
118
118
  return false;
119
119
  }
120
120
 
121
- return versionedPlugins.every((plugin) => (
122
- currentVersionedPlugins.get(plugin.name) === plugin.version
123
- ));
121
+ return versionedPlugins.every(
122
+ (plugin) => currentVersionedPlugins.get(plugin.name) === plugin.version,
123
+ );
124
124
  }
125
125
 
126
126
  function markPluginsCurrent(plugins: PluginInstance[]): void {
@@ -217,7 +217,8 @@ async function runMigrationsWithTimeout(
217
217
  }
218
218
 
219
219
  function resolvePluginMigrationTimeoutMs(): number {
220
- const raw = typeof process !== 'undefined' ? process.env.EDGEBASE_PLUGIN_MIGRATIONS_TIMEOUT_MS : undefined;
220
+ const raw =
221
+ typeof process !== 'undefined' ? process.env.EDGEBASE_PLUGIN_MIGRATIONS_TIMEOUT_MS : undefined;
221
222
  const parsed = Number(raw);
222
223
  if (Number.isFinite(parsed) && parsed > 0) {
223
224
  return parsed;
@@ -250,9 +251,9 @@ async function arePluginMigrationsCurrent(
250
251
  );
251
252
  const versions = new Map(rows.map((row) => [row.key, row.value]));
252
253
 
253
- return versionedPlugins.every((plugin) => (
254
- versions.get(`plugin_version:${plugin.name}`) === plugin.version
255
- ));
254
+ return versionedPlugins.every(
255
+ (plugin) => versions.get(`plugin_version:${plugin.name}`) === plugin.version,
256
+ );
256
257
  }
257
258
 
258
259
  // ─── Helpers ───
@@ -334,6 +335,28 @@ function buildMigrationAdminContext(
334
335
  return directUrl ?? null;
335
336
  }
336
337
 
338
+ const sqlProviderAware = async (
339
+ namespace: string,
340
+ id: string | undefined,
341
+ query: string,
342
+ params?: unknown[],
343
+ ): Promise<unknown[]> => {
344
+ const result = await executeProviderAwareSql(
345
+ {
346
+ env,
347
+ config,
348
+ databaseNamespace: dbNamespace,
349
+ workerUrl,
350
+ serviceKey,
351
+ },
352
+ namespace,
353
+ id,
354
+ query,
355
+ params ?? [],
356
+ );
357
+ return result.rows as unknown[];
358
+ };
359
+
337
360
  return {
338
361
  db(namespace: string, id?: string) {
339
362
  const pgConnStr = resolvePgConnString(namespace);
@@ -350,40 +373,14 @@ function buildMigrationAdminContext(
350
373
  return doAdminDb(namespace, id);
351
374
  },
352
375
 
353
- async sql(
376
+ sqlProviderAware,
377
+ async sqlWithDirectD1Access(
354
378
  namespace: string,
355
379
  id: string | undefined,
356
380
  query: string,
357
381
  params?: unknown[],
358
382
  ): Promise<unknown[]> {
359
- const dbBlock = config.databases?.[namespace];
360
- const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
361
- if (isDynamicNamespace && !id) {
362
- throw new Error(`admin.sql() requires an id for dynamic namespace '${namespace}'.`);
363
- }
364
-
365
- const pgConnStr = resolvePgConnString(namespace);
366
-
367
- // ─── PostgreSQL path ───
368
- if (pgConnStr) {
369
- // Ensure schema is initialized before raw SQL
370
- const dbBlock = config.databases?.[namespace];
371
- if (dbBlock?.tables) {
372
- await ensurePgSchema(pgConnStr, namespace, dbBlock.tables);
373
- }
374
- const result = await executePostgresQuery(pgConnStr, query, params ?? []);
375
- return result.rows as unknown[];
376
- }
377
-
378
- // ─── DO path (existing) ───
379
- return executeDoSql({
380
- databaseNamespace: dbNamespace,
381
- namespace,
382
- id,
383
- query,
384
- params: params ?? [],
385
- internal: true,
386
- });
383
+ return sqlProviderAware(namespace, id, query, params);
387
384
  },
388
385
 
389
386
  // ─── Convenience shortcut: table(name) → db('shared').table(name) ───
@@ -523,7 +520,8 @@ function buildPgTableOps(
523
520
  const setClauses = (updatableCols.length > 0 ? updatableCols : [conflictTarget]).map(
524
521
  (col) => `${escapePgIdentifier(col)} = EXCLUDED.${escapePgIdentifier(col)}`,
525
522
  );
526
- const sql = `INSERT INTO ${escapePgIdentifier(tableName)} (${cols.map(escapePgIdentifier).join(', ')}) VALUES (${placeholders.join(', ')})` +
523
+ const sql =
524
+ `INSERT INTO ${escapePgIdentifier(tableName)} (${cols.map(escapePgIdentifier).join(', ')}) VALUES (${placeholders.join(', ')})` +
527
525
  ` ON CONFLICT (${escapePgIdentifier(conflictTarget)}) DO UPDATE SET ${setClauses.join(', ')} RETURNING *`;
528
526
  const result = await executePostgresQuery(connectionString, sql, vals);
529
527
  return result.rows[0]
@@ -596,7 +594,9 @@ function buildPgTableOps(
596
594
  }
597
595
 
598
596
  const result = await executePostgresQuery(connectionString, sql, params);
599
- return { items: result.rows.map((row) => stripInternalPgFields(row as Record<string, unknown>)) };
597
+ return {
598
+ items: result.rows.map((row) => stripInternalPgFields(row as Record<string, unknown>)),
599
+ };
600
600
  },
601
601
  };
602
602
  }
@@ -298,6 +298,18 @@ function buildHookCtx(
298
298
  };
299
299
  }
300
300
 
301
+ function scheduleDbLive(
302
+ executionCtx: ExecutionContext,
303
+ promise: Promise<void>,
304
+ context: string,
305
+ ): void {
306
+ executionCtx.waitUntil(
307
+ promise.catch((error) => {
308
+ console.warn(`[db-live] ${context} failed`, error);
309
+ }),
310
+ );
311
+ }
312
+
301
313
  function toFieldErrorData(
302
314
  errors: Record<string, string>,
303
315
  ): Record<string, { code: string; message: string }> {
@@ -605,7 +617,8 @@ async function handleInsert(
605
617
  }
606
618
 
607
619
  // Emit database-live event (fire-and-forget)
608
- c.executionCtx.waitUntil(
620
+ scheduleDbLive(
621
+ c.executionCtx,
609
622
  emitDbLiveEvent(
610
623
  c.env,
611
624
  resolved.namespace,
@@ -614,6 +627,7 @@ async function handleInsert(
614
627
  String(inserted.id ?? ''),
615
628
  inserted,
616
629
  ),
630
+ `emit ${isUpsert && isUpdate ? 'modified' : 'added'} ${resolved.namespace}.${tableName}`,
617
631
  );
618
632
  c.executionCtx.waitUntil(
619
633
  executeDbTriggers(
@@ -733,8 +747,10 @@ async function handleUpdate(
733
747
  }
734
748
 
735
749
  // Emit database-live event (fire-and-forget)
736
- c.executionCtx.waitUntil(
750
+ scheduleDbLive(
751
+ c.executionCtx,
737
752
  emitDbLiveEvent(c.env, resolved.namespace, tableName, 'modified', id, updated),
753
+ `emit modified ${resolved.namespace}.${tableName}:${id}`,
738
754
  );
739
755
  c.executionCtx.waitUntil(
740
756
  executeDbTriggers(
@@ -817,8 +833,10 @@ async function handleDelete(
817
833
  }
818
834
 
819
835
  // Emit database-live event (fire-and-forget)
820
- c.executionCtx.waitUntil(
836
+ scheduleDbLive(
837
+ c.executionCtx,
821
838
  emitDbLiveEvent(c.env, resolved.namespace, tableName, 'removed', id, stripInternalPgFields(existingRow)),
839
+ `emit removed ${resolved.namespace}.${tableName}:${id}`,
822
840
  );
823
841
  c.executionCtx.waitUntil(
824
842
  executeDbTriggers(
@@ -944,15 +962,21 @@ async function handleBatch(
944
962
  data: r as Record<string, unknown>,
945
963
  }));
946
964
  if (changes.length >= 10) {
947
- c.executionCtx.waitUntil(
965
+ scheduleDbLive(
966
+ c.executionCtx,
948
967
  emitDbLiveBatchEvent(c.env, resolved.namespace, tableName, changes),
968
+ `emit batch ${resolved.namespace}.${tableName} (${changes.length} changes)`,
949
969
  );
950
970
  } else {
951
- for (const ch of changes) {
952
- c.executionCtx.waitUntil(
953
- emitDbLiveEvent(c.env, resolved.namespace, tableName, ch.type, ch.docId, ch.data),
954
- );
955
- }
971
+ scheduleDbLive(
972
+ c.executionCtx,
973
+ Promise.all(
974
+ changes.map((ch) =>
975
+ emitDbLiveEvent(c.env, resolved.namespace, tableName, ch.type, ch.docId, ch.data),
976
+ ),
977
+ ).then(() => undefined),
978
+ `emit fan-out ${resolved.namespace}.${tableName} (${changes.length} changes)`,
979
+ );
956
980
  }
957
981
  }
958
982
 
@@ -1031,8 +1055,10 @@ async function handleBatchByFilter(
1031
1055
  succeeded = result.rowCount;
1032
1056
 
1033
1057
  if (succeeded > 0) {
1034
- c.executionCtx.waitUntil(
1058
+ scheduleDbLive(
1059
+ c.executionCtx,
1035
1060
  emitDbLiveEvent(c.env, resolved.namespace, tableName, 'removed', '_bulk', { action: 'delete', count: succeeded }),
1061
+ `emit bulk ${resolved.namespace}.${tableName} (delete)`,
1036
1062
  );
1037
1063
  }
1038
1064
 
@@ -1074,8 +1100,10 @@ async function handleBatchByFilter(
1074
1100
  succeeded = result.rowCount;
1075
1101
 
1076
1102
  if (succeeded > 0) {
1077
- c.executionCtx.waitUntil(
1103
+ scheduleDbLive(
1104
+ c.executionCtx,
1078
1105
  emitDbLiveEvent(c.env, resolved.namespace, tableName, 'modified', '_bulk', { action: 'update', count: succeeded }),
1106
+ `emit bulk ${resolved.namespace}.${tableName} (update)`,
1079
1107
  );
1080
1108
  }
1081
1109