@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.
Files changed (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. 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
+ });