@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
@@ -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();
@@ -33,7 +37,7 @@ describe('buildFunctionContext admin.db', () => {
33
37
  workerUrl: 'http://localhost:8787',
34
38
  });
35
39
 
36
- const result = await ctx.admin.db('shared').table('posts').list({ limit: 5 });
40
+ const result = await ctx.admin.db('shared').table('posts').limit(5).getList();
37
41
 
38
42
  expect(result.items).toHaveLength(1);
39
43
  expect(fetchMock).toHaveBeenCalledWith(
@@ -97,16 +101,19 @@ describe('buildFunctionContext admin.db', () => {
97
101
  );
98
102
  });
99
103
 
100
- it('normalizes admin.sql worker responses to row arrays', async () => {
104
+ it('normalizes admin.sqlProviderAware worker responses to row arrays', async () => {
101
105
  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
- }),
106
+ new Response(
107
+ JSON.stringify({
108
+ rows: [{ total: 2 }],
109
+ items: [{ total: 2 }],
110
+ results: [{ total: 2 }],
111
+ }),
112
+ {
113
+ status: 200,
114
+ headers: { 'Content-Type': 'application/json' },
115
+ },
116
+ ),
110
117
  );
111
118
  vi.stubGlobal('fetch', fetchMock);
112
119
 
@@ -129,7 +136,11 @@ describe('buildFunctionContext admin.db', () => {
129
136
  serviceKey: 'sk-test',
130
137
  });
131
138
 
132
- const rows = await ctx.admin.sql('shared', undefined, 'SELECT COUNT(*) AS total FROM posts');
139
+ const rows = await ctx.admin.sqlProviderAware(
140
+ 'shared',
141
+ undefined,
142
+ 'SELECT COUNT(*) AS total FROM posts',
143
+ );
133
144
 
134
145
  expect(rows).toEqual([{ total: 2 }]);
135
146
  expect(fetchMock).toHaveBeenCalledWith(
@@ -144,31 +155,38 @@ describe('buildFunctionContext admin.db', () => {
144
155
  );
145
156
  });
146
157
 
147
- it('routes admin.sql through the database DO when env is available', async () => {
158
+ it('routes admin.sqlWithDirectD1Access through the database DO when env is available', async () => {
148
159
  const fetchMock = vi.fn();
149
160
  vi.stubGlobal('fetch', fetchMock);
150
161
 
151
162
  const stub = {
152
- fetch: vi.fn()
163
+ fetch: vi
164
+ .fn()
153
165
  .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
- }),
166
+ new Response(
167
+ JSON.stringify({
168
+ needsCreate: true,
169
+ namespace: 'workspace',
170
+ id: 'ws-1',
171
+ }),
172
+ {
173
+ status: 201,
174
+ headers: { 'Content-Type': 'application/json' },
175
+ },
176
+ ),
162
177
  )
163
178
  .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
- }),
179
+ new Response(
180
+ JSON.stringify({
181
+ rows: [{ total: 3 }],
182
+ items: [{ total: 3 }],
183
+ results: [{ total: 3 }],
184
+ }),
185
+ {
186
+ status: 200,
187
+ headers: { 'Content-Type': 'application/json' },
188
+ },
189
+ ),
172
190
  ),
173
191
  };
174
192
  const databaseNamespace = {
@@ -196,7 +214,12 @@ describe('buildFunctionContext admin.db', () => {
196
214
  serviceKey: 'sk-test',
197
215
  });
198
216
 
199
- const rows = await ctx.admin.sql('workspace', 'ws-1', 'SELECT COUNT(*) AS total FROM members', []);
217
+ const rows = await ctx.admin.sqlWithDirectD1Access(
218
+ 'workspace',
219
+ 'ws-1',
220
+ 'SELECT COUNT(*) AS total FROM members',
221
+ [],
222
+ );
200
223
 
201
224
  expect(rows).toEqual([{ total: 3 }]);
202
225
  expect(stub.fetch).toHaveBeenCalledTimes(2);
@@ -213,6 +236,530 @@ describe('buildFunctionContext admin.db', () => {
213
236
  expect(fetchMock).not.toHaveBeenCalled();
214
237
  });
215
238
 
239
+ it('falls back to /api/sql for admin.db(...).table(...).sql tagged templates when only workerUrl is available', async () => {
240
+ const fetchMock = vi.fn().mockResolvedValue(
241
+ new Response(
242
+ JSON.stringify({
243
+ rows: [{ total: 2 }],
244
+ items: [{ total: 2 }],
245
+ results: [{ total: 2 }],
246
+ }),
247
+ {
248
+ status: 200,
249
+ headers: { 'Content-Type': 'application/json' },
250
+ },
251
+ ),
252
+ );
253
+ vi.stubGlobal('fetch', fetchMock);
254
+
255
+ const ctx = buildFunctionContext({
256
+ request: new Request('http://localhost/api/functions/feed-summary'),
257
+ auth: null,
258
+ databaseNamespace: {} as DurableObjectNamespace,
259
+ authNamespace: {} as DurableObjectNamespace,
260
+ d1Database: {} as D1Database,
261
+ config: {
262
+ databases: {
263
+ shared: {
264
+ tables: {
265
+ posts: { schema: { title: { type: 'string' } } },
266
+ },
267
+ },
268
+ },
269
+ },
270
+ workerUrl: 'http://localhost:8787',
271
+ serviceKey: 'sk-test',
272
+ });
273
+
274
+ const rows = await ctx.admin.db('shared').table('posts').sql`
275
+ SELECT COUNT(*) AS total FROM posts WHERE status = ${'published'}
276
+ `;
277
+
278
+ expect(rows).toEqual([{ total: 2 }]);
279
+ expect(fetchMock).toHaveBeenCalledTimes(1);
280
+ expect(fetchMock).toHaveBeenCalledWith(
281
+ 'http://localhost:8787/api/sql',
282
+ expect.objectContaining({ method: 'POST' }),
283
+ );
284
+ });
285
+
286
+ it('routes admin.db(...).table(...).sql tagged templates through the direct SQL executor when env is available', async () => {
287
+ const fetchMock = vi.fn();
288
+ vi.stubGlobal('fetch', fetchMock);
289
+
290
+ const stub = {
291
+ fetch: vi
292
+ .fn()
293
+ .mockResolvedValueOnce(
294
+ new Response(
295
+ JSON.stringify({
296
+ needsCreate: true,
297
+ namespace: 'workspace',
298
+ id: 'ws-1',
299
+ }),
300
+ {
301
+ status: 201,
302
+ headers: { 'Content-Type': 'application/json' },
303
+ },
304
+ ),
305
+ )
306
+ .mockResolvedValueOnce(
307
+ new Response(
308
+ JSON.stringify({
309
+ rows: [{ total: 5 }],
310
+ items: [{ total: 5 }],
311
+ results: [{ total: 5 }],
312
+ }),
313
+ {
314
+ status: 200,
315
+ headers: { 'Content-Type': 'application/json' },
316
+ },
317
+ ),
318
+ ),
319
+ };
320
+ const databaseNamespace = {
321
+ idFromName: vi.fn().mockReturnValue('do-id'),
322
+ get: vi.fn().mockReturnValue(stub),
323
+ } as unknown as DurableObjectNamespace;
324
+
325
+ const ctx = buildFunctionContext({
326
+ request: new Request('http://localhost/api/functions/feed-summary'),
327
+ auth: null,
328
+ databaseNamespace,
329
+ authNamespace: {} as DurableObjectNamespace,
330
+ d1Database: {} as D1Database,
331
+ config: {
332
+ databases: {
333
+ workspace: {
334
+ tables: {
335
+ members: { schema: { userId: { type: 'string' } } },
336
+ },
337
+ },
338
+ },
339
+ },
340
+ env: {} as never,
341
+ workerUrl: 'http://localhost:8787',
342
+ serviceKey: 'sk-test',
343
+ });
344
+
345
+ const rows = await ctx.admin.db('workspace', 'ws-1').table('members').sql`
346
+ SELECT COUNT(*) AS total FROM members WHERE role = ${'owner'}
347
+ `;
348
+
349
+ expect(rows).toEqual([{ total: 5 }]);
350
+ expect(stub.fetch).toHaveBeenCalledTimes(2);
351
+ const firstRequest = stub.fetch.mock.calls[0]?.[0] as Request;
352
+ await expect(firstRequest.json()).resolves.toEqual({
353
+ query: 'SELECT COUNT(*) AS total FROM members WHERE role = ?',
354
+ params: ['owner'],
355
+ });
356
+ expect(fetchMock).not.toHaveBeenCalled();
357
+ });
358
+
359
+ it.each(['postgres', 'neon'] as const)(
360
+ 'routes admin.db(...).table(...).sql tagged templates through the provider-aware direct SQL executor for %s',
361
+ async (provider) => {
362
+ const fetchMock = vi
363
+ .fn()
364
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
365
+ .mockResolvedValueOnce(
366
+ new Response(
367
+ JSON.stringify({
368
+ columns: ['literal', 'total'],
369
+ rows: [{ literal: '?', total: 5 }],
370
+ rowCount: 1,
371
+ }),
372
+ {
373
+ status: 200,
374
+ headers: { 'Content-Type': 'application/json' },
375
+ },
376
+ ),
377
+ );
378
+ vi.stubGlobal('fetch', fetchMock);
379
+
380
+ const databaseNamespace = {
381
+ idFromName: vi.fn().mockReturnValue('do-id'),
382
+ get: vi.fn(() => ({ fetch: vi.fn() })),
383
+ } as unknown as DurableObjectNamespace;
384
+
385
+ const ctx = buildFunctionContext({
386
+ request: new Request('http://localhost/api/functions/feed-summary'),
387
+ auth: null,
388
+ databaseNamespace,
389
+ authNamespace: {} as DurableObjectNamespace,
390
+ d1Database: {} as D1Database,
391
+ config: {
392
+ databases: {
393
+ shared: {
394
+ provider,
395
+ tables: {
396
+ posts: { schema: { title: { type: 'string' } } },
397
+ },
398
+ },
399
+ },
400
+ },
401
+ env: {
402
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
403
+ JWT_ADMIN_SECRET: 'jwt-secret',
404
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
405
+ } as never,
406
+ workerUrl: 'http://localhost:8787',
407
+ serviceKey: 'sk-test',
408
+ });
409
+
410
+ const rows = await ctx.admin.db('shared').table('posts').sql`
411
+ SELECT '?' AS literal, COUNT(*) AS total FROM posts WHERE title = ${'owner'}
412
+ `;
413
+
414
+ expect(rows).toEqual([{ literal: '?', total: 5 }]);
415
+ expect(fetchMock).toHaveBeenCalledTimes(2);
416
+ expect(fetchMock.mock.calls[1]?.[0]).toBe('http://127.0.0.1:8788/postgres/query');
417
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
418
+ namespace: 'shared',
419
+ sql: "SELECT '?' AS literal, COUNT(*) AS total FROM posts WHERE title = $1",
420
+ params: ['owner'],
421
+ });
422
+ expect(
423
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
424
+ ).not.toHaveBeenCalled();
425
+ },
426
+ );
427
+
428
+ it.each(['postgres', 'neon'] as const)(
429
+ 'preserves PostgreSQL @? operators when admin.db(...).table(...).sql uses the provider-aware direct SQL executor for %s',
430
+ async (provider) => {
431
+ const fetchMock = vi
432
+ .fn()
433
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
434
+ .mockResolvedValueOnce(
435
+ new Response(
436
+ JSON.stringify({
437
+ columns: ['total'],
438
+ rows: [{ total: 4 }],
439
+ rowCount: 1,
440
+ }),
441
+ {
442
+ status: 200,
443
+ headers: { 'Content-Type': 'application/json' },
444
+ },
445
+ ),
446
+ );
447
+ vi.stubGlobal('fetch', fetchMock);
448
+
449
+ const databaseNamespace = {
450
+ idFromName: vi.fn().mockReturnValue('do-id'),
451
+ get: vi.fn(() => ({ fetch: vi.fn() })),
452
+ } as unknown as DurableObjectNamespace;
453
+
454
+ const ctx = buildFunctionContext({
455
+ request: new Request('http://localhost/api/functions/feed-summary'),
456
+ auth: null,
457
+ databaseNamespace,
458
+ authNamespace: {} as DurableObjectNamespace,
459
+ d1Database: {} as D1Database,
460
+ config: {
461
+ databases: {
462
+ shared: {
463
+ provider,
464
+ tables: {
465
+ posts: { schema: { title: { type: 'string' } } },
466
+ },
467
+ },
468
+ },
469
+ },
470
+ env: {
471
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
472
+ JWT_ADMIN_SECRET: 'jwt-secret',
473
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
474
+ } as never,
475
+ workerUrl: 'http://localhost:8787',
476
+ serviceKey: 'sk-test',
477
+ });
478
+
479
+ const rows = await ctx.admin.db('shared').table('posts').sql`
480
+ SELECT COUNT(*) AS total
481
+ FROM posts
482
+ WHERE metadata @? '$.featured'
483
+ AND title = ${'owner'}
484
+ `;
485
+
486
+ expect(rows).toEqual([{ total: 4 }]);
487
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
488
+ namespace: 'shared',
489
+ sql: "SELECT COUNT(*) AS total\n FROM posts\n WHERE metadata @? '$.featured'\n AND title = $1",
490
+ params: ['owner'],
491
+ });
492
+ expect(
493
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
494
+ ).not.toHaveBeenCalled();
495
+ },
496
+ );
497
+
498
+ it.each(['postgres', 'neon'] as const)(
499
+ 'unescapes PostgreSQL @\\? operators when admin.db(...).table(...).sql uses tagged-template markers for %s',
500
+ async (provider) => {
501
+ const fetchMock = vi
502
+ .fn()
503
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
504
+ .mockResolvedValueOnce(
505
+ new Response(
506
+ JSON.stringify({
507
+ columns: ['total'],
508
+ rows: [{ total: 9 }],
509
+ rowCount: 1,
510
+ }),
511
+ {
512
+ status: 200,
513
+ headers: { 'Content-Type': 'application/json' },
514
+ },
515
+ ),
516
+ );
517
+ vi.stubGlobal('fetch', fetchMock);
518
+
519
+ const databaseNamespace = {
520
+ idFromName: vi.fn().mockReturnValue('do-id'),
521
+ get: vi.fn(() => ({ fetch: vi.fn() })),
522
+ } as unknown as DurableObjectNamespace;
523
+
524
+ const ctx = buildFunctionContext({
525
+ request: new Request('http://localhost/api/functions/feed-summary'),
526
+ auth: null,
527
+ databaseNamespace,
528
+ authNamespace: {} as DurableObjectNamespace,
529
+ d1Database: {} as D1Database,
530
+ config: {
531
+ databases: {
532
+ shared: {
533
+ provider,
534
+ tables: {
535
+ posts: { schema: { title: { type: 'string' } } },
536
+ },
537
+ },
538
+ },
539
+ },
540
+ env: {
541
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
542
+ JWT_ADMIN_SECRET: 'jwt-secret',
543
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
544
+ } as never,
545
+ workerUrl: 'http://localhost:8787',
546
+ serviceKey: 'sk-test',
547
+ });
548
+
549
+ const rows = await ctx.admin.db('shared').table('posts').sql(
550
+ makeTaggedTemplateStrings([
551
+ "SELECT COUNT(*) AS total FROM posts WHERE metadata @\\? '$.featured' AND title = ",
552
+ '',
553
+ ]),
554
+ 'owner',
555
+ );
556
+
557
+ expect(rows).toEqual([{ total: 9 }]);
558
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
559
+ namespace: 'shared',
560
+ sql: "SELECT COUNT(*) AS total FROM posts WHERE metadata @? '$.featured' AND title = $1",
561
+ params: ['owner'],
562
+ });
563
+ expect(
564
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
565
+ ).not.toHaveBeenCalled();
566
+ },
567
+ );
568
+
569
+ it.each(['postgres', 'neon'] as const)(
570
+ 'rejects admin.db(...).table(...).sql tagged templates that mix interpolation with literal $n placeholders for %s',
571
+ async (provider) => {
572
+ const fetchMock = vi.fn();
573
+ vi.stubGlobal('fetch', fetchMock);
574
+
575
+ const databaseNamespace = {
576
+ idFromName: vi.fn().mockReturnValue('do-id'),
577
+ get: vi.fn(() => ({ fetch: vi.fn() })),
578
+ } as unknown as DurableObjectNamespace;
579
+
580
+ const ctx = buildFunctionContext({
581
+ request: new Request('http://localhost/api/functions/feed-summary'),
582
+ auth: null,
583
+ databaseNamespace,
584
+ authNamespace: {} as DurableObjectNamespace,
585
+ d1Database: {} as D1Database,
586
+ config: {
587
+ databases: {
588
+ shared: {
589
+ provider,
590
+ tables: {
591
+ posts: { schema: { title: { type: 'string' } } },
592
+ },
593
+ },
594
+ },
595
+ },
596
+ env: {
597
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
598
+ JWT_ADMIN_SECRET: 'jwt-secret',
599
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
600
+ } as never,
601
+ workerUrl: 'http://localhost:8787',
602
+ serviceKey: 'sk-test',
603
+ });
604
+
605
+ await expect(
606
+ ctx.admin.db('shared').table('posts').sql(
607
+ makeTaggedTemplateStrings([
608
+ 'SELECT COUNT(*) AS total FROM posts WHERE tenant_id = $1 AND title = ',
609
+ '',
610
+ ]),
611
+ 'owner',
612
+ ),
613
+ ).rejects.toThrow(
614
+ 'Cannot mix tagged template interpolation with PostgreSQL-style $n placeholders.',
615
+ );
616
+ expect(fetchMock).not.toHaveBeenCalled();
617
+ expect(
618
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
619
+ ).not.toHaveBeenCalled();
620
+ },
621
+ );
622
+
623
+ it.each(['postgres', 'neon'] as const)(
624
+ 'routes admin.sqlProviderAware through the provider-aware direct SQL executor for %s',
625
+ async (provider) => {
626
+ const fetchMock = vi
627
+ .fn()
628
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
629
+ .mockResolvedValueOnce(
630
+ new Response(
631
+ JSON.stringify({
632
+ columns: ['total'],
633
+ rows: [{ total: 7 }],
634
+ rowCount: 1,
635
+ }),
636
+ {
637
+ status: 200,
638
+ headers: { 'Content-Type': 'application/json' },
639
+ },
640
+ ),
641
+ );
642
+ vi.stubGlobal('fetch', fetchMock);
643
+
644
+ const databaseNamespace = {
645
+ idFromName: vi.fn().mockReturnValue('do-id'),
646
+ get: vi.fn(() => ({ fetch: vi.fn() })),
647
+ } as unknown as DurableObjectNamespace;
648
+
649
+ const ctx = buildFunctionContext({
650
+ request: new Request('http://localhost/api/functions/feed-summary'),
651
+ auth: null,
652
+ databaseNamespace,
653
+ authNamespace: {} as DurableObjectNamespace,
654
+ d1Database: {} as D1Database,
655
+ config: {
656
+ databases: {
657
+ shared: {
658
+ provider,
659
+ tables: {
660
+ posts: { schema: { title: { type: 'string' } } },
661
+ },
662
+ },
663
+ },
664
+ },
665
+ env: {
666
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
667
+ JWT_ADMIN_SECRET: 'jwt-secret',
668
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
669
+ } as never,
670
+ workerUrl: 'http://localhost:8787',
671
+ serviceKey: 'sk-test',
672
+ });
673
+
674
+ const rows = await ctx.admin.sqlProviderAware(
675
+ 'shared',
676
+ undefined,
677
+ 'SELECT COUNT(*) AS total FROM posts WHERE title = ?',
678
+ ['owner'],
679
+ );
680
+
681
+ expect(rows).toEqual([{ total: 7 }]);
682
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
683
+ namespace: 'shared',
684
+ sql: 'SELECT COUNT(*) AS total FROM posts WHERE title = $1',
685
+ params: ['owner'],
686
+ });
687
+ expect(
688
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
689
+ ).not.toHaveBeenCalled();
690
+ },
691
+ );
692
+
693
+ it.each(['postgres', 'neon'] as const)(
694
+ 'preserves PostgreSQL @? operators when admin.sqlProviderAware uses the provider-aware direct SQL executor for %s',
695
+ async (provider) => {
696
+ const fetchMock = vi
697
+ .fn()
698
+ .mockResolvedValueOnce(new Response(null, { status: 200 }))
699
+ .mockResolvedValueOnce(
700
+ new Response(
701
+ JSON.stringify({
702
+ columns: ['total'],
703
+ rows: [{ total: 6 }],
704
+ rowCount: 1,
705
+ }),
706
+ {
707
+ status: 200,
708
+ headers: { 'Content-Type': 'application/json' },
709
+ },
710
+ ),
711
+ );
712
+ vi.stubGlobal('fetch', fetchMock);
713
+
714
+ const databaseNamespace = {
715
+ idFromName: vi.fn().mockReturnValue('do-id'),
716
+ get: vi.fn(() => ({ fetch: vi.fn() })),
717
+ } as unknown as DurableObjectNamespace;
718
+
719
+ const ctx = buildFunctionContext({
720
+ request: new Request('http://localhost/api/functions/feed-summary'),
721
+ auth: null,
722
+ databaseNamespace,
723
+ authNamespace: {} as DurableObjectNamespace,
724
+ d1Database: {} as D1Database,
725
+ config: {
726
+ databases: {
727
+ shared: {
728
+ provider,
729
+ tables: {
730
+ posts: { schema: { title: { type: 'string' } } },
731
+ },
732
+ },
733
+ },
734
+ },
735
+ env: {
736
+ EDGEBASE_DEV_SIDECAR_PORT: '8788',
737
+ JWT_ADMIN_SECRET: 'jwt-secret',
738
+ DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
739
+ } as never,
740
+ workerUrl: 'http://localhost:8787',
741
+ serviceKey: 'sk-test',
742
+ });
743
+
744
+ const rows = await ctx.admin.sqlProviderAware(
745
+ 'shared',
746
+ undefined,
747
+ "SELECT COUNT(*) AS total FROM posts WHERE metadata @? '$.featured' AND title = ?",
748
+ ['owner'],
749
+ );
750
+
751
+ expect(rows).toEqual([{ total: 6 }]);
752
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
753
+ namespace: 'shared',
754
+ sql: "SELECT COUNT(*) AS total FROM posts WHERE metadata @? '$.featured' AND title = $1",
755
+ params: ['owner'],
756
+ });
757
+ expect(
758
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
759
+ ).not.toHaveBeenCalled();
760
+ },
761
+ );
762
+
216
763
  it('routes admin.kv through the configured KV binding when env is available', async () => {
217
764
  const fetchMock = vi.fn();
218
765
  vi.stubGlobal('fetch', fetchMock);
@@ -285,10 +832,14 @@ describe('buildFunctionContext admin.db', () => {
285
832
  serviceKey: 'sk-test',
286
833
  });
287
834
 
288
- const rows = await ctx.admin.d1('analytics').exec('SELECT COUNT(*) AS total FROM rollups WHERE runId = ?', ['r1']);
835
+ const rows = await ctx.admin
836
+ .d1('analytics')
837
+ .exec('SELECT COUNT(*) AS total FROM rollups WHERE runId = ?', ['r1']);
289
838
 
290
839
  expect(rows).toEqual([{ total: 4 }]);
291
- expect(d1Binding.prepare).toHaveBeenCalledWith('SELECT COUNT(*) AS total FROM rollups WHERE runId = ?');
840
+ expect(d1Binding.prepare).toHaveBeenCalledWith(
841
+ 'SELECT COUNT(*) AS total FROM rollups WHERE runId = ?',
842
+ );
292
843
  expect(fetchMock).not.toHaveBeenCalled();
293
844
  });
294
845
 
@@ -306,7 +857,9 @@ describe('buildFunctionContext admin.db', () => {
306
857
  };
307
858
 
308
859
  const ctx = buildFunctionContext({
309
- request: new Request('http://localhost/api/functions/mock/email/inbox/user@test.edgebase.fun'),
860
+ request: new Request(
861
+ 'http://localhost/api/functions/mock/email/inbox/user@test.edgebase.fun',
862
+ ),
310
863
  auth: null,
311
864
  databaseNamespace: {} as DurableObjectNamespace,
312
865
  authNamespace: {} as DurableObjectNamespace,
@@ -317,10 +870,14 @@ describe('buildFunctionContext admin.db', () => {
317
870
  serviceKey: 'sk-test',
318
871
  });
319
872
 
320
- const rows = await ctx.admin.d1('auth').exec('SELECT token FROM _email_tokens WHERE userId = ?', ['u1']);
873
+ const rows = await ctx.admin
874
+ .d1('auth')
875
+ .exec('SELECT token FROM _email_tokens WHERE userId = ?', ['u1']);
321
876
 
322
877
  expect(rows).toEqual([{ token: 'tok-1' }]);
323
- expect(authBinding.prepare).toHaveBeenCalledWith('SELECT token FROM _email_tokens WHERE userId = ?');
878
+ expect(authBinding.prepare).toHaveBeenCalledWith(
879
+ 'SELECT token FROM _email_tokens WHERE userId = ?',
880
+ );
324
881
  expect(fetchMock).not.toHaveBeenCalled();
325
882
  });
326
883