@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.
- package/admin-build/_app/immutable/chunks/{DjOEv9M9.js → A_3UuvCe.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dqk2TGNU.js → B-_-hJ9o.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BFs_qStz.js → B5Nwfelm.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B0QyxC2M.js → BxoNtYHK.js} +3 -3
- package/admin-build/_app/immutable/chunks/{BsFiK_FJ.js → CZ0TVkCa.js} +1 -1
- package/admin-build/_app/immutable/chunks/{k0CIJkw4.js → CzSAxmuj.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D-x55wdW.js → DCKcAiQH.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CSGrwS7E.js → DCvwWZrm.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BTJcQFEp.js → DRqPU3wD.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CqUxCvs_.js → Dc1-6Po6.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D755Tqat.js → DiyBpamp.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BcIUK2sk.js → Dlty5069.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BY07qVPA.js → DpVAayDG.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BCKr7yKd.js → Du5vWVa2.js} +1 -1
- package/admin-build/_app/immutable/chunks/{m9QZTyVV.js → byv2rTy8.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DnLqc9L1.js → nZvorU8i.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BTsq3_xq.js → app.CfrmEXPD.js} +2 -2
- package/admin-build/_app/immutable/entry/start.l1WvHznQ.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.BZ00WDYH.js → 0.Cn2BZ4da.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.RzSJ3yyr.js → 1.Dv4LX_Co.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.D-rsiquF.js → 10.DPVv3kat.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.l7-bgtFD.js → 11.CiCb6Ayu.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.Dkq0H7B5.js → 12.CIPyeekF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DtK_4oRz.js → 13.Z15Lt36e.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.BKo7-AMx.js → 14.s0l5bAq3.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.CQAj_6lq.js → 15.UwSSNO76.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.XVIG-Ffr.js → 16.qiD8i883.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.g6raZLCM.js → 17.Dy3dcSvu.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.IQz6a3T6.js → 18.DeXyPYsO.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.CAAZ8i8h.js → 19.CAbuyS6w.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.BPcX3KPj.js → 20.Bec0T7un.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.DuDYelMY.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.Br5AG_5Z.js → 22.CdVprrv2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.KjbrdXoE.js → 23.Y8RzVLoF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.C3n2-hgw.js → 24.CWhHYFBx.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.SFDSBzHd.js → 25.wCBplOVt.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.D95vui6E.js → 26.Cod_JRFK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.FgLgdjwB.js → 27.BO2HVMu9.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.B9sYYm1F.js → 28.DxG-FBVQ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.DyqZ_wbN.js → 29.CjGqWGvE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.Bzo2yVIO.js → 3.By3_OmdZ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.c1CiNwiS.js → 30.M_H7Htpq.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.CXty66Vh.js → 31.DEU18izM.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.BgQaXZ27.js → 4.DeYhKtzJ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.BuJrHvxH.js → 5.9WLgxhrD.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.CkBBC94k.js → 6.BdT2i_dd.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.D2YBvNFM.js → 7.CHq0s4K6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8qQWo_z.js → 8.DuvRw-XZ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.BLDLX5hV.js → 9.C2Ub82wn.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -2
- package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
- package/src/__tests__/database-live-do.test.ts +50 -0
- package/src/__tests__/database-live-emitter.test.ts +116 -1
- package/src/__tests__/error-format.test.ts +63 -0
- package/src/__tests__/functions-context.test.ts +592 -35
- package/src/__tests__/meta-export-coverage.test.ts +1 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
- package/src/__tests__/provider-aware-sql.test.ts +157 -0
- package/src/__tests__/room-auth-state-loss.test.ts +124 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
- package/src/__tests__/sql-route.test.ts +187 -76
- package/src/durable-objects/database-live-do.ts +46 -1
- package/src/durable-objects/room-runtime-base.ts +26 -2
- package/src/durable-objects/rooms-do.ts +1 -1
- package/src/lib/admin-db-target.ts +30 -74
- package/src/lib/d1-handler.ts +45 -14
- package/src/lib/database-live-emitter.ts +57 -16
- package/src/lib/functions.ts +332 -454
- package/src/lib/internal-transport.ts +316 -0
- package/src/lib/plugin-migrations.ts +39 -39
- package/src/lib/postgres-handler.ts +39 -11
- package/src/lib/provider-aware-sql.ts +827 -0
- package/src/routes/admin.ts +7 -1
- package/src/routes/auth.ts +11 -12
- package/src/routes/sql.ts +51 -76
- package/src/routes/storage.ts +11 -12
- package/src/types.ts +2 -0
- package/admin-build/_app/immutable/entry/start.zXCirpgY.js +0 -1
- 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').
|
|
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.
|
|
104
|
+
it('normalizes admin.sqlProviderAware worker responses to row arrays', async () => {
|
|
101
105
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
102
|
-
new Response(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
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.
|
|
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
|
|
163
|
+
fetch: vi
|
|
164
|
+
.fn()
|
|
153
165
|
.mockResolvedValueOnce(
|
|
154
|
-
new Response(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
878
|
+
expect(authBinding.prepare).toHaveBeenCalledWith(
|
|
879
|
+
'SELECT token FROM _email_tokens WHERE userId = ?',
|
|
880
|
+
);
|
|
324
881
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
325
882
|
});
|
|
326
883
|
|