@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.
- package/admin-build/_app/immutable/chunks/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
- package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
- package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
- package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
- package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
- package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
- package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
- package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
- package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BZxfavhF.js → app.C8ylfBe6.js} +2 -2
- package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.VRVb0wee.js → 20.DYb-q3W8.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.js → 9.DOkvfmir.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -3
- package/src/__tests__/admin-data-routes.test.ts +29 -0
- package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
- package/src/__tests__/database-do-route-validation.test.ts +105 -0
- package/src/__tests__/database-live-do.test.ts +50 -0
- package/src/__tests__/database-live-emitter.test.ts +116 -1
- package/src/__tests__/database-live-route.test.ts +82 -0
- package/src/__tests__/do-router.test.ts +116 -0
- package/src/__tests__/error-format.test.ts +63 -0
- package/src/__tests__/functions-context.test.ts +674 -33
- package/src/__tests__/functions-d1-proxy.test.ts +54 -0
- package/src/__tests__/plugin-migration-routing.test.ts +32 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
- package/src/__tests__/provider-aware-sql.test.ts +163 -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__/scheduled.test.ts +55 -0
- package/src/__tests__/service-key-db-proxy.test.ts +122 -1
- package/src/__tests__/sql-route.test.ts +252 -75
- package/src/__tests__/table-hook-runtime.test.ts +137 -0
- package/src/durable-objects/database-do.ts +36 -45
- 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/index.ts +12 -6
- package/src/lib/admin-db-target.ts +30 -74
- package/src/lib/d1-handler.ts +55 -35
- package/src/lib/database-live-emitter.ts +57 -16
- package/src/lib/do-router.ts +135 -3
- package/src/lib/functions.ts +215 -143
- package/src/lib/internal-transport.ts +28 -12
- package/src/lib/plugin-migration-routing.ts +28 -0
- package/src/lib/plugin-migrations.ts +38 -38
- package/src/lib/postgres-handler.ts +51 -31
- package/src/lib/provider-aware-sql.ts +831 -0
- package/src/lib/table-hook-runtime.ts +62 -0
- package/src/routes/admin.ts +41 -41
- package/src/routes/auth.ts +7 -2
- package/src/routes/database-live.ts +110 -12
- package/src/routes/sql.ts +64 -84
- package/src/routes/storage.ts +7 -2
- package/src/routes/tables.ts +42 -29
- package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
- package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
- package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
- package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { defineConfig } from '@edge-base/shared';
|
|
3
3
|
import { EdgeBaseError } from '@edge-base/shared';
|
|
4
4
|
import { setConfig } from '../lib/do-router.js';
|
|
@@ -117,6 +117,7 @@ describe('DB proxy service key forwarding', () => {
|
|
|
117
117
|
databases: {
|
|
118
118
|
workspace: {
|
|
119
119
|
provider: 'do',
|
|
120
|
+
instance: true,
|
|
120
121
|
tables: {
|
|
121
122
|
users: {},
|
|
122
123
|
},
|
|
@@ -203,6 +204,7 @@ describe('DB proxy service key forwarding', () => {
|
|
|
203
204
|
databases: {
|
|
204
205
|
workspace: {
|
|
205
206
|
provider: 'do',
|
|
207
|
+
instance: true,
|
|
206
208
|
tables: {
|
|
207
209
|
users: {},
|
|
208
210
|
},
|
|
@@ -375,6 +377,125 @@ describe('DB proxy service key forwarding', () => {
|
|
|
375
377
|
await expect(response.json()).resolves.toEqual({ id: 'u1', name: 'June' });
|
|
376
378
|
});
|
|
377
379
|
|
|
380
|
+
it('auto-retries single-instance provider=do namespaces when the DO asks for bootstrap authorization', async () => {
|
|
381
|
+
setConfig(defineConfig({
|
|
382
|
+
release: true,
|
|
383
|
+
databases: {
|
|
384
|
+
app: {
|
|
385
|
+
provider: 'do',
|
|
386
|
+
tables: {
|
|
387
|
+
posts: {
|
|
388
|
+
access: {
|
|
389
|
+
read: () => true,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
}));
|
|
396
|
+
|
|
397
|
+
const forwardedHeaders: Headers[] = [];
|
|
398
|
+
let callCount = 0;
|
|
399
|
+
const app = createApp();
|
|
400
|
+
const response = await app.request('/api/db/app/tables/posts', {
|
|
401
|
+
method: 'GET',
|
|
402
|
+
}, createEnv((_input, init) => {
|
|
403
|
+
forwardedHeaders.push(new Headers(init?.headers));
|
|
404
|
+
callCount += 1;
|
|
405
|
+
if (callCount === 1) {
|
|
406
|
+
return new Response(JSON.stringify({ needsCreate: true, namespace: 'app' }), {
|
|
407
|
+
status: 201,
|
|
408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return new Response(JSON.stringify({ items: [] }), {
|
|
412
|
+
status: 200,
|
|
413
|
+
headers: { 'Content-Type': 'application/json' },
|
|
414
|
+
});
|
|
415
|
+
}));
|
|
416
|
+
|
|
417
|
+
expect(response.status).toBe(200);
|
|
418
|
+
await expect(response.json()).resolves.toEqual({ items: [] });
|
|
419
|
+
expect(forwardedHeaders).toHaveLength(2);
|
|
420
|
+
expect(forwardedHeaders[0].get('X-DO-Create-Authorized')).toBeNull();
|
|
421
|
+
expect(forwardedHeaders[1].get('X-DO-Create-Authorized')).toBe('1');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('rejects instanceId route segments for single-instance namespaces before touching the DO', async () => {
|
|
425
|
+
setConfig(defineConfig({
|
|
426
|
+
release: true,
|
|
427
|
+
databases: {
|
|
428
|
+
app: {
|
|
429
|
+
provider: 'do',
|
|
430
|
+
tables: {
|
|
431
|
+
posts: {
|
|
432
|
+
access: {
|
|
433
|
+
read: () => true,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
}));
|
|
440
|
+
|
|
441
|
+
const onFetch = vi.fn();
|
|
442
|
+
const app = createApp();
|
|
443
|
+
const response = await app.request('/api/db/app/attacker/tables/posts', {
|
|
444
|
+
method: 'GET',
|
|
445
|
+
}, createEnv(onFetch));
|
|
446
|
+
|
|
447
|
+
expect(response.status).toBe(400);
|
|
448
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
449
|
+
code: 400,
|
|
450
|
+
message: "instanceId is not allowed for single-instance namespace 'app'",
|
|
451
|
+
});
|
|
452
|
+
expect(onFetch).not.toHaveBeenCalled();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('rejects missing instanceId for dynamic namespaces even for service key requests', async () => {
|
|
456
|
+
setConfig(defineConfig({
|
|
457
|
+
release: true,
|
|
458
|
+
databases: {
|
|
459
|
+
workspace: {
|
|
460
|
+
provider: 'do',
|
|
461
|
+
instance: true,
|
|
462
|
+
tables: {
|
|
463
|
+
users: {},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
serviceKeys: {
|
|
468
|
+
keys: [
|
|
469
|
+
{
|
|
470
|
+
kid: 'root',
|
|
471
|
+
tier: 'root',
|
|
472
|
+
scopes: ['*'],
|
|
473
|
+
secretSource: 'inline',
|
|
474
|
+
inlineSecret: 'sk-root',
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
}));
|
|
479
|
+
|
|
480
|
+
const onFetch = vi.fn();
|
|
481
|
+
const app = createApp();
|
|
482
|
+
const response = await app.request('/api/db/workspace/tables/users', {
|
|
483
|
+
method: 'POST',
|
|
484
|
+
headers: {
|
|
485
|
+
'X-EdgeBase-Service-Key': 'sk-root',
|
|
486
|
+
'Content-Type': 'application/json',
|
|
487
|
+
},
|
|
488
|
+
body: JSON.stringify({ id: 'user-1' }),
|
|
489
|
+
}, createEnv(onFetch));
|
|
490
|
+
|
|
491
|
+
expect(response.status).toBe(400);
|
|
492
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
493
|
+
code: 400,
|
|
494
|
+
message: "instanceId is required for dynamic namespace 'workspace'",
|
|
495
|
+
});
|
|
496
|
+
expect(onFetch).not.toHaveBeenCalled();
|
|
497
|
+
});
|
|
498
|
+
|
|
378
499
|
it('does not trust raw X-EdgeBase-Internal on public DB requests', async () => {
|
|
379
500
|
setConfig(defineConfig({
|
|
380
501
|
release: true,
|
|
@@ -18,22 +18,28 @@ describe('sql route', () => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it('rejects unconfigured shared namespace instead of treating it as implicit', async () => {
|
|
21
|
-
setConfig(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
setConfig(
|
|
22
|
+
defineConfig({
|
|
23
|
+
databases: {
|
|
24
|
+
app: {
|
|
25
|
+
tables: {
|
|
26
|
+
posts: { schema: { title: { type: 'string' } } },
|
|
27
|
+
},
|
|
26
28
|
},
|
|
27
29
|
},
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
30
32
|
|
|
31
33
|
const app = createApp();
|
|
32
|
-
const response = await app.request(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const response = await app.request(
|
|
35
|
+
'/api/sql',
|
|
36
|
+
{
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ namespace: 'shared', sql: 'SELECT 1' }),
|
|
40
|
+
},
|
|
41
|
+
{} as Env,
|
|
42
|
+
);
|
|
37
43
|
|
|
38
44
|
expect(response.status).toBe(404);
|
|
39
45
|
await expect(response.json()).resolves.toMatchObject({
|
|
@@ -42,31 +48,100 @@ describe('sql route', () => {
|
|
|
42
48
|
});
|
|
43
49
|
});
|
|
44
50
|
|
|
45
|
-
it('
|
|
46
|
-
setConfig(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
it('rejects ids for single-instance namespaces before touching any backend', async () => {
|
|
52
|
+
setConfig(
|
|
53
|
+
defineConfig({
|
|
54
|
+
databases: {
|
|
55
|
+
shared: {
|
|
56
|
+
tables: {
|
|
57
|
+
posts: { schema: { title: { type: 'string' } } },
|
|
58
|
+
},
|
|
52
59
|
},
|
|
53
60
|
},
|
|
61
|
+
serviceKeys: {
|
|
62
|
+
keys: [
|
|
63
|
+
{
|
|
64
|
+
kid: 'root',
|
|
65
|
+
tier: 'root',
|
|
66
|
+
scopes: ['*'],
|
|
67
|
+
secretSource: 'inline',
|
|
68
|
+
inlineSecret: 'sk-root',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const env = {
|
|
76
|
+
DATABASE: {
|
|
77
|
+
idFromName: vi.fn(),
|
|
78
|
+
get: vi.fn(),
|
|
54
79
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
kid: 'root',
|
|
59
|
-
tier: 'root',
|
|
60
|
-
scopes: ['*'],
|
|
61
|
-
secretSource: 'inline',
|
|
62
|
-
inlineSecret: 'sk-root',
|
|
63
|
-
},
|
|
64
|
-
],
|
|
80
|
+
DB_D1_SHARED: {
|
|
81
|
+
prepare: vi.fn(),
|
|
65
82
|
},
|
|
66
|
-
}
|
|
83
|
+
} as unknown as Env;
|
|
84
|
+
|
|
85
|
+
const app = createApp();
|
|
86
|
+
const response = await app.request(
|
|
87
|
+
'/api/sql',
|
|
88
|
+
{
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
'X-EdgeBase-Service-Key': 'sk-root',
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
namespace: 'shared',
|
|
96
|
+
id: 'shadow',
|
|
97
|
+
sql: 'SELECT 1',
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
env,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(response.status).toBe(400);
|
|
104
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
105
|
+
code: 400,
|
|
106
|
+
message: "id is not allowed for single-instance namespace 'shared'",
|
|
107
|
+
});
|
|
108
|
+
expect(
|
|
109
|
+
(env as unknown as { DATABASE: { get: ReturnType<typeof vi.fn> } }).DATABASE.get,
|
|
110
|
+
).not.toHaveBeenCalled();
|
|
111
|
+
expect(
|
|
112
|
+
(env as unknown as { DB_D1_SHARED: { prepare: ReturnType<typeof vi.fn> } }).DB_D1_SHARED
|
|
113
|
+
.prepare,
|
|
114
|
+
).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('retries dynamic DO SQL after create handshake and forwards the DO name', async () => {
|
|
118
|
+
setConfig(
|
|
119
|
+
defineConfig({
|
|
120
|
+
databases: {
|
|
121
|
+
workspace: {
|
|
122
|
+
instance: true,
|
|
123
|
+
tables: {
|
|
124
|
+
members: { schema: { userId: { type: 'string' } } },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
serviceKeys: {
|
|
129
|
+
keys: [
|
|
130
|
+
{
|
|
131
|
+
kid: 'root',
|
|
132
|
+
tier: 'root',
|
|
133
|
+
scopes: ['*'],
|
|
134
|
+
secretSource: 'inline',
|
|
135
|
+
inlineSecret: 'sk-root',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
67
141
|
|
|
68
142
|
const stub = {
|
|
69
|
-
fetch: vi
|
|
143
|
+
fetch: vi
|
|
144
|
+
.fn()
|
|
70
145
|
.mockResolvedValueOnce(
|
|
71
146
|
new Response(JSON.stringify({ needsCreate: true, namespace: 'workspace', id: 'ws-1' }), {
|
|
72
147
|
status: 201,
|
|
@@ -88,19 +163,23 @@ describe('sql route', () => {
|
|
|
88
163
|
} as unknown as Env;
|
|
89
164
|
|
|
90
165
|
const app = createApp();
|
|
91
|
-
const response = await app.request(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
166
|
+
const response = await app.request(
|
|
167
|
+
'/api/sql',
|
|
168
|
+
{
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
'X-EdgeBase-Service-Key': 'sk-root',
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
namespace: 'workspace',
|
|
176
|
+
id: 'ws-1',
|
|
177
|
+
sql: 'SELECT COUNT(*) AS total FROM members',
|
|
178
|
+
params: [],
|
|
179
|
+
}),
|
|
96
180
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
id: 'ws-1',
|
|
100
|
-
sql: 'SELECT COUNT(*) AS total FROM members',
|
|
101
|
-
params: [],
|
|
102
|
-
}),
|
|
103
|
-
}, env);
|
|
181
|
+
env,
|
|
182
|
+
);
|
|
104
183
|
|
|
105
184
|
expect(response.status).toBe(200);
|
|
106
185
|
await expect(response.json()).resolves.toMatchObject({
|
|
@@ -122,26 +201,28 @@ describe('sql route', () => {
|
|
|
122
201
|
});
|
|
123
202
|
|
|
124
203
|
it('uses D1 run() for non-SELECT SQL so schema mutations actually execute', async () => {
|
|
125
|
-
setConfig(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
204
|
+
setConfig(
|
|
205
|
+
defineConfig({
|
|
206
|
+
databases: {
|
|
207
|
+
shared: {
|
|
208
|
+
tables: {
|
|
209
|
+
posts: { schema: { title: { type: 'string' } } },
|
|
210
|
+
},
|
|
130
211
|
},
|
|
131
212
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
},
|
|
144
|
-
|
|
213
|
+
serviceKeys: {
|
|
214
|
+
keys: [
|
|
215
|
+
{
|
|
216
|
+
kid: 'root',
|
|
217
|
+
tier: 'root',
|
|
218
|
+
scopes: ['*'],
|
|
219
|
+
secretSource: 'inline',
|
|
220
|
+
inlineSecret: 'sk-root',
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
145
226
|
|
|
146
227
|
const stmt: {
|
|
147
228
|
bind: ReturnType<typeof vi.fn>;
|
|
@@ -159,33 +240,129 @@ describe('sql route', () => {
|
|
|
159
240
|
} as unknown as Env;
|
|
160
241
|
|
|
161
242
|
const app = createApp();
|
|
162
|
-
const response = await app.request(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
243
|
+
const response = await app.request(
|
|
244
|
+
'/api/sql',
|
|
245
|
+
{
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: {
|
|
248
|
+
'Content-Type': 'application/json',
|
|
249
|
+
'X-EdgeBase-Service-Key': 'sk-root',
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
namespace: 'shared',
|
|
253
|
+
sql: 'ALTER TABLE "posts" RENAME TO "articles"',
|
|
254
|
+
}),
|
|
167
255
|
},
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
sql: 'ALTER TABLE "posts" RENAME TO "articles"',
|
|
171
|
-
}),
|
|
172
|
-
}, env);
|
|
256
|
+
env,
|
|
257
|
+
);
|
|
173
258
|
|
|
174
259
|
expect(response.status).toBe(200);
|
|
175
260
|
await expect(response.json()).resolves.toMatchObject({
|
|
176
261
|
rows: [],
|
|
177
262
|
rowCount: 0,
|
|
178
263
|
});
|
|
179
|
-
expect(
|
|
180
|
-
|
|
181
|
-
|
|
264
|
+
expect(
|
|
265
|
+
(env as unknown as { DB_D1_SHARED: { prepare: ReturnType<typeof vi.fn> } }).DB_D1_SHARED
|
|
266
|
+
.prepare,
|
|
267
|
+
).toHaveBeenCalledWith('ALTER TABLE "posts" RENAME TO "articles"');
|
|
182
268
|
expect(stmt.run).toHaveBeenCalledTimes(1);
|
|
183
269
|
expect(stmt.all).not.toHaveBeenCalled();
|
|
184
270
|
});
|
|
185
271
|
|
|
272
|
+
it.each(['postgres', 'neon'] as const)(
|
|
273
|
+
'routes %s raw SQL through the provider-aware executor and normalizes ? placeholders',
|
|
274
|
+
async (provider) => {
|
|
275
|
+
const fetchMock = vi
|
|
276
|
+
.fn()
|
|
277
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }))
|
|
278
|
+
.mockResolvedValueOnce(
|
|
279
|
+
new Response(
|
|
280
|
+
JSON.stringify({
|
|
281
|
+
columns: ['literal', 'total'],
|
|
282
|
+
rows: [{ literal: '?', total: 3 }],
|
|
283
|
+
rowCount: 1,
|
|
284
|
+
}),
|
|
285
|
+
{
|
|
286
|
+
status: 200,
|
|
287
|
+
headers: { 'Content-Type': 'application/json' },
|
|
288
|
+
},
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
292
|
+
|
|
293
|
+
setConfig(
|
|
294
|
+
defineConfig({
|
|
295
|
+
databases: {
|
|
296
|
+
shared: {
|
|
297
|
+
provider,
|
|
298
|
+
tables: {
|
|
299
|
+
posts: { schema: { title: { type: 'string' } } },
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
serviceKeys: {
|
|
304
|
+
keys: [
|
|
305
|
+
{
|
|
306
|
+
kid: 'root',
|
|
307
|
+
tier: 'root',
|
|
308
|
+
scopes: ['*'],
|
|
309
|
+
secretSource: 'inline',
|
|
310
|
+
inlineSecret: 'sk-root',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const env = {
|
|
318
|
+
DATABASE: {
|
|
319
|
+
idFromName: vi.fn().mockReturnValue('do-id'),
|
|
320
|
+
get: vi.fn(),
|
|
321
|
+
},
|
|
322
|
+
EDGEBASE_DEV_SIDECAR_PORT: '8788',
|
|
323
|
+
JWT_ADMIN_SECRET: 'jwt-secret',
|
|
324
|
+
DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
|
|
325
|
+
} as unknown as Env;
|
|
326
|
+
|
|
327
|
+
const app = createApp();
|
|
328
|
+
const response = await app.request(
|
|
329
|
+
'/api/sql',
|
|
330
|
+
{
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: {
|
|
333
|
+
'Content-Type': 'application/json',
|
|
334
|
+
'X-EdgeBase-Service-Key': 'sk-root',
|
|
335
|
+
},
|
|
336
|
+
body: JSON.stringify({
|
|
337
|
+
namespace: 'shared',
|
|
338
|
+
sql: "SELECT '?' AS literal, COUNT(*) AS total FROM posts WHERE title = ?",
|
|
339
|
+
params: ['owner'],
|
|
340
|
+
}),
|
|
341
|
+
},
|
|
342
|
+
env,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(response.status).toBe(200);
|
|
346
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
347
|
+
rows: [{ literal: '?', total: 3 }],
|
|
348
|
+
rowCount: 1,
|
|
349
|
+
});
|
|
350
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
351
|
+
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body ?? '{}'))).toEqual({
|
|
352
|
+
namespace: 'shared',
|
|
353
|
+
sql: "SELECT '?' AS literal, COUNT(*) AS total FROM posts WHERE title = $1",
|
|
354
|
+
params: ['owner'],
|
|
355
|
+
});
|
|
356
|
+
expect(
|
|
357
|
+
(env as unknown as { DATABASE: { get: ReturnType<typeof vi.fn> } }).DATABASE.get,
|
|
358
|
+
).not.toHaveBeenCalled();
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
|
|
186
362
|
it('executeDoSql retries the create handshake before returning rows', async () => {
|
|
187
363
|
const stub = {
|
|
188
|
-
fetch: vi
|
|
364
|
+
fetch: vi
|
|
365
|
+
.fn()
|
|
189
366
|
.mockResolvedValueOnce(
|
|
190
367
|
new Response(JSON.stringify({ needsCreate: true, namespace: 'workspace', id: 'ws-2' }), {
|
|
191
368
|
status: 201,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { Env } from '../types.js';
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
ensureAuthSchemaMock,
|
|
6
|
+
resolveAuthDbMock,
|
|
7
|
+
sendToDatabaseLiveDOMock,
|
|
8
|
+
createPushProviderMock,
|
|
9
|
+
getDevicesForUserMock,
|
|
10
|
+
providerSendMock,
|
|
11
|
+
} = vi.hoisted(() => ({
|
|
12
|
+
ensureAuthSchemaMock: vi.fn(),
|
|
13
|
+
resolveAuthDbMock: vi.fn(),
|
|
14
|
+
sendToDatabaseLiveDOMock: vi.fn(),
|
|
15
|
+
createPushProviderMock: vi.fn(),
|
|
16
|
+
getDevicesForUserMock: vi.fn(),
|
|
17
|
+
providerSendMock: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('../lib/auth-d1.js', () => ({
|
|
21
|
+
ensureAuthSchema: ensureAuthSchemaMock,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('../lib/auth-db-adapter.js', () => ({
|
|
25
|
+
resolveAuthDb: resolveAuthDbMock,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('../lib/database-live-emitter.js', () => ({
|
|
29
|
+
sendToDatabaseLiveDO: sendToDatabaseLiveDOMock,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('../lib/push-provider.js', () => ({
|
|
33
|
+
createPushProvider: createPushProviderMock,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('../lib/push-token.js', () => ({
|
|
37
|
+
getDevicesForUser: getDevicesForUserMock,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
describe('buildTableHookRuntimeServices', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.resetModules();
|
|
43
|
+
ensureAuthSchemaMock.mockReset().mockResolvedValue(undefined);
|
|
44
|
+
resolveAuthDbMock.mockReset().mockReturnValue({ kind: 'auth-db' });
|
|
45
|
+
sendToDatabaseLiveDOMock.mockReset().mockResolvedValue(undefined);
|
|
46
|
+
createPushProviderMock.mockReset().mockReturnValue({ send: providerSendMock });
|
|
47
|
+
getDevicesForUserMock.mockReset().mockResolvedValue([]);
|
|
48
|
+
providerSendMock.mockReset().mockResolvedValue({ success: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('broadcasts hook events through DatabaseLiveDO', async () => {
|
|
52
|
+
const { buildTableHookRuntimeServices } = await import('../lib/table-hook-runtime.js');
|
|
53
|
+
const env = {
|
|
54
|
+
DATABASE_LIVE: {} as DurableObjectNamespace,
|
|
55
|
+
} as Env;
|
|
56
|
+
|
|
57
|
+
const services = buildTableHookRuntimeServices({} as never, env);
|
|
58
|
+
await services.databaseLive.broadcast('posts', 'created', { id: 'post-1' });
|
|
59
|
+
|
|
60
|
+
expect(sendToDatabaseLiveDOMock).toHaveBeenCalledWith(
|
|
61
|
+
env,
|
|
62
|
+
{
|
|
63
|
+
channel: 'posts',
|
|
64
|
+
event: 'created',
|
|
65
|
+
payload: { id: 'post-1' },
|
|
66
|
+
},
|
|
67
|
+
'/internal/broadcast',
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('sends push notifications using auth-backed device lookups when available', async () => {
|
|
72
|
+
const { buildTableHookRuntimeServices } = await import('../lib/table-hook-runtime.js');
|
|
73
|
+
const authDb = { kind: 'auth-db' };
|
|
74
|
+
resolveAuthDbMock.mockReturnValue(authDb);
|
|
75
|
+
getDevicesForUserMock.mockResolvedValue([
|
|
76
|
+
{ token: 'token-1', platform: 'ios' },
|
|
77
|
+
{ token: 'token-2', platform: 'android' },
|
|
78
|
+
]);
|
|
79
|
+
const env = {
|
|
80
|
+
KV: {} as KVNamespace,
|
|
81
|
+
AUTH_DB: {} as D1Database,
|
|
82
|
+
} as Env;
|
|
83
|
+
|
|
84
|
+
const pushConfig = {
|
|
85
|
+
fcm: {
|
|
86
|
+
projectId: 'demo-project',
|
|
87
|
+
serviceAccount: '{"client_email":"demo@example.com"}',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const services = buildTableHookRuntimeServices({ push: pushConfig } as never, env);
|
|
91
|
+
await services.push.send('user-123', { title: 'Hello', body: 'World' });
|
|
92
|
+
|
|
93
|
+
expect(resolveAuthDbMock).toHaveBeenCalledWith(env);
|
|
94
|
+
expect(ensureAuthSchemaMock).toHaveBeenCalledWith(authDb);
|
|
95
|
+
expect(createPushProviderMock).toHaveBeenCalledWith(pushConfig, env);
|
|
96
|
+
expect(getDevicesForUserMock).toHaveBeenCalledWith({ kv: env.KV, authDb }, 'user-123');
|
|
97
|
+
expect(providerSendMock).toHaveBeenCalledTimes(2);
|
|
98
|
+
expect(providerSendMock).toHaveBeenNthCalledWith(1, {
|
|
99
|
+
token: 'token-1',
|
|
100
|
+
platform: 'ios',
|
|
101
|
+
payload: { title: 'Hello', body: 'World' },
|
|
102
|
+
});
|
|
103
|
+
expect(providerSendMock).toHaveBeenNthCalledWith(2, {
|
|
104
|
+
token: 'token-2',
|
|
105
|
+
platform: 'android',
|
|
106
|
+
payload: { title: 'Hello', body: 'World' },
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('falls back to KV-only token lookups when auth db resolution fails', async () => {
|
|
111
|
+
const { buildTableHookRuntimeServices } = await import('../lib/table-hook-runtime.js');
|
|
112
|
+
resolveAuthDbMock.mockImplementation(() => {
|
|
113
|
+
throw new Error('missing auth db');
|
|
114
|
+
});
|
|
115
|
+
getDevicesForUserMock.mockResolvedValue([{ token: 'token-1', platform: 'web' }]);
|
|
116
|
+
const env = {
|
|
117
|
+
KV: {} as KVNamespace,
|
|
118
|
+
} as Env;
|
|
119
|
+
|
|
120
|
+
const pushConfig = {
|
|
121
|
+
fcm: {
|
|
122
|
+
projectId: 'demo-project',
|
|
123
|
+
serviceAccount: '{"client_email":"demo@example.com"}',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
const services = buildTableHookRuntimeServices({ push: pushConfig } as never, env);
|
|
127
|
+
await services.push.send('user-123', { body: 'Fallback works' });
|
|
128
|
+
|
|
129
|
+
expect(ensureAuthSchemaMock).not.toHaveBeenCalled();
|
|
130
|
+
expect(getDevicesForUserMock).toHaveBeenCalledWith(env.KV, 'user-123');
|
|
131
|
+
expect(providerSendMock).toHaveBeenCalledWith({
|
|
132
|
+
token: 'token-1',
|
|
133
|
+
platform: 'web',
|
|
134
|
+
payload: { body: 'Fallback works' },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|