@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
@@ -1,6 +1,10 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
  import { buildFunctionContext, getWorkerUrl } from '../lib/functions.js';
3
3
 
4
+ function makeTaggedTemplateStrings(parts: string[]): TemplateStringsArray {
5
+ return Object.assign([...parts], { raw: [...parts] }) as unknown as TemplateStringsArray;
6
+ }
7
+
4
8
  describe('buildFunctionContext admin.db', () => {
5
9
  afterEach(() => {
6
10
  vi.unstubAllGlobals();
@@ -48,6 +52,48 @@ describe('buildFunctionContext admin.db', () => {
48
52
  );
49
53
  });
50
54
 
55
+ it('preserves significant whitespace in dynamic admin.db instance ids', async () => {
56
+ const databaseFetch = vi.fn().mockResolvedValue(
57
+ new Response(JSON.stringify({ items: [{ id: 'm1' }] }), {
58
+ status: 200,
59
+ headers: { 'Content-Type': 'application/json' },
60
+ }),
61
+ );
62
+ const databaseNamespace = {
63
+ idFromName: vi.fn(() => 'workspace-id'),
64
+ get: vi.fn(() => ({ fetch: databaseFetch })),
65
+ } as unknown as DurableObjectNamespace;
66
+
67
+ const ctx = buildFunctionContext({
68
+ request: new Request('http://localhost/api/functions/feed-summary'),
69
+ auth: null,
70
+ databaseNamespace,
71
+ authNamespace: {} as DurableObjectNamespace,
72
+ d1Database: {} as D1Database,
73
+ config: {
74
+ databases: {
75
+ workspace: {
76
+ instance: true,
77
+ tables: {
78
+ members: { schema: { role: { type: 'string' } } },
79
+ },
80
+ },
81
+ },
82
+ },
83
+ });
84
+
85
+ await ctx.admin.db('workspace', ' ws-1 ').table('members').getList();
86
+
87
+ expect(databaseNamespace.idFromName).toHaveBeenCalledWith('workspace: ws-1 ');
88
+ expect(databaseFetch.mock.calls[0]?.[1]).toEqual(
89
+ expect.objectContaining({
90
+ headers: expect.objectContaining({
91
+ 'X-DO-Name': 'workspace: ws-1 ',
92
+ }),
93
+ }),
94
+ );
95
+ });
96
+
51
97
  it('routes upsert calls through the worker with upsert query params', async () => {
52
98
  const fetchMock = vi.fn().mockResolvedValue(
53
99
  new Response(JSON.stringify({ id: 'p1', title: 'Upserted', action: 'inserted' }), {
@@ -97,16 +143,19 @@ describe('buildFunctionContext admin.db', () => {
97
143
  );
98
144
  });
99
145
 
100
- it('normalizes admin.sqlWithDirectD1Access worker responses to row arrays', async () => {
146
+ it('normalizes admin.sqlProviderAware worker responses to row arrays', async () => {
101
147
  const fetchMock = vi.fn().mockResolvedValue(
102
- new Response(JSON.stringify({
103
- rows: [{ total: 2 }],
104
- items: [{ total: 2 }],
105
- results: [{ total: 2 }],
106
- }), {
107
- status: 200,
108
- headers: { 'Content-Type': 'application/json' },
109
- }),
148
+ new Response(
149
+ JSON.stringify({
150
+ rows: [{ total: 2 }],
151
+ items: [{ total: 2 }],
152
+ results: [{ total: 2 }],
153
+ }),
154
+ {
155
+ status: 200,
156
+ headers: { 'Content-Type': 'application/json' },
157
+ },
158
+ ),
110
159
  );
111
160
  vi.stubGlobal('fetch', fetchMock);
112
161
 
@@ -129,7 +178,11 @@ describe('buildFunctionContext admin.db', () => {
129
178
  serviceKey: 'sk-test',
130
179
  });
131
180
 
132
- const rows = await ctx.admin.sqlWithDirectD1Access('shared', undefined, 'SELECT COUNT(*) AS total FROM posts');
181
+ const rows = await ctx.admin.sqlProviderAware(
182
+ 'shared',
183
+ undefined,
184
+ 'SELECT COUNT(*) AS total FROM posts',
185
+ );
133
186
 
134
187
  expect(rows).toEqual([{ total: 2 }]);
135
188
  expect(fetchMock).toHaveBeenCalledWith(
@@ -144,31 +197,78 @@ describe('buildFunctionContext admin.db', () => {
144
197
  );
145
198
  });
146
199
 
200
+ it('rejects instance ids for single-instance admin.sqlProviderAware calls before touching direct backends', async () => {
201
+ const fetchMock = vi.fn();
202
+ vi.stubGlobal('fetch', fetchMock);
203
+
204
+ const databaseNamespace = {
205
+ idFromName: vi.fn(),
206
+ get: vi.fn(),
207
+ } as unknown as DurableObjectNamespace;
208
+
209
+ const ctx = buildFunctionContext({
210
+ request: new Request('http://localhost/api/functions/feed-summary'),
211
+ auth: null,
212
+ databaseNamespace,
213
+ authNamespace: {} as DurableObjectNamespace,
214
+ d1Database: {} as D1Database,
215
+ config: {
216
+ databases: {
217
+ shared: {
218
+ tables: {
219
+ posts: { schema: { title: { type: 'string' } } },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ env: {
225
+ DB_D1_SHARED: {
226
+ prepare: vi.fn(),
227
+ },
228
+ } as never,
229
+ });
230
+
231
+ await expect(
232
+ ctx.admin.sqlProviderAware('shared', 'shadow', 'SELECT COUNT(*) AS total FROM posts'),
233
+ ).rejects.toThrow("instanceId is not allowed for single-instance namespace 'shared'");
234
+ expect(
235
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
236
+ ).not.toHaveBeenCalled();
237
+ expect(fetchMock).not.toHaveBeenCalled();
238
+ });
239
+
147
240
  it('routes admin.sqlWithDirectD1Access through the database DO when env is available', async () => {
148
241
  const fetchMock = vi.fn();
149
242
  vi.stubGlobal('fetch', fetchMock);
150
243
 
151
244
  const stub = {
152
- fetch: vi.fn()
245
+ fetch: vi
246
+ .fn()
153
247
  .mockResolvedValueOnce(
154
- new Response(JSON.stringify({
155
- needsCreate: true,
156
- namespace: 'workspace',
157
- id: 'ws-1',
158
- }), {
159
- status: 201,
160
- headers: { 'Content-Type': 'application/json' },
161
- }),
248
+ new Response(
249
+ JSON.stringify({
250
+ needsCreate: true,
251
+ namespace: 'workspace',
252
+ id: 'ws-1',
253
+ }),
254
+ {
255
+ status: 201,
256
+ headers: { 'Content-Type': 'application/json' },
257
+ },
258
+ ),
162
259
  )
163
260
  .mockResolvedValueOnce(
164
- new Response(JSON.stringify({
165
- rows: [{ total: 3 }],
166
- items: [{ total: 3 }],
167
- results: [{ total: 3 }],
168
- }), {
169
- status: 200,
170
- headers: { 'Content-Type': 'application/json' },
171
- }),
261
+ new Response(
262
+ JSON.stringify({
263
+ rows: [{ total: 3 }],
264
+ items: [{ total: 3 }],
265
+ results: [{ total: 3 }],
266
+ }),
267
+ {
268
+ status: 200,
269
+ headers: { 'Content-Type': 'application/json' },
270
+ },
271
+ ),
172
272
  ),
173
273
  };
174
274
  const databaseNamespace = {
@@ -185,6 +285,7 @@ describe('buildFunctionContext admin.db', () => {
185
285
  config: {
186
286
  databases: {
187
287
  workspace: {
288
+ instance: true,
188
289
  tables: {
189
290
  members: { schema: { userId: { type: 'string' } } },
190
291
  },
@@ -196,7 +297,12 @@ describe('buildFunctionContext admin.db', () => {
196
297
  serviceKey: 'sk-test',
197
298
  });
198
299
 
199
- const rows = await ctx.admin.sqlWithDirectD1Access('workspace', 'ws-1', 'SELECT COUNT(*) AS total FROM members', []);
300
+ const rows = await ctx.admin.sqlWithDirectD1Access(
301
+ 'workspace',
302
+ 'ws-1',
303
+ 'SELECT COUNT(*) AS total FROM members',
304
+ [],
305
+ );
200
306
 
201
307
  expect(rows).toEqual([{ total: 3 }]);
202
308
  expect(stub.fetch).toHaveBeenCalledTimes(2);
@@ -213,6 +319,531 @@ describe('buildFunctionContext admin.db', () => {
213
319
  expect(fetchMock).not.toHaveBeenCalled();
214
320
  });
215
321
 
322
+ it('falls back to /api/sql for admin.db(...).table(...).sql tagged templates when only workerUrl is available', async () => {
323
+ const fetchMock = vi.fn().mockResolvedValue(
324
+ new Response(
325
+ JSON.stringify({
326
+ rows: [{ total: 2 }],
327
+ items: [{ total: 2 }],
328
+ results: [{ total: 2 }],
329
+ }),
330
+ {
331
+ status: 200,
332
+ headers: { 'Content-Type': 'application/json' },
333
+ },
334
+ ),
335
+ );
336
+ vi.stubGlobal('fetch', fetchMock);
337
+
338
+ const ctx = buildFunctionContext({
339
+ request: new Request('http://localhost/api/functions/feed-summary'),
340
+ auth: null,
341
+ databaseNamespace: {} as DurableObjectNamespace,
342
+ authNamespace: {} as DurableObjectNamespace,
343
+ d1Database: {} as D1Database,
344
+ config: {
345
+ databases: {
346
+ shared: {
347
+ tables: {
348
+ posts: { schema: { title: { type: 'string' } } },
349
+ },
350
+ },
351
+ },
352
+ },
353
+ workerUrl: 'http://localhost:8787',
354
+ serviceKey: 'sk-test',
355
+ });
356
+
357
+ const rows = await ctx.admin.db('shared').table('posts').sql`
358
+ SELECT COUNT(*) AS total FROM posts WHERE status = ${'published'}
359
+ `;
360
+
361
+ expect(rows).toEqual([{ total: 2 }]);
362
+ expect(fetchMock).toHaveBeenCalledTimes(1);
363
+ expect(fetchMock).toHaveBeenCalledWith(
364
+ 'http://localhost:8787/api/sql',
365
+ expect.objectContaining({ method: 'POST' }),
366
+ );
367
+ });
368
+
369
+ it('routes admin.db(...).table(...).sql tagged templates through the direct SQL executor when env is available', async () => {
370
+ const fetchMock = vi.fn();
371
+ vi.stubGlobal('fetch', fetchMock);
372
+
373
+ const stub = {
374
+ fetch: vi
375
+ .fn()
376
+ .mockResolvedValueOnce(
377
+ new Response(
378
+ JSON.stringify({
379
+ needsCreate: true,
380
+ namespace: 'workspace',
381
+ id: 'ws-1',
382
+ }),
383
+ {
384
+ status: 201,
385
+ headers: { 'Content-Type': 'application/json' },
386
+ },
387
+ ),
388
+ )
389
+ .mockResolvedValueOnce(
390
+ new Response(
391
+ JSON.stringify({
392
+ rows: [{ total: 5 }],
393
+ items: [{ total: 5 }],
394
+ results: [{ total: 5 }],
395
+ }),
396
+ {
397
+ status: 200,
398
+ headers: { 'Content-Type': 'application/json' },
399
+ },
400
+ ),
401
+ ),
402
+ };
403
+ const databaseNamespace = {
404
+ idFromName: vi.fn().mockReturnValue('do-id'),
405
+ get: vi.fn().mockReturnValue(stub),
406
+ } as unknown as DurableObjectNamespace;
407
+
408
+ const ctx = buildFunctionContext({
409
+ request: new Request('http://localhost/api/functions/feed-summary'),
410
+ auth: null,
411
+ databaseNamespace,
412
+ authNamespace: {} as DurableObjectNamespace,
413
+ d1Database: {} as D1Database,
414
+ config: {
415
+ databases: {
416
+ workspace: {
417
+ instance: true,
418
+ tables: {
419
+ members: { schema: { userId: { type: 'string' } } },
420
+ },
421
+ },
422
+ },
423
+ },
424
+ env: {} as never,
425
+ workerUrl: 'http://localhost:8787',
426
+ serviceKey: 'sk-test',
427
+ });
428
+
429
+ const rows = await ctx.admin.db('workspace', 'ws-1').table('members').sql`
430
+ SELECT COUNT(*) AS total FROM members WHERE role = ${'owner'}
431
+ `;
432
+
433
+ expect(rows).toEqual([{ total: 5 }]);
434
+ expect(stub.fetch).toHaveBeenCalledTimes(2);
435
+ const firstRequest = stub.fetch.mock.calls[0]?.[0] as Request;
436
+ await expect(firstRequest.json()).resolves.toEqual({
437
+ query: 'SELECT COUNT(*) AS total FROM members WHERE role = ?',
438
+ params: ['owner'],
439
+ });
440
+ expect(fetchMock).not.toHaveBeenCalled();
441
+ });
442
+
443
+ it.each(['postgres', 'neon'] as const)(
444
+ 'routes admin.db(...).table(...).sql tagged templates through the provider-aware direct SQL executor for %s',
445
+ async (provider) => {
446
+ const fetchMock = vi
447
+ .fn()
448
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
449
+ .mockResolvedValueOnce(
450
+ new Response(
451
+ JSON.stringify({
452
+ columns: ['literal', 'total'],
453
+ rows: [{ literal: '?', total: 5 }],
454
+ rowCount: 1,
455
+ }),
456
+ {
457
+ status: 200,
458
+ headers: { 'Content-Type': 'application/json' },
459
+ },
460
+ ),
461
+ );
462
+ vi.stubGlobal('fetch', fetchMock);
463
+
464
+ const databaseNamespace = {
465
+ idFromName: vi.fn().mockReturnValue('do-id'),
466
+ get: vi.fn(() => ({ fetch: vi.fn() })),
467
+ } as unknown as DurableObjectNamespace;
468
+
469
+ const ctx = buildFunctionContext({
470
+ request: new Request('http://localhost/api/functions/feed-summary'),
471
+ auth: null,
472
+ databaseNamespace,
473
+ authNamespace: {} as DurableObjectNamespace,
474
+ d1Database: {} as D1Database,
475
+ config: {
476
+ databases: {
477
+ shared: {
478
+ provider,
479
+ tables: {
480
+ posts: { schema: { title: { type: 'string' } } },
481
+ },
482
+ },
483
+ },
484
+ },
485
+ env: {
486
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
487
+ JWT_ADMIN_SECRET: 'jwt-secret',
488
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
489
+ } as never,
490
+ workerUrl: 'http://localhost:8787',
491
+ serviceKey: 'sk-test',
492
+ });
493
+
494
+ const rows = await ctx.admin.db('shared').table('posts').sql`
495
+ SELECT '?' AS literal, COUNT(*) AS total FROM posts WHERE title = ${'owner'}
496
+ `;
497
+
498
+ expect(rows).toEqual([{ literal: '?', total: 5 }]);
499
+ expect(fetchMock).toHaveBeenCalledTimes(2);
500
+ expect(fetchMock.mock.calls[1]?.[0]).toBe('http://127.0.0.1:8788/postgres/query');
501
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
502
+ namespace: 'shared',
503
+ sql: "SELECT '?' AS literal, COUNT(*) AS total FROM posts WHERE title = $1",
504
+ params: ['owner'],
505
+ });
506
+ expect(
507
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
508
+ ).not.toHaveBeenCalled();
509
+ },
510
+ );
511
+
512
+ it.each(['postgres', 'neon'] as const)(
513
+ 'preserves PostgreSQL @? operators when admin.db(...).table(...).sql uses the provider-aware direct SQL executor for %s',
514
+ async (provider) => {
515
+ const fetchMock = vi
516
+ .fn()
517
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
518
+ .mockResolvedValueOnce(
519
+ new Response(
520
+ JSON.stringify({
521
+ columns: ['total'],
522
+ rows: [{ total: 4 }],
523
+ rowCount: 1,
524
+ }),
525
+ {
526
+ status: 200,
527
+ headers: { 'Content-Type': 'application/json' },
528
+ },
529
+ ),
530
+ );
531
+ vi.stubGlobal('fetch', fetchMock);
532
+
533
+ const databaseNamespace = {
534
+ idFromName: vi.fn().mockReturnValue('do-id'),
535
+ get: vi.fn(() => ({ fetch: vi.fn() })),
536
+ } as unknown as DurableObjectNamespace;
537
+
538
+ const ctx = buildFunctionContext({
539
+ request: new Request('http://localhost/api/functions/feed-summary'),
540
+ auth: null,
541
+ databaseNamespace,
542
+ authNamespace: {} as DurableObjectNamespace,
543
+ d1Database: {} as D1Database,
544
+ config: {
545
+ databases: {
546
+ shared: {
547
+ provider,
548
+ tables: {
549
+ posts: { schema: { title: { type: 'string' } } },
550
+ },
551
+ },
552
+ },
553
+ },
554
+ env: {
555
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
556
+ JWT_ADMIN_SECRET: 'jwt-secret',
557
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
558
+ } as never,
559
+ workerUrl: 'http://localhost:8787',
560
+ serviceKey: 'sk-test',
561
+ });
562
+
563
+ const rows = await ctx.admin.db('shared').table('posts').sql`
564
+ SELECT COUNT(*) AS total
565
+ FROM posts
566
+ WHERE metadata @? '$.featured'
567
+ AND title = ${'owner'}
568
+ `;
569
+
570
+ expect(rows).toEqual([{ total: 4 }]);
571
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
572
+ namespace: 'shared',
573
+ sql: "SELECT COUNT(*) AS total\n FROM posts\n WHERE metadata @? '$.featured'\n AND title = $1",
574
+ params: ['owner'],
575
+ });
576
+ expect(
577
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
578
+ ).not.toHaveBeenCalled();
579
+ },
580
+ );
581
+
582
+ it.each(['postgres', 'neon'] as const)(
583
+ 'unescapes PostgreSQL @\\? operators when admin.db(...).table(...).sql uses tagged-template markers for %s',
584
+ async (provider) => {
585
+ const fetchMock = vi
586
+ .fn()
587
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
588
+ .mockResolvedValueOnce(
589
+ new Response(
590
+ JSON.stringify({
591
+ columns: ['total'],
592
+ rows: [{ total: 9 }],
593
+ rowCount: 1,
594
+ }),
595
+ {
596
+ status: 200,
597
+ headers: { 'Content-Type': 'application/json' },
598
+ },
599
+ ),
600
+ );
601
+ vi.stubGlobal('fetch', fetchMock);
602
+
603
+ const databaseNamespace = {
604
+ idFromName: vi.fn().mockReturnValue('do-id'),
605
+ get: vi.fn(() => ({ fetch: vi.fn() })),
606
+ } as unknown as DurableObjectNamespace;
607
+
608
+ const ctx = buildFunctionContext({
609
+ request: new Request('http://localhost/api/functions/feed-summary'),
610
+ auth: null,
611
+ databaseNamespace,
612
+ authNamespace: {} as DurableObjectNamespace,
613
+ d1Database: {} as D1Database,
614
+ config: {
615
+ databases: {
616
+ shared: {
617
+ provider,
618
+ tables: {
619
+ posts: { schema: { title: { type: 'string' } } },
620
+ },
621
+ },
622
+ },
623
+ },
624
+ env: {
625
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
626
+ JWT_ADMIN_SECRET: 'jwt-secret',
627
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
628
+ } as never,
629
+ workerUrl: 'http://localhost:8787',
630
+ serviceKey: 'sk-test',
631
+ });
632
+
633
+ const rows = await ctx.admin.db('shared').table('posts').sql(
634
+ makeTaggedTemplateStrings([
635
+ "SELECT COUNT(*) AS total FROM posts WHERE metadata @\\? '$.featured' AND title = ",
636
+ '',
637
+ ]),
638
+ 'owner',
639
+ );
640
+
641
+ expect(rows).toEqual([{ total: 9 }]);
642
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
643
+ namespace: 'shared',
644
+ sql: "SELECT COUNT(*) AS total FROM posts WHERE metadata @? '$.featured' AND title = $1",
645
+ params: ['owner'],
646
+ });
647
+ expect(
648
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
649
+ ).not.toHaveBeenCalled();
650
+ },
651
+ );
652
+
653
+ it.each(['postgres', 'neon'] as const)(
654
+ 'rejects admin.db(...).table(...).sql tagged templates that mix interpolation with literal $n placeholders for %s',
655
+ async (provider) => {
656
+ const fetchMock = vi.fn();
657
+ vi.stubGlobal('fetch', fetchMock);
658
+
659
+ const databaseNamespace = {
660
+ idFromName: vi.fn().mockReturnValue('do-id'),
661
+ get: vi.fn(() => ({ fetch: vi.fn() })),
662
+ } as unknown as DurableObjectNamespace;
663
+
664
+ const ctx = buildFunctionContext({
665
+ request: new Request('http://localhost/api/functions/feed-summary'),
666
+ auth: null,
667
+ databaseNamespace,
668
+ authNamespace: {} as DurableObjectNamespace,
669
+ d1Database: {} as D1Database,
670
+ config: {
671
+ databases: {
672
+ shared: {
673
+ provider,
674
+ tables: {
675
+ posts: { schema: { title: { type: 'string' } } },
676
+ },
677
+ },
678
+ },
679
+ },
680
+ env: {
681
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
682
+ JWT_ADMIN_SECRET: 'jwt-secret',
683
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
684
+ } as never,
685
+ workerUrl: 'http://localhost:8787',
686
+ serviceKey: 'sk-test',
687
+ });
688
+
689
+ await expect(
690
+ ctx.admin.db('shared').table('posts').sql(
691
+ makeTaggedTemplateStrings([
692
+ 'SELECT COUNT(*) AS total FROM posts WHERE tenant_id = $1 AND title = ',
693
+ '',
694
+ ]),
695
+ 'owner',
696
+ ),
697
+ ).rejects.toThrow(
698
+ 'Cannot mix tagged template interpolation with PostgreSQL-style $n placeholders.',
699
+ );
700
+ expect(fetchMock).not.toHaveBeenCalled();
701
+ expect(
702
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
703
+ ).not.toHaveBeenCalled();
704
+ },
705
+ );
706
+
707
+ it.each(['postgres', 'neon'] as const)(
708
+ 'routes admin.sqlProviderAware through the provider-aware direct SQL executor for %s',
709
+ async (provider) => {
710
+ const fetchMock = vi
711
+ .fn()
712
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
713
+ .mockResolvedValueOnce(
714
+ new Response(
715
+ JSON.stringify({
716
+ columns: ['total'],
717
+ rows: [{ total: 7 }],
718
+ rowCount: 1,
719
+ }),
720
+ {
721
+ status: 200,
722
+ headers: { 'Content-Type': 'application/json' },
723
+ },
724
+ ),
725
+ );
726
+ vi.stubGlobal('fetch', fetchMock);
727
+
728
+ const databaseNamespace = {
729
+ idFromName: vi.fn().mockReturnValue('do-id'),
730
+ get: vi.fn(() => ({ fetch: vi.fn() })),
731
+ } as unknown as DurableObjectNamespace;
732
+
733
+ const ctx = buildFunctionContext({
734
+ request: new Request('http://localhost/api/functions/feed-summary'),
735
+ auth: null,
736
+ databaseNamespace,
737
+ authNamespace: {} as DurableObjectNamespace,
738
+ d1Database: {} as D1Database,
739
+ config: {
740
+ databases: {
741
+ shared: {
742
+ provider,
743
+ tables: {
744
+ posts: { schema: { title: { type: 'string' } } },
745
+ },
746
+ },
747
+ },
748
+ },
749
+ env: {
750
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
751
+ JWT_ADMIN_SECRET: 'jwt-secret',
752
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
753
+ } as never,
754
+ workerUrl: 'http://localhost:8787',
755
+ serviceKey: 'sk-test',
756
+ });
757
+
758
+ const rows = await ctx.admin.sqlProviderAware(
759
+ 'shared',
760
+ undefined,
761
+ 'SELECT COUNT(*) AS total FROM posts WHERE title = ?',
762
+ ['owner'],
763
+ );
764
+
765
+ expect(rows).toEqual([{ total: 7 }]);
766
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
767
+ namespace: 'shared',
768
+ sql: 'SELECT COUNT(*) AS total FROM posts WHERE title = $1',
769
+ params: ['owner'],
770
+ });
771
+ expect(
772
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
773
+ ).not.toHaveBeenCalled();
774
+ },
775
+ );
776
+
777
+ it.each(['postgres', 'neon'] as const)(
778
+ 'preserves PostgreSQL @? operators when admin.sqlProviderAware uses the provider-aware direct SQL executor for %s',
779
+ async (provider) => {
780
+ const fetchMock = vi
781
+ .fn()
782
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
783
+ .mockResolvedValueOnce(
784
+ new Response(
785
+ JSON.stringify({
786
+ columns: ['total'],
787
+ rows: [{ total: 6 }],
788
+ rowCount: 1,
789
+ }),
790
+ {
791
+ status: 200,
792
+ headers: { 'Content-Type': 'application/json' },
793
+ },
794
+ ),
795
+ );
796
+ vi.stubGlobal('fetch', fetchMock);
797
+
798
+ const databaseNamespace = {
799
+ idFromName: vi.fn().mockReturnValue('do-id'),
800
+ get: vi.fn(() => ({ fetch: vi.fn() })),
801
+ } as unknown as DurableObjectNamespace;
802
+
803
+ const ctx = buildFunctionContext({
804
+ request: new Request('http://localhost/api/functions/feed-summary'),
805
+ auth: null,
806
+ databaseNamespace,
807
+ authNamespace: {} as DurableObjectNamespace,
808
+ d1Database: {} as D1Database,
809
+ config: {
810
+ databases: {
811
+ shared: {
812
+ provider,
813
+ tables: {
814
+ posts: { schema: { title: { type: 'string' } } },
815
+ },
816
+ },
817
+ },
818
+ },
819
+ env: {
820
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
821
+ JWT_ADMIN_SECRET: 'jwt-secret',
822
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
823
+ } as never,
824
+ workerUrl: 'http://localhost:8787',
825
+ serviceKey: 'sk-test',
826
+ });
827
+
828
+ const rows = await ctx.admin.sqlProviderAware(
829
+ 'shared',
830
+ undefined,
831
+ "SELECT COUNT(*) AS total FROM posts WHERE metadata @? '$.featured' AND title = ?",
832
+ ['owner'],
833
+ );
834
+
835
+ expect(rows).toEqual([{ total: 6 }]);
836
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
837
+ namespace: 'shared',
838
+ sql: "SELECT COUNT(*) AS total FROM posts WHERE metadata @? '$.featured' AND title = $1",
839
+ params: ['owner'],
840
+ });
841
+ expect(
842
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
843
+ ).not.toHaveBeenCalled();
844
+ },
845
+ );
846
+
216
847
  it('routes admin.kv through the configured KV binding when env is available', async () => {
217
848
  const fetchMock = vi.fn();
218
849
  vi.stubGlobal('fetch', fetchMock);
@@ -285,10 +916,14 @@ describe('buildFunctionContext admin.db', () => {
285
916
  serviceKey: 'sk-test',
286
917
  });
287
918
 
288
- const rows = await ctx.admin.d1('analytics').exec('SELECT COUNT(*) AS total FROM rollups WHERE runId = ?', ['r1']);
919
+ const rows = await ctx.admin
920
+ .d1('analytics')
921
+ .exec('SELECT COUNT(*) AS total FROM rollups WHERE runId = ?', ['r1']);
289
922
 
290
923
  expect(rows).toEqual([{ total: 4 }]);
291
- expect(d1Binding.prepare).toHaveBeenCalledWith('SELECT COUNT(*) AS total FROM rollups WHERE runId = ?');
924
+ expect(d1Binding.prepare).toHaveBeenCalledWith(
925
+ 'SELECT COUNT(*) AS total FROM rollups WHERE runId = ?',
926
+ );
292
927
  expect(fetchMock).not.toHaveBeenCalled();
293
928
  });
294
929
 
@@ -306,7 +941,9 @@ describe('buildFunctionContext admin.db', () => {
306
941
  };
307
942
 
308
943
  const ctx = buildFunctionContext({
309
- request: new Request('http://localhost/api/functions/mock/email/inbox/user@test.edgebase.fun'),
944
+ request: new Request(
945
+ 'http://localhost/api/functions/mock/email/inbox/user@test.edgebase.fun',
946
+ ),
310
947
  auth: null,
311
948
  databaseNamespace: {} as DurableObjectNamespace,
312
949
  authNamespace: {} as DurableObjectNamespace,
@@ -317,10 +954,14 @@ describe('buildFunctionContext admin.db', () => {
317
954
  serviceKey: 'sk-test',
318
955
  });
319
956
 
320
- const rows = await ctx.admin.d1('auth').exec('SELECT token FROM _email_tokens WHERE userId = ?', ['u1']);
957
+ const rows = await ctx.admin
958
+ .d1('auth')
959
+ .exec('SELECT token FROM _email_tokens WHERE userId = ?', ['u1']);
321
960
 
322
961
  expect(rows).toEqual([{ token: 'tok-1' }]);
323
- expect(authBinding.prepare).toHaveBeenCalledWith('SELECT token FROM _email_tokens WHERE userId = ?');
962
+ expect(authBinding.prepare).toHaveBeenCalledWith(
963
+ 'SELECT token FROM _email_tokens WHERE userId = ?',
964
+ );
324
965
  expect(fetchMock).not.toHaveBeenCalled();
325
966
  });
326
967