@edge-base/server 0.1.5 → 0.2.1
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/assets/19.4Si2ZFC_.css +1 -0
- package/admin-build/_app/immutable/assets/{3.Dg81Pgmd.css → 3.BtHYobTg.css} +1 -1
- package/admin-build/_app/immutable/assets/SqlEditor.Bbp1RIk0.css +1 -0
- package/admin-build/_app/immutable/assets/TableSqlTab.yeNZfhgG.css +1 -0
- package/admin-build/_app/immutable/chunks/B0QyxC2M.js +128 -0
- package/admin-build/_app/immutable/chunks/{Bsp3uE8m.js → BCKr7yKd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CWHVkYdi.js → BFs_qStz.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CKIubXVC.js → BTJcQFEp.js} +1 -1
- package/admin-build/_app/immutable/chunks/BY07qVPA.js +1 -0
- package/admin-build/_app/immutable/chunks/{Cn5ZQY9O.js → BcIUK2sk.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CKFIU4S8.js → BsFiK_FJ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{COYZ6F_d.js → CSGrwS7E.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DLc6L4xD.js → CqUxCvs_.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DLTo2HQr.js → D-x55wdW.js} +1 -1
- package/admin-build/_app/immutable/chunks/D755Tqat.js +1 -0
- package/admin-build/_app/immutable/chunks/{Jxx0jGlP.js → DjOEv9M9.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DLRcaFHo.js → DnLqc9L1.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BnlxEqP4.js → Dqk2TGNU.js} +1 -1
- package/admin-build/_app/immutable/chunks/{7FYfV8UU.js → k0CIJkw4.js} +1 -1
- package/admin-build/_app/immutable/chunks/lSpxLU5p.js +2 -0
- package/admin-build/_app/immutable/chunks/m9QZTyVV.js +1 -0
- package/admin-build/_app/immutable/entry/{app.C4kLStKR.js → app.BTsq3_xq.js} +2 -2
- package/admin-build/_app/immutable/entry/start.zXCirpgY.js +1 -0
- package/admin-build/_app/immutable/nodes/0.BZ00WDYH.js +1 -0
- package/admin-build/_app/immutable/nodes/{1.s1kW8gyv.js → 1.RzSJ3yyr.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.Cr9ml-GD.js → 10.D-rsiquF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.DXTOyMEn.js → 11.l7-bgtFD.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.DzhitECA.js → 12.Dkq0H7B5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.U_1VNA5x.js → 13.DtK_4oRz.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.x0SzmV0P.js → 14.BKo7-AMx.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.BJI1Z1hk.js → 15.CQAj_6lq.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.CUvRFqLW.js → 16.XVIG-Ffr.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.sDFD-Vbm.js → 17.g6raZLCM.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.DdoTnAXc.js → 18.IQz6a3T6.js} +1 -1
- package/admin-build/_app/immutable/nodes/19.CAAZ8i8h.js +2 -0
- package/admin-build/_app/immutable/nodes/{20.fxudyPKp.js → 20.BPcX3KPj.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.DoPabrY_.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.CY6ICoyn.js → 22.Br5AG_5Z.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.DmEaHsZw.js → 23.KjbrdXoE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.Cqerdln2.js → 24.C3n2-hgw.js} +1 -1
- package/admin-build/_app/immutable/nodes/25.SFDSBzHd.js +2 -0
- package/admin-build/_app/immutable/nodes/26.D95vui6E.js +1 -0
- package/admin-build/_app/immutable/nodes/{27.DAeKhnG9.js → 27.FgLgdjwB.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.D_lJZpNR.js → 28.B9sYYm1F.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.Dx7iQCpT.js → 29.DyqZ_wbN.js} +1 -1
- package/admin-build/_app/immutable/nodes/3.Bzo2yVIO.js +2 -0
- package/admin-build/_app/immutable/nodes/{30.DcO-bjQZ.js → 30.c1CiNwiS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.DesWAzIF.js → 31.CXty66Vh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.B2rm4_4a.js → 4.BgQaXZ27.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.4Os1aYeW.js → 5.BuJrHvxH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BzR--5Dj.js → 6.CkBBC94k.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.QDC-47H1.js → 7.D2YBvNFM.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.DDPWEc96.js → 8.D8qQWo_z.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.fyfVIJPa.js → 9.BLDLX5hV.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/openapi.json +6710 -5866
- package/package.json +2 -2
- package/src/__tests__/functions-route.test.ts +153 -0
- package/src/__tests__/internal-request.test.ts +5 -5
- package/src/__tests__/meta-export-coverage.test.ts +3 -2
- package/src/__tests__/meta-route-registration.test.ts +3 -2
- package/src/__tests__/openapi-coverage.test.ts +5 -1
- package/src/__tests__/pagination.test.ts +12 -8
- package/src/__tests__/postgres-dialect.test.ts +2 -2
- package/src/__tests__/query.test.ts +7 -7
- package/src/__tests__/rate-limit.test.ts +0 -1
- package/src/__tests__/room-handler-context.test.ts +31 -0
- package/src/__tests__/room-runtime-routing.test.ts +48 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +5 -4
- package/src/__tests__/smoke-skip-report.test.ts +3 -2
- package/src/durable-objects/database-do.ts +9 -4
- package/src/durable-objects/database-live-do.ts +22 -10
- package/src/durable-objects/logs-do.ts +2 -2
- package/src/durable-objects/rooms-do.ts +202 -0
- package/src/lib/auth-d1-service.ts +1 -1
- package/src/lib/auth-d1.ts +10 -0
- package/src/lib/d1-handler.ts +23 -4
- package/src/lib/internal-request.ts +5 -8
- package/src/lib/openapi.ts +1 -0
- package/src/lib/pagination.ts +3 -3
- package/src/lib/postgres-handler.ts +2 -2
- package/src/lib/query-engine.ts +2 -2
- package/src/middleware/rate-limit.ts +11 -11
- package/src/routes/admin.ts +30 -3
- package/src/routes/auth.ts +74 -33
- package/src/routes/room.ts +42 -0
- package/src/types.ts +6 -0
- package/admin-build/_app/immutable/assets/TableSqlTab.BHquaMBM.css +0 -1
- package/admin-build/_app/immutable/chunks/BPXFNSAT.js +0 -128
- package/admin-build/_app/immutable/chunks/DKjA1S1a.js +0 -1
- package/admin-build/_app/immutable/chunks/fBE8lw-R.js +0 -1
- package/admin-build/_app/immutable/chunks/gE9fQ_Ff.js +0 -2
- package/admin-build/_app/immutable/entry/start.CAAH6ztW.js +0 -1
- package/admin-build/_app/immutable/nodes/0.BywaJfpH.js +0 -1
- package/admin-build/_app/immutable/nodes/19.CUvRFqLW.js +0 -1
- package/admin-build/_app/immutable/nodes/21.N2QN0lbw.js +0 -1
- package/admin-build/_app/immutable/nodes/25.x3hL7Y-O.js +0 -2
- package/admin-build/_app/immutable/nodes/26.CEzfjTSO.js +0 -1
- package/admin-build/_app/immutable/nodes/3.CKC_yDnF.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edge-base/server",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "EdgeBase runtime assets consumed by the EdgeBase CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"jose": "^6.0.0",
|
|
35
35
|
"pg": "^8.16.3",
|
|
36
36
|
"zod": "^4.3.6",
|
|
37
|
-
"@edge-base/shared": "0.1
|
|
37
|
+
"@edge-base/shared": "0.2.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@cloudflare/vitest-pool-workers": "^0.8.71",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import type { FunctionDefinition } from '@edge-base/shared';
|
|
2
3
|
import { OpenAPIHono } from '../lib/hono.js';
|
|
3
4
|
import { setConfig } from '../lib/do-router.js';
|
|
4
5
|
import {
|
|
@@ -73,6 +74,29 @@ function createExecutionContext(): ExecutionContext {
|
|
|
73
74
|
} as unknown as ExecutionContext;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
function httpFunction(
|
|
78
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
79
|
+
handler: (ctx: Record<string, any>) => Promise<unknown> | unknown,
|
|
80
|
+
path?: string,
|
|
81
|
+
): FunctionDefinition {
|
|
82
|
+
return {
|
|
83
|
+
trigger: {
|
|
84
|
+
type: 'http' as const,
|
|
85
|
+
method,
|
|
86
|
+
...(path ? { path } : {}),
|
|
87
|
+
},
|
|
88
|
+
handler: async (ctx: unknown) => handler(ctx as Record<string, any>),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function invokeFunction(path: string, method = 'GET') {
|
|
93
|
+
return createApp().fetch(
|
|
94
|
+
new Request(`http://localhost/api/functions/${path}`, { method }),
|
|
95
|
+
createEnv(),
|
|
96
|
+
createExecutionContext(),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
afterEach(() => {
|
|
77
101
|
clearFunctionRegistry();
|
|
78
102
|
clearMiddlewareRegistry();
|
|
@@ -137,3 +161,132 @@ describe('functionsRoute FunctionError compatibility', () => {
|
|
|
137
161
|
});
|
|
138
162
|
});
|
|
139
163
|
});
|
|
164
|
+
|
|
165
|
+
describe('functionsRoute HTTP contracts', () => {
|
|
166
|
+
it('serializes plain objects as JSON responses', async () => {
|
|
167
|
+
registerFunction('reports/summary', httpFunction('GET', async () => ({
|
|
168
|
+
ok: true,
|
|
169
|
+
total: 3,
|
|
170
|
+
})));
|
|
171
|
+
rebuildCompiledRoutes();
|
|
172
|
+
|
|
173
|
+
const response = await invokeFunction('reports/summary');
|
|
174
|
+
|
|
175
|
+
expect(response.status).toBe(200);
|
|
176
|
+
expect(response.headers.get('content-type')).toContain('application/json');
|
|
177
|
+
await expect(response.json()).resolves.toEqual({ ok: true, total: 3 });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('returns text/plain when handler returns a string', async () => {
|
|
181
|
+
registerFunction('health', httpFunction('GET', async () => 'healthy'));
|
|
182
|
+
rebuildCompiledRoutes();
|
|
183
|
+
|
|
184
|
+
const response = await invokeFunction('health');
|
|
185
|
+
|
|
186
|
+
expect(response.status).toBe(200);
|
|
187
|
+
expect(response.headers.get('content-type')).toContain('text/plain');
|
|
188
|
+
await expect(response.text()).resolves.toBe('healthy');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('returns 204 when handler returns null', async () => {
|
|
192
|
+
registerFunction('empty', httpFunction('POST', async () => null));
|
|
193
|
+
rebuildCompiledRoutes();
|
|
194
|
+
|
|
195
|
+
const response = await invokeFunction('empty', 'POST');
|
|
196
|
+
|
|
197
|
+
expect(response.status).toBe(204);
|
|
198
|
+
await expect(response.text()).resolves.toBe('');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('passes through native Response objects untouched', async () => {
|
|
202
|
+
registerFunction('created', httpFunction('POST', async () => (
|
|
203
|
+
new Response('created-body', {
|
|
204
|
+
status: 201,
|
|
205
|
+
headers: { 'content-type': 'text/plain', 'x-fn': 'direct-response' },
|
|
206
|
+
})
|
|
207
|
+
)));
|
|
208
|
+
rebuildCompiledRoutes();
|
|
209
|
+
|
|
210
|
+
const response = await invokeFunction('created', 'POST');
|
|
211
|
+
|
|
212
|
+
expect(response.status).toBe(201);
|
|
213
|
+
expect(response.headers.get('x-fn')).toBe('direct-response');
|
|
214
|
+
await expect(response.text()).resolves.toBe('created-body');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('executes directory middleware before the handler', async () => {
|
|
218
|
+
const executionOrder: string[] = [];
|
|
219
|
+
registerMiddleware('secure', async () => {
|
|
220
|
+
executionOrder.push('middleware');
|
|
221
|
+
});
|
|
222
|
+
registerFunction('secure/audit', httpFunction('GET', async () => {
|
|
223
|
+
executionOrder.push('handler');
|
|
224
|
+
return { executionOrder };
|
|
225
|
+
}));
|
|
226
|
+
rebuildCompiledRoutes();
|
|
227
|
+
|
|
228
|
+
const response = await invokeFunction('secure/audit');
|
|
229
|
+
|
|
230
|
+
expect(response.status).toBe(200);
|
|
231
|
+
await expect(response.json()).resolves.toEqual({
|
|
232
|
+
executionOrder: ['middleware', 'handler'],
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('supports custom trigger.path params and preserves query strings', async () => {
|
|
237
|
+
registerFunction(
|
|
238
|
+
'shortlink/resolve',
|
|
239
|
+
httpFunction(
|
|
240
|
+
'GET',
|
|
241
|
+
async (ctx) => {
|
|
242
|
+
const requestUrl = new URL(ctx.request.url);
|
|
243
|
+
return {
|
|
244
|
+
code: ctx.params.code,
|
|
245
|
+
target: requestUrl.searchParams.get('target'),
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
'/s/:code',
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
rebuildCompiledRoutes();
|
|
252
|
+
|
|
253
|
+
const response = await createApp().fetch(
|
|
254
|
+
new Request('http://localhost/api/functions/s/abc123?target=docs', { method: 'GET' }),
|
|
255
|
+
createEnv(),
|
|
256
|
+
createExecutionContext(),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(response.status).toBe(200);
|
|
260
|
+
await expect(response.json()).resolves.toEqual({
|
|
261
|
+
code: 'abc123',
|
|
262
|
+
target: 'docs',
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('supports catch-all params at execution time', async () => {
|
|
267
|
+
registerFunction('docs/[...slug]', httpFunction('GET', async (ctx) => ({
|
|
268
|
+
slug: ctx.params.slug,
|
|
269
|
+
})));
|
|
270
|
+
rebuildCompiledRoutes();
|
|
271
|
+
|
|
272
|
+
const response = await invokeFunction('docs/guides/getting-started/install');
|
|
273
|
+
|
|
274
|
+
expect(response.status).toBe(200);
|
|
275
|
+
await expect(response.json()).resolves.toEqual({
|
|
276
|
+
slug: 'guides/getting-started/install',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('returns 405 with structured JSON when method does not match', async () => {
|
|
281
|
+
registerFunction('users', httpFunction('GET', async () => ({ ok: true })));
|
|
282
|
+
rebuildCompiledRoutes();
|
|
283
|
+
|
|
284
|
+
const response = await invokeFunction('users', 'POST');
|
|
285
|
+
|
|
286
|
+
expect(response.status).toBe(405);
|
|
287
|
+
await expect(response.json()).resolves.toEqual({
|
|
288
|
+
code: 405,
|
|
289
|
+
message: "Method POST not allowed for 'users'.",
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -21,16 +21,16 @@ function makeContext(
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
describe('internal request helpers', () => {
|
|
24
|
-
it('isTrustedInternalRequestUrl
|
|
25
|
-
expect(isTrustedInternalRequestUrl('http://internal/api/db/shared')).toBe(
|
|
26
|
-
expect(isTrustedInternalRequestUrl('http://do/internal/functions/reindex')).toBe(
|
|
24
|
+
it('isTrustedInternalRequestUrl never trusts hostnames', () => {
|
|
25
|
+
expect(isTrustedInternalRequestUrl('http://internal/api/db/shared')).toBe(false);
|
|
26
|
+
expect(isTrustedInternalRequestUrl('http://do/internal/functions/reindex')).toBe(false);
|
|
27
27
|
expect(isTrustedInternalRequestUrl('http://localhost:8787/api/db/shared')).toBe(false);
|
|
28
28
|
expect(isTrustedInternalRequestUrl('not-a-url')).toBe(false);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it('isTrustedInternalContext accepts explicit internal flags
|
|
31
|
+
it('isTrustedInternalContext only accepts explicit internal flags', () => {
|
|
32
32
|
expect(isTrustedInternalContext(makeContext('http://localhost/api/db/shared', true))).toBe(true);
|
|
33
|
-
expect(isTrustedInternalContext(makeContext('http://do/api/db/shared'))).toBe(
|
|
33
|
+
expect(isTrustedInternalContext(makeContext('http://do/api/db/shared'))).toBe(false);
|
|
34
34
|
expect(isTrustedInternalContext(makeContext('http://example.com/api/db/shared'))).toBe(false);
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readFileSync, readdirSync } from 'fs';
|
|
11
11
|
import { resolve } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
12
13
|
import { describe, it, expect } from 'vitest';
|
|
13
14
|
|
|
14
|
-
const LIB_DIR = resolve(new URL('../lib', import.meta.url)
|
|
15
|
-
const TEST_DIR = resolve(new URL('.', import.meta.url)
|
|
15
|
+
const LIB_DIR = resolve(fileURLToPath(new URL('../lib', import.meta.url)));
|
|
16
|
+
const TEST_DIR = resolve(fileURLToPath(new URL('.', import.meta.url)));
|
|
16
17
|
|
|
17
18
|
// ─── Lib files that do NOT yet have dedicated tests ─────────────────────────
|
|
18
19
|
// Remove entries as tests are added — each removal is a net win.
|
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync, readdirSync } from 'fs';
|
|
8
8
|
import { resolve } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
9
10
|
import { describe, it, expect } from 'vitest';
|
|
10
11
|
|
|
11
12
|
describe('index.ts route registration completeness', () => {
|
|
12
13
|
const source = readFileSync(
|
|
13
|
-
new URL('../index.ts', import.meta.url),
|
|
14
|
+
fileURLToPath(new URL('../index.ts', import.meta.url)),
|
|
14
15
|
'utf-8',
|
|
15
16
|
);
|
|
16
|
-
const routesDir = resolve(new URL('../routes', import.meta.url)
|
|
17
|
+
const routesDir = resolve(fileURLToPath(new URL('../routes', import.meta.url)));
|
|
17
18
|
const EXPECTED_ROUTES = readdirSync(routesDir)
|
|
18
19
|
.filter((fileName) => fileName.endsWith('.ts'))
|
|
19
20
|
.sort()
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { readFileSync, readdirSync } from 'fs';
|
|
14
14
|
import { resolve } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
15
16
|
import { describe, it, expect } from 'vitest';
|
|
16
17
|
import { normalizeOpenApiDocument, type OpenApiSpec } from '../lib/openapi.js';
|
|
17
18
|
|
|
18
|
-
const ROUTES_DIR = resolve(new URL('../routes', import.meta.url)
|
|
19
|
+
const ROUTES_DIR = resolve(fileURLToPath(new URL('../routes', import.meta.url)));
|
|
19
20
|
|
|
20
21
|
// ─── Intentionally non-OpenAPI route registrations ───────────────────────────
|
|
21
22
|
// Format: "filename.ts:<line-content-substring>"
|
|
@@ -118,6 +119,7 @@ describe('OpenAPI route coverage', () => {
|
|
|
118
119
|
'/admin/api/setup': { get: {} },
|
|
119
120
|
'/admin/api/data/users': { get: {} },
|
|
120
121
|
'/api/room/media/realtime/session': { post: {} },
|
|
122
|
+
'/api/room/media/cloudflare_realtimekit/session': { post: {} },
|
|
121
123
|
},
|
|
122
124
|
};
|
|
123
125
|
|
|
@@ -134,6 +136,8 @@ describe('OpenAPI route coverage', () => {
|
|
|
134
136
|
.toEqual([{ userBearerAuth: [] }]);
|
|
135
137
|
expect((normalized.paths?.['/api/room/media/realtime/session'] as Record<string, { security?: unknown }>).post.security)
|
|
136
138
|
.toEqual([{ userBearerAuth: [] }]);
|
|
139
|
+
expect((normalized.paths?.['/api/room/media/cloudflare_realtimekit/session'] as Record<string, { security?: unknown }>).post.security)
|
|
140
|
+
.toEqual([{ userBearerAuth: [] }]);
|
|
137
141
|
expect((normalized.paths?.['/api/sql'] as Record<string, { security?: unknown }>).post.security)
|
|
138
142
|
.toEqual([{ serviceKeyAuth: [] }]);
|
|
139
143
|
expect((normalized.paths?.['/admin/api/setup'] as Record<string, { security?: unknown }>).get.security)
|
|
@@ -10,7 +10,7 @@ import { parsePagination } from '../lib/pagination.js';
|
|
|
10
10
|
describe('parsePagination', () => {
|
|
11
11
|
// ── Defaults ──
|
|
12
12
|
it('returns defaults when no params provided', () => {
|
|
13
|
-
expect(parsePagination(undefined, undefined)).toEqual({ limit:
|
|
13
|
+
expect(parsePagination(undefined, undefined)).toEqual({ limit: 100, offset: 0 });
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
// ── Valid values ──
|
|
@@ -23,21 +23,25 @@ describe('parsePagination', () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
// ── Limit clamping ──
|
|
26
|
-
it('clamps limit to
|
|
27
|
-
expect(parsePagination('
|
|
26
|
+
it('clamps limit to 1000', () => {
|
|
27
|
+
expect(parsePagination('9999', '0')).toEqual({ limit: 1000, offset: 0 });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('accepts limit within range (500)', () => {
|
|
31
|
+
expect(parsePagination('500', '0')).toEqual({ limit: 500, offset: 0 });
|
|
28
32
|
});
|
|
29
33
|
|
|
30
34
|
it('rejects limit=0 (falls back to default)', () => {
|
|
31
|
-
expect(parsePagination('0', '0')).toEqual({ limit:
|
|
35
|
+
expect(parsePagination('0', '0')).toEqual({ limit: 100, offset: 0 });
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
// ── REGRESSION: negative and NaN values ──
|
|
35
39
|
it('rejects negative limit', () => {
|
|
36
|
-
expect(parsePagination('-5', '0')).toEqual({ limit:
|
|
40
|
+
expect(parsePagination('-5', '0')).toEqual({ limit: 100, offset: 0 });
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
it('rejects NaN limit', () => {
|
|
40
|
-
expect(parsePagination('abc', '0')).toEqual({ limit:
|
|
44
|
+
expect(parsePagination('abc', '0')).toEqual({ limit: 100, offset: 0 });
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
it('rejects negative offset', () => {
|
|
@@ -49,11 +53,11 @@ describe('parsePagination', () => {
|
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
it('rejects Infinity limit', () => {
|
|
52
|
-
expect(parsePagination('Infinity', '0')).toEqual({ limit:
|
|
56
|
+
expect(parsePagination('Infinity', '0')).toEqual({ limit: 100, offset: 0 });
|
|
53
57
|
});
|
|
54
58
|
|
|
55
59
|
// ── Empty string (same as undefined) ──
|
|
56
60
|
it('treats empty strings as defaults', () => {
|
|
57
|
-
expect(parsePagination('', '')).toEqual({ limit:
|
|
61
|
+
expect(parsePagination('', '')).toEqual({ limit: 100, offset: 0 });
|
|
58
62
|
});
|
|
59
63
|
});
|
|
@@ -43,7 +43,7 @@ describe('PostgreSQL dialect — bind params', () => {
|
|
|
43
43
|
const { sql, params } = buildListQuery('products', {}, 'postgres');
|
|
44
44
|
expect(sql).toContain('LIMIT $1');
|
|
45
45
|
expect(sql).not.toContain('?');
|
|
46
|
-
expect(params).toEqual([
|
|
46
|
+
expect(params).toEqual([100]);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it('buildListQuery with offset → $1 OFFSET $2', () => {
|
|
@@ -71,7 +71,7 @@ describe('PostgreSQL dialect — bind params', () => {
|
|
|
71
71
|
expect(sql).toContain('"status" = $1');
|
|
72
72
|
expect(sql).toContain('"price" > $2');
|
|
73
73
|
expect(sql).toContain('LIMIT $3');
|
|
74
|
-
expect(params).toEqual(['active', 100,
|
|
74
|
+
expect(params).toEqual(['active', 100, 100]);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
it('buildListQuery cursor after → $1 for cursor, $2 for limit', () => {
|
|
@@ -79,10 +79,10 @@ describe('buildListQuery — no filters', () => {
|
|
|
79
79
|
expect(sql).toContain('LIMIT');
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
it('default limit is
|
|
82
|
+
it('default limit is 100', () => {
|
|
83
83
|
const { params } = buildListQuery('posts', {});
|
|
84
|
-
// params should contain
|
|
85
|
-
expect(params).toContain(
|
|
84
|
+
// params should contain 100 as default limit
|
|
85
|
+
expect(params).toContain(100);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('generates countSql for non-cursor pagination', () => {
|
|
@@ -313,9 +313,9 @@ describe('buildSearchQuery', () => {
|
|
|
313
313
|
expect(sql).toContain('"posts_fts"');
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
-
it('default limit
|
|
316
|
+
it('default limit 100, offset 0', () => {
|
|
317
317
|
const { params } = buildSearchQuery('posts', 'q');
|
|
318
|
-
expect(params[1]).toBe(
|
|
318
|
+
expect(params[1]).toBe(100);
|
|
319
319
|
expect(params[2]).toBeUndefined();
|
|
320
320
|
});
|
|
321
321
|
|
|
@@ -560,7 +560,7 @@ describe('3-way sync: QUERY_PARAM_KEYS ↔ Zod queryParamsSchema', () => {
|
|
|
560
560
|
describe('buildListQuery — exact params verification', () => {
|
|
561
561
|
it('no filters → params contains only default limit', () => {
|
|
562
562
|
const { params } = buildListQuery('t', {});
|
|
563
|
-
expect(params).toEqual([
|
|
563
|
+
expect(params).toEqual([100]);
|
|
564
564
|
});
|
|
565
565
|
|
|
566
566
|
it('no filters → countParams is empty array', () => {
|
|
@@ -722,7 +722,7 @@ describe('buildSubstringSearchQuery', () => {
|
|
|
722
722
|
const { sql, params } = buildSubstringSearchQuery('posts', '준규', { fields: ['title', 'content'] });
|
|
723
723
|
expect(sql).toContain('instr(lower(CAST("title" AS TEXT)), lower(?)) > 0');
|
|
724
724
|
expect(sql).toContain('instr(lower(CAST("content" AS TEXT)), lower(?)) > 0');
|
|
725
|
-
expect(params).toEqual(['준규', '준규',
|
|
725
|
+
expect(params).toEqual(['준규', '준규', 100]);
|
|
726
726
|
});
|
|
727
727
|
|
|
728
728
|
it('passes the raw term through the SQLite instr() fallback', () => {
|
|
@@ -127,4 +127,35 @@ describe('RoomsDO handler context', () => {
|
|
|
127
127
|
}),
|
|
128
128
|
);
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
it('returns 409 when creating a Cloudflare RealtimeKit session while media is already published', async () => {
|
|
132
|
+
const { RoomsDO } = await import('../durable-objects/rooms-do.js');
|
|
133
|
+
|
|
134
|
+
const room: any = Object.create(RoomsDO.prototype);
|
|
135
|
+
room.readJsonBody = vi.fn().mockResolvedValue({ connectionId: 'conn-1' });
|
|
136
|
+
room.authenticateRealtimeRequest = vi.fn().mockResolvedValue({
|
|
137
|
+
memberId: 'member-1',
|
|
138
|
+
connectionId: 'conn-1',
|
|
139
|
+
meta: {
|
|
140
|
+
authenticated: true,
|
|
141
|
+
connectionId: 'conn-1',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
room.hasPublishedTracks = vi.fn().mockReturnValue(true);
|
|
145
|
+
|
|
146
|
+
const response = await room.handleCloudflareRealtimeKitSessionCreate(
|
|
147
|
+
new Request('http://do/media/cloudflare_realtimekit/session?room=game::room-1', {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
body: JSON.stringify({ connectionId: 'conn-1' }),
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
}),
|
|
152
|
+
new URL('http://do/media/cloudflare_realtimekit/session?room=game::room-1'),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(response.status).toBe(409);
|
|
156
|
+
await expect(response.json()).resolves.toEqual({
|
|
157
|
+
code: 409,
|
|
158
|
+
message: 'Unpublish existing room media before creating a new Cloudflare RealtimeKit session',
|
|
159
|
+
});
|
|
160
|
+
});
|
|
130
161
|
});
|
|
@@ -219,4 +219,52 @@ describe('room route runtime routing', () => {
|
|
|
219
219
|
expect(doFetch).toHaveBeenCalledTimes(1);
|
|
220
220
|
expect(new URL((doFetch.mock.calls[0][0] as Request).url).searchParams.get('room')).toBe('game::room-1');
|
|
221
221
|
});
|
|
222
|
+
|
|
223
|
+
it('routes room cloudflare realtimekit session requests to the rooms runtime', async () => {
|
|
224
|
+
setConfig(defineConfig({
|
|
225
|
+
rooms: {
|
|
226
|
+
game: {
|
|
227
|
+
runtime: {
|
|
228
|
+
target: 'rooms',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}));
|
|
233
|
+
|
|
234
|
+
const doFetch = vi.fn(async (request: Request) => new Response(JSON.stringify({
|
|
235
|
+
runtime: 'rooms',
|
|
236
|
+
path: new URL(request.url).pathname,
|
|
237
|
+
auth: request.headers.get('Authorization'),
|
|
238
|
+
body: await request.clone().json(),
|
|
239
|
+
}), {
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
status: 201,
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const env = createRoomRuntimeEnv();
|
|
245
|
+
env.ROOMS = {
|
|
246
|
+
idFromName: (name: string) => name as unknown as DurableObjectId,
|
|
247
|
+
get: () => ({ fetch: doFetch }),
|
|
248
|
+
} as unknown as DurableObjectNamespace;
|
|
249
|
+
|
|
250
|
+
const app = createAuthedRoomApp();
|
|
251
|
+
const response = await app.request('/api/room/media/cloudflare_realtimekit/session?namespace=game&id=room-1', {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: {
|
|
254
|
+
Authorization: 'Bearer room-token',
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
},
|
|
257
|
+
body: JSON.stringify({ connectionId: 'conn-1' }),
|
|
258
|
+
}, env);
|
|
259
|
+
|
|
260
|
+
expect(response.status).toBe(201);
|
|
261
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
262
|
+
runtime: 'rooms',
|
|
263
|
+
path: '/media/cloudflare_realtimekit/session',
|
|
264
|
+
auth: 'Bearer room-token',
|
|
265
|
+
body: { connectionId: 'conn-1' },
|
|
266
|
+
});
|
|
267
|
+
expect(doFetch).toHaveBeenCalledTimes(1);
|
|
268
|
+
expect(new URL((doFetch.mock.calls[0][0] as Request).url).searchParams.get('room')).toBe('game::room-1');
|
|
269
|
+
});
|
|
222
270
|
});
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* indirectly covered.
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync, readdirSync } from 'fs';
|
|
10
|
-
import { resolve } from 'path';
|
|
10
|
+
import { basename, relative, resolve } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
11
12
|
import { describe, expect, it } from 'vitest';
|
|
12
13
|
|
|
13
|
-
const SRC_ROOT = resolve(new URL('..', import.meta.url)
|
|
14
|
+
const SRC_ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
14
15
|
const PACKAGE_ROOT = resolve(SRC_ROOT, '..');
|
|
15
16
|
const RUNTIME_DIRS = [
|
|
16
17
|
resolve(SRC_ROOT, 'routes'),
|
|
@@ -21,7 +22,7 @@ const TEST_DIRS = [
|
|
|
21
22
|
resolve(SRC_ROOT, '__tests__'),
|
|
22
23
|
resolve(PACKAGE_ROOT, 'test/integration'),
|
|
23
24
|
];
|
|
24
|
-
const THIS_TEST_PATH = resolve(
|
|
25
|
+
const THIS_TEST_PATH = resolve(fileURLToPath(import.meta.url));
|
|
25
26
|
|
|
26
27
|
// Files below are exercised indirectly, but the test sources do not mention the
|
|
27
28
|
// exact filename/stem yet. Tracking them here keeps the gap reviewable.
|
|
@@ -60,7 +61,7 @@ function collectFiles(dir: string, predicate: (path: string) => boolean): string
|
|
|
60
61
|
function toRuntimeKey(absPath: string): string {
|
|
61
62
|
for (const dir of RUNTIME_DIRS) {
|
|
62
63
|
if (absPath.startsWith(dir)) {
|
|
63
|
-
return
|
|
64
|
+
return `${basename(dir)}/${relative(dir, absPath).replace(/\\/g, '/')}`;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
throw new Error(`Unexpected runtime path: ${absPath}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
|
|
5
6
|
interface SmokeSkipEntry {
|
|
@@ -18,7 +19,7 @@ interface SmokeSkipReport {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const REPORT_PATH = resolve(
|
|
21
|
-
new URL('../../test/integration/generated/smoke-skip-report.json', import.meta.url)
|
|
22
|
+
fileURLToPath(new URL('../../test/integration/generated/smoke-skip-report.json', import.meta.url)),
|
|
22
23
|
);
|
|
23
24
|
|
|
24
25
|
function readReport(): SmokeSkipReport {
|
|
@@ -37,7 +38,7 @@ describe('smoke skip report', () => {
|
|
|
37
38
|
{
|
|
38
39
|
"skippedRouteCount": 0,
|
|
39
40
|
"summaryByReason": {},
|
|
40
|
-
"totalRoutes":
|
|
41
|
+
"totalRoutes": 197,
|
|
41
42
|
}
|
|
42
43
|
`);
|
|
43
44
|
});
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
type FilterTuple,
|
|
49
49
|
} from '../lib/query-engine.js';
|
|
50
50
|
import { summarizeValidationErrors, validateInsert, validateUpdate } from '../lib/validation.js';
|
|
51
|
-
import { hookRejectedError, validationError, notFoundError } from '../lib/errors.js';
|
|
51
|
+
import { hookRejectedError, validationError, notFoundError, normalizeDatabaseError } from '../lib/errors.js';
|
|
52
52
|
import {
|
|
53
53
|
executeDbTriggers,
|
|
54
54
|
getRegisteredFunctions,
|
|
@@ -470,7 +470,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
|
|
|
470
470
|
if (countSql && countParams) {
|
|
471
471
|
const countResult = [...this.sql(countSql, ...countParams)];
|
|
472
472
|
const total = (countResult[0]?.total as number) ?? 0;
|
|
473
|
-
const perPage = options.pagination?.perPage ?? options.pagination?.limit ??
|
|
473
|
+
const perPage = options.pagination?.perPage ?? options.pagination?.limit ?? 100;
|
|
474
474
|
response.total = total;
|
|
475
475
|
response.page = options.pagination?.page ?? 1;
|
|
476
476
|
response.perPage = perPage;
|
|
@@ -478,7 +478,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
|
|
|
478
478
|
|
|
479
479
|
// Cursor pagination: always include cursor and hasMore when items exist
|
|
480
480
|
// so clients can start cursor-based pagination from any page (including the first)
|
|
481
|
-
const limit = options.pagination?.limit ?? options.pagination?.perPage ??
|
|
481
|
+
const limit = options.pagination?.limit ?? options.pagination?.perPage ?? 100;
|
|
482
482
|
const hasMore = normalizedRows.length === limit;
|
|
483
483
|
response.hasMore = hasMore;
|
|
484
484
|
if (normalizedRows.length > 0) {
|
|
@@ -516,7 +516,7 @@ export class DatabaseDO extends DurableObject<DOEnv> {
|
|
|
516
516
|
return c.json({ items: [] });
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
-
const limit = options.pagination?.limit ?? options.pagination?.perPage ??
|
|
519
|
+
const limit = options.pagination?.limit ?? options.pagination?.perPage ?? 100;
|
|
520
520
|
const offset = options.pagination?.offset ?? ((options.pagination?.page ?? 1) - 1) * limit;
|
|
521
521
|
|
|
522
522
|
const tableConfig = this.getTableConfig(name);
|
|
@@ -1936,6 +1936,11 @@ export class DatabaseDO extends DurableObject<DOEnv> {
|
|
|
1936
1936
|
const e = err as { code: number; message: string; data?: unknown };
|
|
1937
1937
|
return c.json({ code: e.code, message: e.message, data: e.data }, e.code as 200);
|
|
1938
1938
|
}
|
|
1939
|
+
// Normalize well-known database errors (e.g. UNIQUE constraint violations → 409)
|
|
1940
|
+
const normalizedDbError = normalizeDatabaseError(err);
|
|
1941
|
+
if (normalizedDbError) {
|
|
1942
|
+
return c.json(normalizedDbError.toJSON(), normalizedDbError.code as 400);
|
|
1943
|
+
}
|
|
1939
1944
|
console.error('DatabaseDO Error:', err);
|
|
1940
1945
|
return c.json({ code: 500, message: 'Internal server error.' }, 500);
|
|
1941
1946
|
});
|