@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,250 @@
1
+ /**
2
+ * 서버 단위 테스트 — lib/errors.ts + RoomsDO pure logic
3
+ * 1-26 error-format.test.ts — 40개
4
+ *
5
+ * 실행: cd packages/server && npx vitest run src/__tests__/error-format.test.ts
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest';
9
+ import {
10
+ validationError,
11
+ unauthorizedError,
12
+ forbiddenError,
13
+ notFoundError,
14
+ methodNotAllowedError,
15
+ rateLimitError,
16
+ hookRejectedError,
17
+ normalizeDatabaseError,
18
+ } from '../lib/errors.js';
19
+ import { EdgeBaseError } from '@edge-base/shared';
20
+
21
+ // ─── A. EdgeBaseError 구조 ─────────────────────────────────────────────────────
22
+
23
+ describe('EdgeBaseError structure', () => {
24
+ it('is an instanceof Error', () => {
25
+ const err = new EdgeBaseError(400, 'Bad request');
26
+ expect(err).toBeInstanceOf(Error);
27
+ });
28
+
29
+ it('is an instanceof EdgeBaseError', () => {
30
+ const err = new EdgeBaseError(400, 'Bad request');
31
+ expect(err).toBeInstanceOf(EdgeBaseError);
32
+ });
33
+
34
+ it('code stored correctly', () => {
35
+ const err = new EdgeBaseError(404, 'Not found');
36
+ expect(err.code).toBe(404);
37
+ });
38
+
39
+ it('message stored correctly', () => {
40
+ const err = new EdgeBaseError(400, 'Validation failed');
41
+ expect(err.message).toBe('Validation failed');
42
+ });
43
+
44
+ it('name is "EdgeBaseError"', () => {
45
+ const err = new EdgeBaseError(500, 'Internal error');
46
+ expect(err.name).toBe('EdgeBaseError');
47
+ });
48
+
49
+ it('stack is present (runtime-generated)', () => {
50
+ const err = new EdgeBaseError(500, 'Internal error');
51
+ // Stack should be present (set by Error constructor)
52
+ expect(typeof err.stack).toBe('string');
53
+ });
54
+ });
55
+
56
+ // ─── B. Error helper factory functions ────────────────────────────────────────
57
+
58
+ describe('validationError', () => {
59
+ it('returns status 400', () => {
60
+ expect(validationError('Bad input').code).toBe(400);
61
+ });
62
+
63
+ it('stores message', () => {
64
+ expect(validationError('Required fields missing').message).toBe('Required fields missing');
65
+ });
66
+
67
+ it('instanceof EdgeBaseError', () => {
68
+ expect(validationError('x')).toBeInstanceOf(EdgeBaseError);
69
+ });
70
+
71
+ it('with field errors', () => {
72
+ const err = validationError('Validation failed', {
73
+ email: { message: 'Invalid email', code: 'invalid_email' },
74
+ });
75
+ expect(err.code).toBe(400);
76
+ expect(err.message).toBe('Validation failed');
77
+ });
78
+ });
79
+
80
+ describe('unauthorizedError', () => {
81
+ it('returns status 401', () => {
82
+ expect(unauthorizedError().code).toBe(401);
83
+ });
84
+
85
+ it('default message', () => {
86
+ expect(unauthorizedError().message).toBe('Unauthorized.');
87
+ });
88
+
89
+ it('custom message', () => {
90
+ expect(unauthorizedError('Token expired').message).toBe('Token expired');
91
+ });
92
+ });
93
+
94
+ describe('forbiddenError', () => {
95
+ it('returns status 403', () => {
96
+ expect(forbiddenError().code).toBe(403);
97
+ });
98
+
99
+ it('default message', () => {
100
+ expect(forbiddenError().message).toBe('Access denied.');
101
+ });
102
+
103
+ it('custom message', () => {
104
+ expect(forbiddenError('Service Key required').message).toBe('Service Key required');
105
+ });
106
+ });
107
+
108
+ describe('notFoundError', () => {
109
+ it('returns status 404', () => {
110
+ expect(notFoundError().code).toBe(404);
111
+ });
112
+
113
+ it('default message', () => {
114
+ expect(notFoundError().message).toBe('Not found.');
115
+ });
116
+
117
+ it('custom message', () => {
118
+ expect(notFoundError('Record not found').message).toBe('Record not found');
119
+ });
120
+ });
121
+
122
+ describe('methodNotAllowedError', () => {
123
+ it('returns status 405', () => {
124
+ expect(methodNotAllowedError().code).toBe(405);
125
+ });
126
+
127
+ it('default message', () => {
128
+ expect(methodNotAllowedError().message).toBe('Method not allowed.');
129
+ });
130
+
131
+ it('custom message', () => {
132
+ expect(methodNotAllowedError('Custom not allowed message').message).toBe('Custom not allowed message');
133
+ });
134
+ });
135
+
136
+ describe('rateLimitError', () => {
137
+ it('returns status 429', () => {
138
+ expect(rateLimitError(30).code).toBe(429);
139
+ });
140
+
141
+ it('includes retryAfter in message', () => {
142
+ expect(rateLimitError(60).message).toContain('60');
143
+ });
144
+
145
+ it('different retryAfter values', () => {
146
+ expect(rateLimitError(5).message).toContain('5');
147
+ expect(rateLimitError(3600).message).toContain('3600');
148
+ });
149
+ });
150
+
151
+ describe('hookRejectedError', () => {
152
+ it('passes EdgeBaseError instances through untouched', () => {
153
+ const original = forbiddenError('Already normalized');
154
+ expect(hookRejectedError(original)).toBe(original);
155
+ });
156
+
157
+ it('maps ownership denial messages to 403', () => {
158
+ const err = hookRejectedError(new Error('Only owners can update this record.'));
159
+ expect(err.code).toBe(403);
160
+ expect(err.message).toContain('Only owners');
161
+ });
162
+
163
+ it('maps blocked hook messages to 403', () => {
164
+ const err = hookRejectedError(new Error('Blocked by test beforeUpload'));
165
+ expect(err.code).toBe(403);
166
+ expect(err.message).toContain('Blocked by test beforeUpload');
167
+ });
168
+
169
+ it('maps conflict-style messages to 409', () => {
170
+ const err = hookRejectedError(new Error('Email already exists.'));
171
+ expect(err.code).toBe(409);
172
+ });
173
+
174
+ it('falls back to validation errors for unknown hook failures', () => {
175
+ const err = hookRejectedError(new Error('Custom hook failure.'));
176
+ expect(err.code).toBe(400);
177
+ expect(err.message).toBe('Custom hook failure.');
178
+ });
179
+ });
180
+
181
+ describe('normalizeDatabaseError', () => {
182
+ it('maps foreign key failures to validation errors', () => {
183
+ const err = normalizeDatabaseError(new Error('D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT'));
184
+ expect(err).toBeInstanceOf(EdgeBaseError);
185
+ expect(err?.code).toBe(400);
186
+ expect(err?.message).toContain('Referenced record does not exist');
187
+ expect(err?.slug).toBe('foreign-key-failed');
188
+ });
189
+
190
+ it('maps foreign key failures from cross-realm error-like objects', () => {
191
+ const err = normalizeDatabaseError({
192
+ message: 'D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT',
193
+ });
194
+ expect(err).toBeInstanceOf(EdgeBaseError);
195
+ expect(err?.code).toBe(400);
196
+ });
197
+
198
+ it('maps unique constraint failures to conflict errors', () => {
199
+ const err = normalizeDatabaseError(new Error('UNIQUE constraint failed: categories.name'));
200
+ expect(err).toBeInstanceOf(EdgeBaseError);
201
+ expect(err?.code).toBe(409);
202
+ expect(err?.message).toContain('Record already exists');
203
+ expect(err?.message).toContain("'name'");
204
+ expect(err?.slug).toBe('record-already-exists');
205
+ });
206
+
207
+ it('returns null for unrelated runtime errors', () => {
208
+ expect(normalizeDatabaseError(new Error('socket hang up'))).toBeNull();
209
+ });
210
+ });
211
+
212
+ // ─── C. Error 구조 직렬화 ────────────────────────────────────────────────────
213
+
214
+ describe('error JSON structure', () => {
215
+ it('EdgeBaseError has code + message accessible', () => {
216
+ const err = new EdgeBaseError(401, 'Not authenticated');
217
+ const serialized = {
218
+ code: err.code,
219
+ message: err.message,
220
+ };
221
+ expect(serialized).toEqual({ code: 401, message: 'Not authenticated' });
222
+ });
223
+
224
+ it('stack trace not exposed in serialized form', () => {
225
+ const err = new EdgeBaseError(500, 'Internal');
226
+ const json = { code: err.code, message: err.message };
227
+ // Confirm stack is not in the response body
228
+ expect(json).not.toHaveProperty('stack');
229
+ });
230
+
231
+ it('400 errors are distinct from 500s', () => {
232
+ const e400 = validationError('Bad input');
233
+ const e500 = new EdgeBaseError(500, 'Internal');
234
+ expect(e400.code).not.toBe(e500.code);
235
+ });
236
+
237
+ it('all helper-created errors are EdgeBaseError instances', () => {
238
+ const errors = [
239
+ validationError('x'),
240
+ unauthorizedError(),
241
+ forbiddenError(),
242
+ notFoundError(),
243
+ methodNotAllowedError(),
244
+ rateLimitError(10),
245
+ ];
246
+ for (const err of errors) {
247
+ expect(err).toBeInstanceOf(EdgeBaseError);
248
+ }
249
+ });
250
+ });
@@ -0,0 +1,242 @@
1
+ /**
2
+ * 서버 단위 테스트 — lib/op-parser.ts
3
+ * 1-07 field-ops.test.ts — 40개
4
+ *
5
+ * 실행: cd packages/server && npx vitest run src/__tests__/field-ops.test.ts
6
+ *
7
+ * 테스트 대상 (op-parser.ts):
8
+ * parseUpdateBody — increment / deleteField / 일반 값 / 혼합 / 에러
9
+ * buildOpClause (indirect through parseUpdateBody)
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { parseUpdateBody } from '../lib/op-parser.js';
14
+
15
+ // ─── A. 일반 값 (non-$op) ────────────────────────────────────────────────────
16
+
17
+ describe('parseUpdateBody — regular values', () => {
18
+ it('single string field', () => {
19
+ const { setClauses, params } = parseUpdateBody({ title: 'Hello' });
20
+ expect(setClauses).toEqual(['"title" = ?']);
21
+ expect(params).toEqual(['Hello']);
22
+ });
23
+
24
+ it('single number field', () => {
25
+ const { setClauses, params } = parseUpdateBody({ views: 42 });
26
+ expect(setClauses).toEqual(['"views" = ?']);
27
+ expect(params).toEqual([42]);
28
+ });
29
+
30
+ it('null value → field = NULL via regular path', () => {
31
+ const { setClauses, params } = parseUpdateBody({ deletedAt: null });
32
+ expect(setClauses).toContain('"deletedAt" = ?');
33
+ expect(params).toContain(null);
34
+ });
35
+
36
+ it('boolean value', () => {
37
+ const { setClauses: _setClauses, params } = parseUpdateBody({ active: true });
38
+ expect(params).toContain(true);
39
+ });
40
+
41
+ it('excludes id field by default', () => {
42
+ const { setClauses, params } = parseUpdateBody({ id: 'ignored', title: 'kept' });
43
+ expect(setClauses).toHaveLength(1);
44
+ expect(setClauses[0]).toContain('"title"');
45
+ expect(params).toEqual(['kept']);
46
+ });
47
+
48
+ it('excludes custom excludeFields', () => {
49
+ const { setClauses } = parseUpdateBody(
50
+ { createdAt: 'now', title: 'hi' },
51
+ ['id', 'createdAt'],
52
+ );
53
+ expect(setClauses).toHaveLength(1);
54
+ expect(setClauses[0]).toContain('"title"');
55
+ });
56
+
57
+ it('multiple fields → multiple setClauses', () => {
58
+ const { setClauses, params } = parseUpdateBody({ a: 1, b: 2 });
59
+ expect(setClauses).toHaveLength(2);
60
+ expect(params).toEqual([1, 2]);
61
+ });
62
+
63
+ it('empty object → empty setClauses', () => {
64
+ const { setClauses, params } = parseUpdateBody({});
65
+ expect(setClauses).toEqual([]);
66
+ expect(params).toEqual([]);
67
+ });
68
+
69
+ it('field name with special chars is escaped', () => {
70
+ const { setClauses } = parseUpdateBody({ 'my-field': 'value' });
71
+ // Should be double-quoted
72
+ expect(setClauses[0]).toContain('"my-field"');
73
+ });
74
+
75
+ it('field name with double quote is escaped', () => {
76
+ const { setClauses } = parseUpdateBody({ 'col"name': 'v' });
77
+ expect(setClauses[0]).toContain('"col""name"');
78
+ });
79
+ });
80
+
81
+ // ─── B. increment $op ──────────────────────────────────────────────────────
82
+
83
+ describe('parseUpdateBody — increment', () => {
84
+ it('increment generates COALESCE SQL', () => {
85
+ const { setClauses, params } = parseUpdateBody({
86
+ viewCount: { $op: 'increment', value: 5 },
87
+ });
88
+ expect(setClauses[0]).toBe('"viewCount" = COALESCE("viewCount", 0) + ?');
89
+ expect(params[0]).toBe(5);
90
+ });
91
+
92
+ it('increment negative value', () => {
93
+ const { setClauses: _setClauses, params } = parseUpdateBody({
94
+ score: { $op: 'increment', value: -10 },
95
+ });
96
+ expect(params[0]).toBe(-10);
97
+ });
98
+
99
+ it('increment by 0', () => {
100
+ const { params } = parseUpdateBody({ count: { $op: 'increment', value: 0 } });
101
+ expect(params[0]).toBe(0);
102
+ });
103
+
104
+ it('increment decimal value', () => {
105
+ const { params } = parseUpdateBody({ price: { $op: 'increment', value: 3.14 } });
106
+ expect(params[0]).toBe(3.14);
107
+ });
108
+
109
+ it('increment with no value → defaults to 0', () => {
110
+ // op-parser: op.value ?? 0
111
+ const { params } = parseUpdateBody({ count: { $op: 'increment' } } as any);
112
+ expect(params[0]).toBe(0);
113
+ });
114
+
115
+ it('COALESCE pattern protects against NULL field', () => {
116
+ const { setClauses } = parseUpdateBody({ n: { $op: 'increment', value: 1 } });
117
+ expect(setClauses[0]).toContain('COALESCE');
118
+ });
119
+
120
+ it('postgres dialect uses numbered placeholders', () => {
121
+ const { setClauses, params, nextParamIndex } = parseUpdateBody(
122
+ { viewCount: { $op: 'increment', value: 5 } },
123
+ ['id'],
124
+ { dialect: 'postgres', startIndex: 3 },
125
+ );
126
+ expect(setClauses[0]).toBe('"viewCount" = COALESCE("viewCount", 0) + $3');
127
+ expect(params).toEqual([5]);
128
+ expect(nextParamIndex).toBe(4);
129
+ });
130
+ });
131
+
132
+ // ─── C. deleteField $op ────────────────────────────────────────────────────
133
+
134
+ describe('parseUpdateBody — deleteField', () => {
135
+ it('deleteField generates field = NULL', () => {
136
+ const { setClauses, params } = parseUpdateBody({
137
+ avatar: { $op: 'deleteField' },
138
+ });
139
+ expect(setClauses[0]).toBe('"avatar" = NULL');
140
+ expect(params).toEqual([]);
141
+ });
142
+
143
+ it('deleteField has no params', () => {
144
+ const { params } = parseUpdateBody({ x: { $op: 'deleteField' } });
145
+ expect(params).toHaveLength(0);
146
+ });
147
+
148
+ it('multiple deleteField ops', () => {
149
+ const { setClauses } = parseUpdateBody({
150
+ a: { $op: 'deleteField' },
151
+ b: { $op: 'deleteField' },
152
+ });
153
+ expect(setClauses).toHaveLength(2);
154
+ expect(setClauses.every((c) => c.endsWith('= NULL'))).toBe(true);
155
+ });
156
+ });
157
+
158
+ // ─── D. unknown $op ──────────────────────────────────────────────────────
159
+
160
+ describe('parseUpdateBody — unknown $op', () => {
161
+ it('unknown $op → throws', () => {
162
+ expect(() =>
163
+ parseUpdateBody({ x: { $op: 'multiply', value: 3 } } as any),
164
+ ).toThrow("Unknown field operator 'multiply'. Supported operators: increment, deleteField.");
165
+ });
166
+
167
+ it('unknown $op error contains op name', () => {
168
+ try {
169
+ parseUpdateBody({ x: { $op: 'badOp' } } as any);
170
+ } catch (err) {
171
+ expect((err as Error).message).toContain('badOp');
172
+ }
173
+ });
174
+ });
175
+
176
+ // ─── E. 혼합 (mix regular + $op) ─────────────────────────────────────────────
177
+
178
+ describe('parseUpdateBody — mixed', () => {
179
+ it('regular + increment mixed', () => {
180
+ const { setClauses, params: _params } = parseUpdateBody({
181
+ title: 'New Title',
182
+ viewCount: { $op: 'increment', value: 1 },
183
+ });
184
+ expect(setClauses).toHaveLength(2);
185
+ const titleClause = setClauses.find((c) => c.includes('"title"'));
186
+ const viewClause = setClauses.find((c) => c.includes('COALESCE'));
187
+ expect(titleClause).toBeTruthy();
188
+ expect(viewClause).toBeTruthy();
189
+ });
190
+
191
+ it('increment + deleteField mixed', () => {
192
+ const { setClauses } = parseUpdateBody({
193
+ count: { $op: 'increment', value: 5 },
194
+ avatar: { $op: 'deleteField' },
195
+ });
196
+ expect(setClauses).toHaveLength(2);
197
+ });
198
+
199
+ it('id excluded but $op fields processed', () => {
200
+ const { setClauses } = parseUpdateBody({
201
+ id: 'excluded',
202
+ views: { $op: 'increment', value: 1 },
203
+ });
204
+ expect(setClauses).toHaveLength(1);
205
+ expect(setClauses[0]).toContain('COALESCE');
206
+ });
207
+
208
+ it('postgres dialect preserves numbering across regular + deleteField', () => {
209
+ const { setClauses, params, nextParamIndex } = parseUpdateBody(
210
+ {
211
+ title: 'New Title',
212
+ avatar: { $op: 'deleteField' },
213
+ },
214
+ ['id'],
215
+ { dialect: 'postgres', startIndex: 2 },
216
+ );
217
+ expect(setClauses).toEqual(['"title" = $2', '"avatar" = NULL']);
218
+ expect(params).toEqual(['New Title']);
219
+ expect(nextParamIndex).toBe(3);
220
+ });
221
+ });
222
+
223
+ // ─── F. isOpObject detection ─────────────────────────────────────────────────
224
+
225
+ describe('parseUpdateBody — $op marker detection', () => {
226
+ it('plain object without $op is not treated as op', () => {
227
+ const { setClauses, params } = parseUpdateBody({ meta: { foo: 'bar' } });
228
+ // Without $op, it should be treated as regular JSON value
229
+ expect(setClauses[0]).toBe('"meta" = ?');
230
+ expect(params[0]).toEqual({ foo: 'bar' });
231
+ });
232
+
233
+ it('null value is not an op object', () => {
234
+ const { setClauses } = parseUpdateBody({ x: null });
235
+ expect(setClauses[0]).toBe('"x" = ?');
236
+ });
237
+
238
+ it('array value is not an op object', () => {
239
+ const { setClauses } = parseUpdateBody({ tags: ['a', 'b'] });
240
+ expect(setClauses[0]).toBe('"tags" = ?');
241
+ });
242
+ });