@edge-base/server 0.1.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/.gitkeep +0 -0
- package/admin-build/_app/env.js +1 -0
- package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
- package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
- package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
- package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
- package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
- package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
- package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
- package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
- package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
- package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
- package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
- package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
- package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
- package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
- package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
- package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
- package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
- package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
- package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
- package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
- package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
- package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
- package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
- package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
- package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
- package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
- package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
- package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
- package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
- package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
- package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
- package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
- package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
- package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
- package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
- package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
- package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
- package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
- package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
- package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
- package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
- package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
- package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
- package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
- package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
- package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
- package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
- package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
- package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
- package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
- package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
- package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
- package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
- package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
- package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
- package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
- package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
- package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
- package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
- package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
- package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
- package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
- package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
- package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
- package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
- package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
- package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
- package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
- package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
- package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
- package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
- package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
- package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
- package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
- package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
- package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
- package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
- package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
- package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
- package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
- package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
- package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
- package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
- package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
- package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
- package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
- package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
- package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
- package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
- package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
- package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
- package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
- package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
- package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
- package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
- package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
- package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
- package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
- package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
- package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
- package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
- package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
- package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
- package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
- package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
- package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
- package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
- package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
- package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
- package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
- package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
- package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
- package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
- package/admin-build/_app/version.json +1 -0
- package/admin-build/favicon.svg +26 -0
- package/admin-build/index.html +45 -0
- package/openapi.json +19543 -0
- package/package.json +66 -0
- package/src/__tests__/admin-assets.test.ts +55 -0
- package/src/__tests__/admin-data-routes.test.ts +488 -0
- package/src/__tests__/admin-db-target.test.ts +103 -0
- package/src/__tests__/admin-routing.test.ts +31 -0
- package/src/__tests__/admin-user-management.test.ts +311 -0
- package/src/__tests__/analytics-query.test.ts +75 -0
- package/src/__tests__/auth-d1.test.ts +749 -0
- package/src/__tests__/auth-db-adapter.test.ts +73 -0
- package/src/__tests__/auth-jwt.test.ts +440 -0
- package/src/__tests__/auth-oauth.test.ts +389 -0
- package/src/__tests__/auth-password.test.ts +367 -0
- package/src/__tests__/auth-redirect.test.ts +87 -0
- package/src/__tests__/backup-restore.test.ts +711 -0
- package/src/__tests__/broadcast.test.ts +128 -0
- package/src/__tests__/cli.test.ts +178 -0
- package/src/__tests__/cloudflare-realtime.test.ts +113 -0
- package/src/__tests__/config.test.ts +469 -0
- package/src/__tests__/cors.test.ts +154 -0
- package/src/__tests__/cron.test.ts +302 -0
- package/src/__tests__/d1-handler.test.ts +402 -0
- package/src/__tests__/d1-sql.test.ts +120 -0
- package/src/__tests__/database-live-config.test.ts +42 -0
- package/src/__tests__/database-live-emitter.test.ts +56 -0
- package/src/__tests__/database-live-filters.test.ts +63 -0
- package/src/__tests__/database-live-route.test.ts +113 -0
- package/src/__tests__/db-sql.test.ts +163 -0
- package/src/__tests__/do-lifecycle.test.ts +263 -0
- package/src/__tests__/do-router.test.ts +729 -0
- package/src/__tests__/email-provider.test.ts +128 -0
- package/src/__tests__/email-templates.test.ts +528 -0
- package/src/__tests__/error-format.test.ts +250 -0
- package/src/__tests__/field-ops.test.ts +242 -0
- package/src/__tests__/functions-context.test.ts +334 -0
- package/src/__tests__/functions-d1-proxy.test.ts +229 -0
- package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
- package/src/__tests__/functions-route.test.ts +139 -0
- package/src/__tests__/internal-request.test.ts +77 -0
- package/src/__tests__/log-writer.test.ts +44 -0
- package/src/__tests__/logger.test.ts +58 -0
- package/src/__tests__/meta-admin-proxy.test.ts +48 -0
- package/src/__tests__/meta-export-coverage.test.ts +191 -0
- package/src/__tests__/meta-route-registration.test.ts +47 -0
- package/src/__tests__/namespace-dump.test.ts +28 -0
- package/src/__tests__/oauth-providers.test.ts +337 -0
- package/src/__tests__/openapi-coverage.test.ts +144 -0
- package/src/__tests__/pagination.test.ts +59 -0
- package/src/__tests__/password-policy.test.ts +191 -0
- package/src/__tests__/plugin-migrations.test.ts +379 -0
- package/src/__tests__/postgres-batch-compat.test.ts +133 -0
- package/src/__tests__/postgres-dialect.test.ts +328 -0
- package/src/__tests__/postgres-executor.test.ts +79 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
- package/src/__tests__/postgres-schema-init.test.ts +105 -0
- package/src/__tests__/postgres-table-utils.test.ts +107 -0
- package/src/__tests__/presence.test.ts +199 -0
- package/src/__tests__/provider.test.ts +550 -0
- package/src/__tests__/public-user-profile.test.ts +339 -0
- package/src/__tests__/push-handlers.test.ts +179 -0
- package/src/__tests__/push-provider.test.ts +80 -0
- package/src/__tests__/push-token.test.ts +418 -0
- package/src/__tests__/query.test.ts +771 -0
- package/src/__tests__/rate-limit.test.ts +260 -0
- package/src/__tests__/room-access-policy.test.ts +101 -0
- package/src/__tests__/room-handler-context.test.ts +130 -0
- package/src/__tests__/room-monitoring.test.ts +138 -0
- package/src/__tests__/room-runtime-routing.test.ts +222 -0
- package/src/__tests__/room.test.ts +254 -0
- package/src/__tests__/route-parser.test.ts +490 -0
- package/src/__tests__/rules.test.ts +234 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
- package/src/__tests__/scheduled.test.ts +80 -0
- package/src/__tests__/schema.test.ts +1273 -0
- package/src/__tests__/security-hardening.test.ts +312 -0
- package/src/__tests__/server.unit.test.ts +333 -0
- package/src/__tests__/service-key-db-proxy.test.ts +650 -0
- package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
- package/src/__tests__/service-key.test.ts +757 -0
- package/src/__tests__/smoke-skip-report.test.ts +72 -0
- package/src/__tests__/sms-provider.test.ts +39 -0
- package/src/__tests__/sql-route.test.ts +218 -0
- package/src/__tests__/storage-hook-context.test.ts +115 -0
- package/src/__tests__/totp.test.ts +200 -0
- package/src/__tests__/uuid.test.ts +144 -0
- package/src/__tests__/validation.test.ts +773 -0
- package/src/__tests__/websocket-pending.test.ts +163 -0
- package/src/_functions-registry.ts +51 -0
- package/src/bench-entry.ts +9 -0
- package/src/cloudflare-test.d.ts +1 -0
- package/src/durable-objects/auth-do.ts +49 -0
- package/src/durable-objects/database-do.ts +2240 -0
- package/src/durable-objects/database-live-do.ts +949 -0
- package/src/durable-objects/logs-do.ts +1200 -0
- package/src/durable-objects/room-runtime-base.ts +1604 -0
- package/src/durable-objects/rooms-do.ts +2191 -0
- package/src/generated-config.ts +6 -0
- package/src/index.ts +382 -0
- package/src/lib/admin-assets.ts +54 -0
- package/src/lib/admin-db-target.ts +301 -0
- package/src/lib/admin-routing.ts +35 -0
- package/src/lib/admin-user-management.ts +464 -0
- package/src/lib/analytics-adapter.ts +103 -0
- package/src/lib/analytics-query.ts +579 -0
- package/src/lib/auth-d1-service.ts +1193 -0
- package/src/lib/auth-d1.ts +1056 -0
- package/src/lib/auth-db-adapter.ts +289 -0
- package/src/lib/auth-redirect.ts +116 -0
- package/src/lib/cidr.ts +115 -0
- package/src/lib/client-ip.ts +51 -0
- package/src/lib/cloudflare-realtime.ts +251 -0
- package/src/lib/control-db.ts +36 -0
- package/src/lib/cron.ts +163 -0
- package/src/lib/d1-handler.ts +1425 -0
- package/src/lib/d1-schema-init.ts +255 -0
- package/src/lib/d1-sql.ts +33 -0
- package/src/lib/database-live-config.ts +24 -0
- package/src/lib/database-live-emitter.ts +111 -0
- package/src/lib/db-sql.ts +66 -0
- package/src/lib/do-retry.ts +36 -0
- package/src/lib/do-router.ts +270 -0
- package/src/lib/do-sql.ts +73 -0
- package/src/lib/email-provider.ts +379 -0
- package/src/lib/email-templates.ts +285 -0
- package/src/lib/email-translations.ts +422 -0
- package/src/lib/errors.ts +151 -0
- package/src/lib/functions.ts +2091 -0
- package/src/lib/hono.ts +56 -0
- package/src/lib/internal-request.ts +56 -0
- package/src/lib/jwt.ts +354 -0
- package/src/lib/log-writer.ts +272 -0
- package/src/lib/namespace-dump.ts +125 -0
- package/src/lib/oauth-providers.ts +1225 -0
- package/src/lib/op-parser.ts +99 -0
- package/src/lib/openapi.ts +146 -0
- package/src/lib/pagination.ts +19 -0
- package/src/lib/password-policy.ts +102 -0
- package/src/lib/password.ts +145 -0
- package/src/lib/plugin-migrations.ts +612 -0
- package/src/lib/postgres-executor.ts +203 -0
- package/src/lib/postgres-handler.ts +1102 -0
- package/src/lib/postgres-schema-init.ts +341 -0
- package/src/lib/postgres-table-utils.ts +87 -0
- package/src/lib/public-user-profile.ts +187 -0
- package/src/lib/push-provider.ts +409 -0
- package/src/lib/push-token.ts +294 -0
- package/src/lib/query-engine.ts +768 -0
- package/src/lib/room-monitoring.ts +97 -0
- package/src/lib/room-runtime.ts +14 -0
- package/src/lib/route-parser.ts +434 -0
- package/src/lib/schema.ts +538 -0
- package/src/lib/schemas.ts +152 -0
- package/src/lib/service-key.ts +419 -0
- package/src/lib/sms-provider.ts +230 -0
- package/src/lib/startup-config.ts +99 -0
- package/src/lib/totp.ts +242 -0
- package/src/lib/uuid.ts +87 -0
- package/src/lib/validation.ts +205 -0
- package/src/lib/version.ts +2 -0
- package/src/lib/websocket-pending.ts +40 -0
- package/src/middleware/auth.ts +169 -0
- package/src/middleware/captcha-verify.ts +217 -0
- package/src/middleware/cors.ts +159 -0
- package/src/middleware/error-handler.ts +54 -0
- package/src/middleware/internal-guard.ts +26 -0
- package/src/middleware/logger.ts +126 -0
- package/src/middleware/rate-limit.ts +283 -0
- package/src/middleware/rules.ts +475 -0
- package/src/routes/admin-auth.ts +447 -0
- package/src/routes/admin.ts +3501 -0
- package/src/routes/analytics-api.ts +290 -0
- package/src/routes/auth.ts +4222 -0
- package/src/routes/backup.ts +1466 -0
- package/src/routes/config.ts +53 -0
- package/src/routes/d1.ts +109 -0
- package/src/routes/database-live.ts +281 -0
- package/src/routes/functions.ts +155 -0
- package/src/routes/health.ts +32 -0
- package/src/routes/kv.ts +167 -0
- package/src/routes/oauth.ts +1055 -0
- package/src/routes/push.ts +1465 -0
- package/src/routes/room.ts +639 -0
- package/src/routes/schema-endpoint.ts +76 -0
- package/src/routes/sql.ts +176 -0
- package/src/routes/storage.ts +1674 -0
- package/src/routes/tables.ts +699 -0
- package/src/routes/users.ts +21 -0
- package/src/routes/vectorize.ts +372 -0
- package/src/types.ts +99 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 서버 단위 테스트 — lib/cron.ts
|
|
3
|
+
*
|
|
4
|
+
* 실행: cd packages/server && npx vitest run src/__tests__/cron.test.ts
|
|
5
|
+
*
|
|
6
|
+
* 테스트 대상:
|
|
7
|
+
* parseCronField / parseCron / matchesCron / getNextFireTime / getNextAlarm
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
parseCronField,
|
|
13
|
+
parseCron,
|
|
14
|
+
matchesCron,
|
|
15
|
+
getNextFireTime,
|
|
16
|
+
getNextAlarm,
|
|
17
|
+
type ParsedScheduleFunction,
|
|
18
|
+
} from '../lib/cron.js';
|
|
19
|
+
|
|
20
|
+
// ─── A. parseCronField ──────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe('parseCronField', () => {
|
|
23
|
+
it('* → all values in range', () => {
|
|
24
|
+
const result = parseCronField('*', 0, 59);
|
|
25
|
+
expect(result.size).toBe(60);
|
|
26
|
+
expect(result.has(0)).toBe(true);
|
|
27
|
+
expect(result.has(59)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('single value', () => {
|
|
31
|
+
const result = parseCronField('5', 0, 59);
|
|
32
|
+
expect(result.size).toBe(1);
|
|
33
|
+
expect(result.has(5)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('range N-M', () => {
|
|
37
|
+
const result = parseCronField('10-15', 0, 59);
|
|
38
|
+
expect(result.size).toBe(6);
|
|
39
|
+
for (let i = 10; i <= 15; i++) expect(result.has(i)).toBe(true);
|
|
40
|
+
expect(result.has(9)).toBe(false);
|
|
41
|
+
expect(result.has(16)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('*/step (star step)', () => {
|
|
45
|
+
const result = parseCronField('*/5', 0, 59);
|
|
46
|
+
expect(result.has(0)).toBe(true);
|
|
47
|
+
expect(result.has(5)).toBe(true);
|
|
48
|
+
expect(result.has(10)).toBe(true);
|
|
49
|
+
expect(result.has(55)).toBe(true);
|
|
50
|
+
expect(result.has(3)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('range/step (N-M/step)', () => {
|
|
54
|
+
const result = parseCronField('0-30/5', 0, 59);
|
|
55
|
+
expect([...result].sort((a, b) => a - b)).toEqual([0, 5, 10, 15, 20, 25, 30]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('comma-separated list', () => {
|
|
59
|
+
const result = parseCronField('1,15,30', 0, 59);
|
|
60
|
+
expect(result.size).toBe(3);
|
|
61
|
+
expect(result.has(1)).toBe(true);
|
|
62
|
+
expect(result.has(15)).toBe(true);
|
|
63
|
+
expect(result.has(30)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('mixed list: value, range, step', () => {
|
|
67
|
+
const result = parseCronField('0,10-12,*/20', 0, 59);
|
|
68
|
+
// 0, 10, 11, 12, 0, 20, 40 → unique: {0, 10, 11, 12, 20, 40}
|
|
69
|
+
expect(result.has(0)).toBe(true);
|
|
70
|
+
expect(result.has(10)).toBe(true);
|
|
71
|
+
expect(result.has(11)).toBe(true);
|
|
72
|
+
expect(result.has(12)).toBe(true);
|
|
73
|
+
expect(result.has(20)).toBe(true);
|
|
74
|
+
expect(result.has(40)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('step 0 → throws', () => {
|
|
78
|
+
expect(() => parseCronField('*/0', 0, 59)).toThrow('Invalid cron step');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('out of range single value → throws', () => {
|
|
82
|
+
expect(() => parseCronField('60', 0, 59)).toThrow();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('below min → throws', () => {
|
|
86
|
+
expect(() => parseCronField('-1', 0, 59)).toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('non-numeric → throws', () => {
|
|
90
|
+
expect(() => parseCronField('abc', 0, 59)).toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('day of week 0-6', () => {
|
|
94
|
+
const result = parseCronField('0', 0, 6);
|
|
95
|
+
expect(result.has(0)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('month 1-12 single', () => {
|
|
99
|
+
const result = parseCronField('12', 1, 12);
|
|
100
|
+
expect(result.has(12)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('whitespace in comma list trimmed', () => {
|
|
104
|
+
const result = parseCronField('1, 2, 3', 0, 59);
|
|
105
|
+
expect(result.size).toBe(3);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ─── B. parseCron ───────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
describe('parseCron', () => {
|
|
112
|
+
it('every day at 09:00 → minute=0, hour=9', () => {
|
|
113
|
+
const sched = parseCron('0 9 * * *');
|
|
114
|
+
expect(sched.minutes.has(0)).toBe(true);
|
|
115
|
+
expect(sched.minutes.size).toBe(1);
|
|
116
|
+
expect(sched.hours.has(9)).toBe(true);
|
|
117
|
+
expect(sched.hours.size).toBe(1);
|
|
118
|
+
expect(sched.daysOfMonth.size).toBe(31);
|
|
119
|
+
expect(sched.months.size).toBe(12);
|
|
120
|
+
expect(sched.daysOfWeek.size).toBe(7);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('every 5 minutes', () => {
|
|
124
|
+
const sched = parseCron('*/5 * * * *');
|
|
125
|
+
expect(sched.minutes.size).toBe(12); // 0,5,10,...,55
|
|
126
|
+
expect(sched.minutes.has(0)).toBe(true);
|
|
127
|
+
expect(sched.minutes.has(55)).toBe(true);
|
|
128
|
+
expect(sched.minutes.has(3)).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('every Monday at 02:30', () => {
|
|
132
|
+
const sched = parseCron('30 2 * * 1');
|
|
133
|
+
expect(sched.minutes.has(30)).toBe(true);
|
|
134
|
+
expect(sched.hours.has(2)).toBe(true);
|
|
135
|
+
expect(sched.daysOfWeek.has(1)).toBe(true);
|
|
136
|
+
expect(sched.daysOfWeek.size).toBe(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('first day of every month at midnight', () => {
|
|
140
|
+
const sched = parseCron('0 0 1 * *');
|
|
141
|
+
expect(sched.minutes.has(0)).toBe(true);
|
|
142
|
+
expect(sched.hours.has(0)).toBe(true);
|
|
143
|
+
expect(sched.daysOfMonth.has(1)).toBe(true);
|
|
144
|
+
expect(sched.daysOfMonth.size).toBe(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('wrong number of fields → throws', () => {
|
|
148
|
+
expect(() => parseCron('* * *')).toThrow('expected 5 fields');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('6 fields → throws', () => {
|
|
152
|
+
expect(() => parseCron('0 0 0 0 0 0')).toThrow('expected 5 fields');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('extra whitespace is trimmed', () => {
|
|
156
|
+
const sched = parseCron(' 0 9 * * * ');
|
|
157
|
+
expect(sched.minutes.has(0)).toBe(true);
|
|
158
|
+
expect(sched.hours.has(9)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ─── C. matchesCron ─────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
describe('matchesCron', () => {
|
|
165
|
+
it('matches exact time', () => {
|
|
166
|
+
const sched = parseCron('30 14 * * *');
|
|
167
|
+
// 2024-06-15 14:30 UTC is a Saturday (day 6)
|
|
168
|
+
const date = new Date('2024-06-15T14:30:00Z');
|
|
169
|
+
expect(matchesCron(date, sched)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('does not match wrong minute', () => {
|
|
173
|
+
const sched = parseCron('30 14 * * *');
|
|
174
|
+
const date = new Date('2024-06-15T14:31:00Z');
|
|
175
|
+
expect(matchesCron(date, sched)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('does not match wrong hour', () => {
|
|
179
|
+
const sched = parseCron('30 14 * * *');
|
|
180
|
+
const date = new Date('2024-06-15T15:30:00Z');
|
|
181
|
+
expect(matchesCron(date, sched)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('day of week matters', () => {
|
|
185
|
+
const sched = parseCron('0 0 * * 1'); // Monday only
|
|
186
|
+
// 2024-06-17 is Monday
|
|
187
|
+
expect(matchesCron(new Date('2024-06-17T00:00:00Z'), sched)).toBe(true);
|
|
188
|
+
// 2024-06-18 is Tuesday
|
|
189
|
+
expect(matchesCron(new Date('2024-06-18T00:00:00Z'), sched)).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('month matters', () => {
|
|
193
|
+
const sched = parseCron('0 0 1 6 *'); // June 1st only
|
|
194
|
+
expect(matchesCron(new Date('2024-06-01T00:00:00Z'), sched)).toBe(true);
|
|
195
|
+
expect(matchesCron(new Date('2024-07-01T00:00:00Z'), sched)).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('day of month matters', () => {
|
|
199
|
+
const sched = parseCron('0 0 15 * *'); // 15th of each month
|
|
200
|
+
expect(matchesCron(new Date('2024-06-15T00:00:00Z'), sched)).toBe(true);
|
|
201
|
+
expect(matchesCron(new Date('2024-06-14T00:00:00Z'), sched)).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('uses UTC methods', () => {
|
|
205
|
+
const sched = parseCron('0 0 * * *'); // midnight UTC
|
|
206
|
+
const date = new Date('2024-06-15T00:00:00Z');
|
|
207
|
+
expect(matchesCron(date, sched)).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ─── D. getNextFireTime ─────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
describe('getNextFireTime', () => {
|
|
214
|
+
it('next minute for every-minute cron', () => {
|
|
215
|
+
const sched = parseCron('* * * * *');
|
|
216
|
+
const from = new Date('2024-06-15T10:30:00Z');
|
|
217
|
+
const next = getNextFireTime(sched, from);
|
|
218
|
+
expect(next).toBe(new Date('2024-06-15T10:31:00Z').getTime());
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('next day for daily cron', () => {
|
|
222
|
+
const sched = parseCron('0 9 * * *');
|
|
223
|
+
const from = new Date('2024-06-15T10:00:00Z'); // after 09:00
|
|
224
|
+
const next = getNextFireTime(sched, from);
|
|
225
|
+
expect(next).toBe(new Date('2024-06-16T09:00:00Z').getTime());
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('same day if before target time', () => {
|
|
229
|
+
const sched = parseCron('0 14 * * *');
|
|
230
|
+
const from = new Date('2024-06-15T10:00:00Z'); // before 14:00
|
|
231
|
+
const next = getNextFireTime(sched, from);
|
|
232
|
+
expect(next).toBe(new Date('2024-06-15T14:00:00Z').getTime());
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('skips to correct day of week', () => {
|
|
236
|
+
const sched = parseCron('0 0 * * 1'); // Monday only
|
|
237
|
+
// 2024-06-15 is Saturday → next Monday is June 17
|
|
238
|
+
const from = new Date('2024-06-15T00:00:00Z');
|
|
239
|
+
const next = getNextFireTime(sched, from);
|
|
240
|
+
expect(next).toBe(new Date('2024-06-17T00:00:00Z').getTime());
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('month boundary: Dec 31 → Jan 1 next year', () => {
|
|
244
|
+
const sched = parseCron('0 0 1 1 *'); // Jan 1st midnight
|
|
245
|
+
const from = new Date('2024-12-31T23:59:00Z');
|
|
246
|
+
const next = getNextFireTime(sched, from);
|
|
247
|
+
expect(next).toBe(new Date('2025-01-01T00:00:00Z').getTime());
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('seconds are zeroed', () => {
|
|
251
|
+
const sched = parseCron('* * * * *');
|
|
252
|
+
const from = new Date('2024-06-15T10:30:45Z'); // 45 seconds
|
|
253
|
+
const next = getNextFireTime(sched, from);
|
|
254
|
+
const d = new Date(next);
|
|
255
|
+
expect(d.getUTCSeconds()).toBe(0);
|
|
256
|
+
expect(d.getUTCMilliseconds()).toBe(0);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ─── E. getNextAlarm ────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
describe('getNextAlarm', () => {
|
|
263
|
+
it('empty schedules → null', () => {
|
|
264
|
+
expect(getNextAlarm([], new Date())).toBe(null);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('single schedule → returns its next fire time', () => {
|
|
268
|
+
const sched = parseCron('0 9 * * *');
|
|
269
|
+
const schedules: ParsedScheduleFunction[] = [
|
|
270
|
+
{ name: 'daily', cron: '0 9 * * *', schedule: sched },
|
|
271
|
+
];
|
|
272
|
+
const from = new Date('2024-06-15T10:00:00Z');
|
|
273
|
+
const result = getNextAlarm(schedules, from);
|
|
274
|
+
expect(result).not.toBe(null);
|
|
275
|
+
expect(result!.functions).toEqual(['daily']);
|
|
276
|
+
expect(result!.time).toBe(new Date('2024-06-16T09:00:00Z').getTime());
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('multiple schedules → picks earliest', () => {
|
|
280
|
+
const schedules: ParsedScheduleFunction[] = [
|
|
281
|
+
{ name: 'hourly', cron: '0 * * * *', schedule: parseCron('0 * * * *') },
|
|
282
|
+
{ name: 'daily', cron: '0 9 * * *', schedule: parseCron('0 9 * * *') },
|
|
283
|
+
];
|
|
284
|
+
const from = new Date('2024-06-15T10:30:00Z');
|
|
285
|
+
const result = getNextAlarm(schedules, from);
|
|
286
|
+
expect(result!.functions).toEqual(['hourly']);
|
|
287
|
+
// Next hour is 11:00
|
|
288
|
+
expect(result!.time).toBe(new Date('2024-06-15T11:00:00Z').getTime());
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('simultaneous fire times → groups function names', () => {
|
|
292
|
+
const schedules: ParsedScheduleFunction[] = [
|
|
293
|
+
{ name: 'jobA', cron: '0 12 * * *', schedule: parseCron('0 12 * * *') },
|
|
294
|
+
{ name: 'jobB', cron: '0 12 * * *', schedule: parseCron('0 12 * * *') },
|
|
295
|
+
];
|
|
296
|
+
const from = new Date('2024-06-15T10:00:00Z');
|
|
297
|
+
const result = getNextAlarm(schedules, from);
|
|
298
|
+
expect(result!.functions).toContain('jobA');
|
|
299
|
+
expect(result!.functions).toContain('jobB');
|
|
300
|
+
expect(result!.functions.length).toBe(2);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 서버 단위 테스트 — D1 Schema Init + Routing
|
|
3
|
+
* d1-handler.test.ts
|
|
4
|
+
*
|
|
5
|
+
* 실행: cd packages/server && npx vitest run src/__tests__/d1-handler.test.ts
|
|
6
|
+
*
|
|
7
|
+
* 테스트 대상:
|
|
8
|
+
* A. ensureD1Schema — lazy schema initialization
|
|
9
|
+
* B. _resetD1SchemaCache — cache invalidation
|
|
10
|
+
* C. shouldRouteToD1 / getD1BindingName — routing (also in do-router.test.ts)
|
|
11
|
+
* D. D1 meta helpers (schemaHash, migration_version)
|
|
12
|
+
* E. D1 migration engine
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
16
|
+
import { ensureD1Schema, _resetD1SchemaCache } from '../lib/d1-schema-init.js';
|
|
17
|
+
import { shouldRouteToD1, getD1BindingName } from '../lib/do-router.js';
|
|
18
|
+
import type { TableConfig } from '@edge-base/shared';
|
|
19
|
+
|
|
20
|
+
// ─── Mock D1 ─────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface MockCall {
|
|
23
|
+
sql: string;
|
|
24
|
+
bindings: unknown[];
|
|
25
|
+
method: 'first' | 'all' | 'run';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createMockD1(options: {
|
|
29
|
+
firstResult?: unknown;
|
|
30
|
+
allResult?: { results: unknown[] };
|
|
31
|
+
/** Per-call sequential results for `.first()` */
|
|
32
|
+
firstResults?: unknown[];
|
|
33
|
+
/** Per-call sequential results for `.all()` */
|
|
34
|
+
allResults?: Array<{ results: unknown[] }>;
|
|
35
|
+
} = {}): D1Database & { _calls: MockCall[]; _batchCalls: number; _batchStmts: string[][] } {
|
|
36
|
+
const calls: MockCall[] = [];
|
|
37
|
+
let firstCallIdx = 0;
|
|
38
|
+
let allCallIdx = 0;
|
|
39
|
+
let batchCalls = 0;
|
|
40
|
+
const batchStmts: string[][] = [];
|
|
41
|
+
|
|
42
|
+
function makeStmt(sql: string): any {
|
|
43
|
+
const call: MockCall = { sql, bindings: [], method: 'run' };
|
|
44
|
+
calls.push(call);
|
|
45
|
+
|
|
46
|
+
const stmt = {
|
|
47
|
+
_sql: sql,
|
|
48
|
+
bind: (...values: unknown[]) => {
|
|
49
|
+
call.bindings = values;
|
|
50
|
+
return stmt;
|
|
51
|
+
},
|
|
52
|
+
first: async () => {
|
|
53
|
+
call.method = 'first';
|
|
54
|
+
if (options.firstResults && firstCallIdx < options.firstResults.length) {
|
|
55
|
+
return options.firstResults[firstCallIdx++];
|
|
56
|
+
}
|
|
57
|
+
return options.firstResult ?? null;
|
|
58
|
+
},
|
|
59
|
+
all: async () => {
|
|
60
|
+
call.method = 'all';
|
|
61
|
+
if (options.allResults && allCallIdx < options.allResults.length) {
|
|
62
|
+
return options.allResults[allCallIdx++];
|
|
63
|
+
}
|
|
64
|
+
return options.allResult ?? { results: [] };
|
|
65
|
+
},
|
|
66
|
+
run: async () => {
|
|
67
|
+
call.method = 'run';
|
|
68
|
+
return { success: true };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
return stmt;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const db = {
|
|
75
|
+
prepare: (sql: string) => makeStmt(sql),
|
|
76
|
+
batch: async (stmts: any[]) => {
|
|
77
|
+
batchCalls++;
|
|
78
|
+
batchStmts.push(stmts.map((s: any) => s._sql));
|
|
79
|
+
return stmts;
|
|
80
|
+
},
|
|
81
|
+
_calls: calls,
|
|
82
|
+
_batchCalls: 0,
|
|
83
|
+
_batchStmts: batchStmts,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
Object.defineProperty(db, '_batchCalls', { get: () => batchCalls });
|
|
87
|
+
|
|
88
|
+
return db as any;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── A. ensureD1Schema ───────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
describe('ensureD1Schema', () => {
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
_resetD1SchemaCache();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const simpleTables: Record<string, TableConfig> = {
|
|
99
|
+
posts: {
|
|
100
|
+
schema: {
|
|
101
|
+
title: { type: 'string' },
|
|
102
|
+
content: { type: 'text' },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
it('creates _meta table on first call', async () => {
|
|
108
|
+
const db = createMockD1();
|
|
109
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
110
|
+
|
|
111
|
+
// First call should be _meta DDL
|
|
112
|
+
const metaCall = db._calls.find(c => c.sql.includes('_meta'));
|
|
113
|
+
expect(metaCall).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('enables foreign keys via PRAGMA', async () => {
|
|
117
|
+
const db = createMockD1();
|
|
118
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
119
|
+
|
|
120
|
+
const pragmaCall = db._calls.find(c => c.sql.includes('PRAGMA foreign_keys'));
|
|
121
|
+
expect(pragmaCall).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('checks schema hash for each table', async () => {
|
|
125
|
+
const db = createMockD1();
|
|
126
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
127
|
+
|
|
128
|
+
// Should query _meta for schemaHash:posts
|
|
129
|
+
const hashCheck = db._calls.find(c =>
|
|
130
|
+
c.sql.includes('_meta') && c.bindings.includes('schemaHash:posts'),
|
|
131
|
+
);
|
|
132
|
+
expect(hashCheck).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('creates table DDL when no stored hash exists', async () => {
|
|
136
|
+
const db = createMockD1();
|
|
137
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
138
|
+
|
|
139
|
+
// Should batch table creation DDLs
|
|
140
|
+
expect(db._batchCalls).toBeGreaterThan(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('stores schema hash after table creation', async () => {
|
|
144
|
+
const db = createMockD1();
|
|
145
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
146
|
+
|
|
147
|
+
// Should INSERT INTO _meta with schemaHash:posts
|
|
148
|
+
const hashStore = db._calls.find(c =>
|
|
149
|
+
c.sql.includes('INSERT INTO "_meta"') && c.bindings[0] === 'schemaHash:posts',
|
|
150
|
+
);
|
|
151
|
+
expect(hashStore).toBeDefined();
|
|
152
|
+
// Hash value should be a non-empty string
|
|
153
|
+
expect(hashStore!.bindings[1]).toBeTruthy();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('skips re-init on second call (memory cache)', async () => {
|
|
157
|
+
const db = createMockD1();
|
|
158
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
159
|
+
const callCount = db._calls.length;
|
|
160
|
+
|
|
161
|
+
// Second call should be no-op
|
|
162
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
163
|
+
expect(db._calls.length).toBe(callCount); // no new calls
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('different namespaces are tracked independently', async () => {
|
|
167
|
+
const db = createMockD1();
|
|
168
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
169
|
+
const callCount1 = db._calls.length;
|
|
170
|
+
|
|
171
|
+
// Different namespace should trigger init
|
|
172
|
+
await ensureD1Schema(db, 'analytics', { events: { schema: { name: { type: 'string' } } } });
|
|
173
|
+
expect(db._calls.length).toBeGreaterThan(callCount1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('_resetD1SchemaCache clears the memory cache', async () => {
|
|
177
|
+
const db = createMockD1();
|
|
178
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
179
|
+
const callCount = db._calls.length;
|
|
180
|
+
|
|
181
|
+
_resetD1SchemaCache();
|
|
182
|
+
await ensureD1Schema(db, 'shared', simpleTables);
|
|
183
|
+
// Should re-run init after cache clear
|
|
184
|
+
expect(db._calls.length).toBeGreaterThan(callCount);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ─── B. ensureD1Schema — schema update (hash mismatch) ──────────────────────
|
|
189
|
+
|
|
190
|
+
describe('ensureD1Schema — schema update', () => {
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
_resetD1SchemaCache();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('detects schema hash mismatch and runs PRAGMA table_info', async () => {
|
|
196
|
+
// Simulate: _meta has a stale hash for 'posts'
|
|
197
|
+
const db = createMockD1({
|
|
198
|
+
// first .first() call returns stale hash, rest return null
|
|
199
|
+
firstResults: [{ value: 'stale-hash' }],
|
|
200
|
+
// PRAGMA table_info returns existing columns
|
|
201
|
+
allResult: { results: [{ name: 'id' }, { name: 'title' }] },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await ensureD1Schema(db, 'shared', {
|
|
205
|
+
posts: {
|
|
206
|
+
schema: {
|
|
207
|
+
title: { type: 'string' },
|
|
208
|
+
content: { type: 'text' }, // new column
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Should run PRAGMA table_info to detect existing columns
|
|
214
|
+
const pragmaInfo = db._calls.find(c => c.sql.includes('PRAGMA table_info'));
|
|
215
|
+
expect(pragmaInfo).toBeDefined();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ─── C. ensureD1Schema — migrations ─────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
describe('ensureD1Schema — migrations', () => {
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
_resetD1SchemaCache();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('sets initial migration version on fresh table', async () => {
|
|
227
|
+
const db = createMockD1();
|
|
228
|
+
|
|
229
|
+
await ensureD1Schema(db, 'shared', {
|
|
230
|
+
posts: {
|
|
231
|
+
schema: { title: { type: 'string' } },
|
|
232
|
+
migrations: [
|
|
233
|
+
{ version: 2, description: 'Add slug column', up: 'ALTER TABLE posts ADD COLUMN slug TEXT' },
|
|
234
|
+
{ version: 3, description: 'Add slug index', up: 'CREATE INDEX idx_slug ON posts(slug)' },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Should set migration_version:posts to 3 (max version)
|
|
240
|
+
const versionSet = db._calls.find(c =>
|
|
241
|
+
c.sql.includes('INSERT INTO "_meta"') && c.bindings[0] === 'migration_version:posts',
|
|
242
|
+
);
|
|
243
|
+
expect(versionSet).toBeDefined();
|
|
244
|
+
expect(versionSet!.bindings[1]).toBe('3');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('runs pending migrations when schema hash matches', async () => {
|
|
248
|
+
// We need to force hash match — pass a config that produces the stored hash
|
|
249
|
+
// Since we can't predict the hash, test that migration SQL runs
|
|
250
|
+
// Use fresh DB (no stored hash) with migrations
|
|
251
|
+
const db2 = createMockD1({
|
|
252
|
+
firstResults: [
|
|
253
|
+
null, // schemaHash:posts → null = fresh table
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await ensureD1Schema(db2, 'shared', {
|
|
258
|
+
posts: {
|
|
259
|
+
schema: { title: { type: 'string' } },
|
|
260
|
+
migrations: [
|
|
261
|
+
{ version: 2, description: 'Add category column', up: 'ALTER TABLE posts ADD COLUMN category TEXT' },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Fresh table: sets migration_version to max (2), skips running them
|
|
267
|
+
const versionSet = db2._calls.find(c =>
|
|
268
|
+
c.bindings[0] === 'migration_version:posts',
|
|
269
|
+
);
|
|
270
|
+
expect(versionSet).toBeDefined();
|
|
271
|
+
expect(versionSet!.bindings[1]).toBe('2');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ─── D. Multiple tables in single namespace ─────────────────────────────────
|
|
276
|
+
|
|
277
|
+
describe('ensureD1Schema — multiple tables', () => {
|
|
278
|
+
beforeEach(() => {
|
|
279
|
+
_resetD1SchemaCache();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('initializes all tables in the namespace', async () => {
|
|
283
|
+
const db = createMockD1();
|
|
284
|
+
|
|
285
|
+
await ensureD1Schema(db, 'shared', {
|
|
286
|
+
posts: { schema: { title: { type: 'string' } } },
|
|
287
|
+
comments: { schema: { body: { type: 'text' } } },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Should check hash for both tables
|
|
291
|
+
const postHash = db._calls.find(c =>
|
|
292
|
+
c.bindings.includes('schemaHash:posts'),
|
|
293
|
+
);
|
|
294
|
+
const commentHash = db._calls.find(c =>
|
|
295
|
+
c.bindings.includes('schemaHash:comments'),
|
|
296
|
+
);
|
|
297
|
+
expect(postHash).toBeDefined();
|
|
298
|
+
expect(commentHash).toBeDefined();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ─── E. D1 routing integration ──────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
describe('D1 routing — shouldRouteToD1 integration', () => {
|
|
305
|
+
it('shared namespace with tables → D1 (auto-detect)', () => {
|
|
306
|
+
const config = {
|
|
307
|
+
databases: {
|
|
308
|
+
shared: {
|
|
309
|
+
tables: {
|
|
310
|
+
posts: { schema: { title: { type: 'string' } } },
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
expect(shouldRouteToD1('shared', config as any)).toBe(true);
|
|
316
|
+
expect(getD1BindingName('shared')).toBe('DB_D1_SHARED');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('explicit provider: "d1" → D1', () => {
|
|
320
|
+
const config = {
|
|
321
|
+
databases: {
|
|
322
|
+
analytics: {
|
|
323
|
+
provider: 'd1',
|
|
324
|
+
tables: { events: {} },
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
expect(shouldRouteToD1('analytics', config as any)).toBe(true);
|
|
329
|
+
expect(getD1BindingName('analytics')).toBe('DB_D1_ANALYTICS');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('workspace with instance: true → DO (not D1)', () => {
|
|
333
|
+
const config = {
|
|
334
|
+
databases: {
|
|
335
|
+
workspace: {
|
|
336
|
+
instance: true,
|
|
337
|
+
tables: { members: {} },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
expect(shouldRouteToD1('workspace', config as any)).toBe(false);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('namespace with canCreate access → DO', () => {
|
|
345
|
+
const config = {
|
|
346
|
+
databases: {
|
|
347
|
+
project: {
|
|
348
|
+
access: { canCreate: 'auth.role == "admin"' },
|
|
349
|
+
tables: { tasks: {} },
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
expect(shouldRouteToD1('project', config as any)).toBe(false);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('namespace with access callback config → DO', () => {
|
|
357
|
+
const config = {
|
|
358
|
+
databases: {
|
|
359
|
+
project: {
|
|
360
|
+
access: { access: 'auth.id == instanceId' },
|
|
361
|
+
tables: { tasks: {} },
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
expect(shouldRouteToD1('project', config as any)).toBe(false);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('mixed config: some D1, some DO', () => {
|
|
369
|
+
const config = {
|
|
370
|
+
databases: {
|
|
371
|
+
shared: { tables: { posts: {} } },
|
|
372
|
+
workspace: { instance: true, tables: { members: {} } },
|
|
373
|
+
logs: { provider: 'do' as const, tables: { entries: {} } },
|
|
374
|
+
analytics: { provider: 'd1' as const, tables: { events: {} } },
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
expect(shouldRouteToD1('shared', config as any)).toBe(true);
|
|
378
|
+
expect(shouldRouteToD1('workspace', config as any)).toBe(false);
|
|
379
|
+
expect(shouldRouteToD1('logs', config as any)).toBe(false);
|
|
380
|
+
expect(shouldRouteToD1('analytics', config as any)).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ─── F. D1 binding name convention ──────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
describe('getD1BindingName', () => {
|
|
387
|
+
it('simple namespace', () => {
|
|
388
|
+
expect(getD1BindingName('shared')).toBe('DB_D1_SHARED');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('camelCase namespace → uppercased', () => {
|
|
392
|
+
expect(getD1BindingName('myData')).toBe('DB_D1_MYDATA');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('hyphenated namespace → uppercased (with hyphen)', () => {
|
|
396
|
+
expect(getD1BindingName('my-data')).toBe('DB_D1_MY-DATA');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('already uppercase → unchanged', () => {
|
|
400
|
+
expect(getD1BindingName('SHARED')).toBe('DB_D1_SHARED');
|
|
401
|
+
});
|
|
402
|
+
});
|