@gettersethya/mira 0.1.0-alpha.2

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 (338) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/app/app.d.ts +96 -0
  3. package/dist/src/app/app.d.ts.map +1 -0
  4. package/dist/src/app/app.js +139 -0
  5. package/dist/src/app/app.js.map +1 -0
  6. package/dist/src/app/builder.d.ts +106 -0
  7. package/dist/src/app/builder.d.ts.map +1 -0
  8. package/dist/src/app/builder.js +122 -0
  9. package/dist/src/app/builder.js.map +1 -0
  10. package/dist/src/app/index.d.ts +31 -0
  11. package/dist/src/app/index.d.ts.map +1 -0
  12. package/dist/src/app/index.js +29 -0
  13. package/dist/src/app/index.js.map +1 -0
  14. package/dist/src/app/types.d.ts +55 -0
  15. package/dist/src/app/types.d.ts.map +1 -0
  16. package/dist/src/app/types.js +2 -0
  17. package/dist/src/app/types.js.map +1 -0
  18. package/dist/src/cache/cached-collection.d.ts +30 -0
  19. package/dist/src/cache/cached-collection.d.ts.map +1 -0
  20. package/dist/src/cache/cached-collection.js +107 -0
  21. package/dist/src/cache/cached-collection.js.map +1 -0
  22. package/dist/src/cache/collection-cache.d.ts +17 -0
  23. package/dist/src/cache/collection-cache.d.ts.map +1 -0
  24. package/dist/src/cache/collection-cache.js +37 -0
  25. package/dist/src/cache/collection-cache.js.map +1 -0
  26. package/dist/src/cache/index.d.ts +4 -0
  27. package/dist/src/cache/index.d.ts.map +1 -0
  28. package/dist/src/cache/index.js +3 -0
  29. package/dist/src/cache/index.js.map +1 -0
  30. package/dist/src/cache/types.d.ts +12 -0
  31. package/dist/src/cache/types.d.ts.map +1 -0
  32. package/dist/src/cache/types.js +2 -0
  33. package/dist/src/cache/types.js.map +1 -0
  34. package/dist/src/collection-service/collection-service.d.ts +62 -0
  35. package/dist/src/collection-service/collection-service.d.ts.map +1 -0
  36. package/dist/src/collection-service/collection-service.js +267 -0
  37. package/dist/src/collection-service/collection-service.js.map +1 -0
  38. package/dist/src/collection-service/context.d.ts +17 -0
  39. package/dist/src/collection-service/context.d.ts.map +1 -0
  40. package/dist/src/collection-service/context.js +2 -0
  41. package/dist/src/collection-service/context.js.map +1 -0
  42. package/dist/src/collection-service/decode.d.ts +6 -0
  43. package/dist/src/collection-service/decode.d.ts.map +1 -0
  44. package/dist/src/collection-service/decode.js +26 -0
  45. package/dist/src/collection-service/decode.js.map +1 -0
  46. package/dist/src/collection-service/errors.d.ts +67 -0
  47. package/dist/src/collection-service/errors.d.ts.map +1 -0
  48. package/dist/src/collection-service/errors.js +43 -0
  49. package/dist/src/collection-service/errors.js.map +1 -0
  50. package/dist/src/collection-service/index.d.ts +5 -0
  51. package/dist/src/collection-service/index.d.ts.map +1 -0
  52. package/dist/src/collection-service/index.js +3 -0
  53. package/dist/src/collection-service/index.js.map +1 -0
  54. package/dist/src/collection-service/input-schema.d.ts +10 -0
  55. package/dist/src/collection-service/input-schema.d.ts.map +1 -0
  56. package/dist/src/collection-service/input-schema.js +109 -0
  57. package/dist/src/collection-service/input-schema.js.map +1 -0
  58. package/dist/src/collection-service/where.d.ts +35 -0
  59. package/dist/src/collection-service/where.d.ts.map +1 -0
  60. package/dist/src/collection-service/where.js +88 -0
  61. package/dist/src/collection-service/where.js.map +1 -0
  62. package/dist/src/config/config.d.ts +44 -0
  63. package/dist/src/config/config.d.ts.map +1 -0
  64. package/dist/src/config/config.js +89 -0
  65. package/dist/src/config/config.js.map +1 -0
  66. package/dist/src/config/index.d.ts +3 -0
  67. package/dist/src/config/index.d.ts.map +1 -0
  68. package/dist/src/config/index.js +2 -0
  69. package/dist/src/config/index.js.map +1 -0
  70. package/dist/src/config/types.d.ts +29 -0
  71. package/dist/src/config/types.d.ts.map +1 -0
  72. package/dist/src/config/types.js +2 -0
  73. package/dist/src/config/types.js.map +1 -0
  74. package/dist/src/crypto/crypto.d.ts +11 -0
  75. package/dist/src/crypto/crypto.d.ts.map +1 -0
  76. package/dist/src/crypto/crypto.js +4 -0
  77. package/dist/src/crypto/crypto.js.map +1 -0
  78. package/dist/src/crypto/index.d.ts +3 -0
  79. package/dist/src/crypto/index.d.ts.map +1 -0
  80. package/dist/src/crypto/index.js +3 -0
  81. package/dist/src/crypto/index.js.map +1 -0
  82. package/dist/src/crypto/node.d.ts +4 -0
  83. package/dist/src/crypto/node.d.ts.map +1 -0
  84. package/dist/src/crypto/node.js +9 -0
  85. package/dist/src/crypto/node.js.map +1 -0
  86. package/dist/src/databases/index.d.ts +2 -0
  87. package/dist/src/databases/index.d.ts.map +1 -0
  88. package/dist/src/databases/index.js +2 -0
  89. package/dist/src/databases/index.js.map +1 -0
  90. package/dist/src/databases/sqlite.d.ts +24 -0
  91. package/dist/src/databases/sqlite.d.ts.map +1 -0
  92. package/dist/src/databases/sqlite.js +27 -0
  93. package/dist/src/databases/sqlite.js.map +1 -0
  94. package/dist/src/http/auth-node.d.ts +4 -0
  95. package/dist/src/http/auth-node.d.ts.map +1 -0
  96. package/dist/src/http/auth-node.js +34 -0
  97. package/dist/src/http/auth-node.js.map +1 -0
  98. package/dist/src/http/auth.d.ts +38 -0
  99. package/dist/src/http/auth.d.ts.map +1 -0
  100. package/dist/src/http/auth.js +66 -0
  101. package/dist/src/http/auth.js.map +1 -0
  102. package/dist/src/http/errors.d.ts +31 -0
  103. package/dist/src/http/errors.d.ts.map +1 -0
  104. package/dist/src/http/errors.js +36 -0
  105. package/dist/src/http/errors.js.map +1 -0
  106. package/dist/src/http/file-serve.d.ts +10 -0
  107. package/dist/src/http/file-serve.d.ts.map +1 -0
  108. package/dist/src/http/file-serve.js +159 -0
  109. package/dist/src/http/file-serve.js.map +1 -0
  110. package/dist/src/http/file-token.d.ts +7 -0
  111. package/dist/src/http/file-token.d.ts.map +1 -0
  112. package/dist/src/http/file-token.js +55 -0
  113. package/dist/src/http/file-token.js.map +1 -0
  114. package/dist/src/http/files.d.ts +11 -0
  115. package/dist/src/http/files.d.ts.map +1 -0
  116. package/dist/src/http/files.js +85 -0
  117. package/dist/src/http/files.js.map +1 -0
  118. package/dist/src/http/index.d.ts +10 -0
  119. package/dist/src/http/index.d.ts.map +1 -0
  120. package/dist/src/http/index.js +8 -0
  121. package/dist/src/http/index.js.map +1 -0
  122. package/dist/src/http/ip-middleware.d.ts +4 -0
  123. package/dist/src/http/ip-middleware.d.ts.map +1 -0
  124. package/dist/src/http/ip-middleware.js +21 -0
  125. package/dist/src/http/ip-middleware.js.map +1 -0
  126. package/dist/src/http/params.d.ts +15 -0
  127. package/dist/src/http/params.d.ts.map +1 -0
  128. package/dist/src/http/params.js +48 -0
  129. package/dist/src/http/params.js.map +1 -0
  130. package/dist/src/http/router.d.ts +11 -0
  131. package/dist/src/http/router.d.ts.map +1 -0
  132. package/dist/src/http/router.js +216 -0
  133. package/dist/src/http/router.js.map +1 -0
  134. package/dist/src/http/server-factory-node.d.ts +4 -0
  135. package/dist/src/http/server-factory-node.d.ts.map +1 -0
  136. package/dist/src/http/server-factory-node.js +8 -0
  137. package/dist/src/http/server-factory-node.js.map +1 -0
  138. package/dist/src/http/server-factory.d.ts +11 -0
  139. package/dist/src/http/server-factory.d.ts.map +1 -0
  140. package/dist/src/http/server-factory.js +4 -0
  141. package/dist/src/http/server-factory.js.map +1 -0
  142. package/dist/src/index.d.ts +8 -0
  143. package/dist/src/index.d.ts.map +1 -0
  144. package/dist/src/index.js +10 -0
  145. package/dist/src/index.js.map +1 -0
  146. package/dist/src/migrator/dialect-sqlite.d.ts +9 -0
  147. package/dist/src/migrator/dialect-sqlite.d.ts.map +1 -0
  148. package/dist/src/migrator/dialect-sqlite.js +102 -0
  149. package/dist/src/migrator/dialect-sqlite.js.map +1 -0
  150. package/dist/src/migrator/dialect.d.ts +24 -0
  151. package/dist/src/migrator/dialect.d.ts.map +1 -0
  152. package/dist/src/migrator/dialect.js +5 -0
  153. package/dist/src/migrator/dialect.js.map +1 -0
  154. package/dist/src/migrator/index.d.ts +8 -0
  155. package/dist/src/migrator/index.d.ts.map +1 -0
  156. package/dist/src/migrator/index.js +6 -0
  157. package/dist/src/migrator/index.js.map +1 -0
  158. package/dist/src/migrator/migrator.d.ts +36 -0
  159. package/dist/src/migrator/migrator.d.ts.map +1 -0
  160. package/dist/src/migrator/migrator.js +200 -0
  161. package/dist/src/migrator/migrator.js.map +1 -0
  162. package/dist/src/migrator/schema-diff.d.ts +25 -0
  163. package/dist/src/migrator/schema-diff.d.ts.map +1 -0
  164. package/dist/src/migrator/schema-diff.js +157 -0
  165. package/dist/src/migrator/schema-diff.js.map +1 -0
  166. package/dist/src/migrator/types.d.ts +91 -0
  167. package/dist/src/migrator/types.d.ts.map +1 -0
  168. package/dist/src/migrator/types.js +13 -0
  169. package/dist/src/migrator/types.js.map +1 -0
  170. package/dist/src/platforms/node.d.ts +40 -0
  171. package/dist/src/platforms/node.d.ts.map +1 -0
  172. package/dist/src/platforms/node.js +45 -0
  173. package/dist/src/platforms/node.js.map +1 -0
  174. package/dist/src/repository/id.d.ts +5 -0
  175. package/dist/src/repository/id.d.ts.map +1 -0
  176. package/dist/src/repository/id.js +9 -0
  177. package/dist/src/repository/id.js.map +1 -0
  178. package/dist/src/repository/index.d.ts +3 -0
  179. package/dist/src/repository/index.d.ts.map +1 -0
  180. package/dist/src/repository/index.js +2 -0
  181. package/dist/src/repository/index.js.map +1 -0
  182. package/dist/src/repository/repository.d.ts +53 -0
  183. package/dist/src/repository/repository.d.ts.map +1 -0
  184. package/dist/src/repository/repository.js +150 -0
  185. package/dist/src/repository/repository.js.map +1 -0
  186. package/dist/src/repository/types.d.ts +42 -0
  187. package/dist/src/repository/types.d.ts.map +1 -0
  188. package/dist/src/repository/types.js +2 -0
  189. package/dist/src/repository/types.js.map +1 -0
  190. package/dist/src/rule/compiler.d.ts +52 -0
  191. package/dist/src/rule/compiler.d.ts.map +1 -0
  192. package/dist/src/rule/compiler.js +113 -0
  193. package/dist/src/rule/compiler.js.map +1 -0
  194. package/dist/src/rule/enforcer.d.ts +47 -0
  195. package/dist/src/rule/enforcer.d.ts.map +1 -0
  196. package/dist/src/rule/enforcer.js +60 -0
  197. package/dist/src/rule/enforcer.js.map +1 -0
  198. package/dist/src/rule/index.d.ts +3 -0
  199. package/dist/src/rule/index.d.ts.map +1 -0
  200. package/dist/src/rule/index.js +3 -0
  201. package/dist/src/rule/index.js.map +1 -0
  202. package/dist/src/storage/index.d.ts +3 -0
  203. package/dist/src/storage/index.d.ts.map +1 -0
  204. package/dist/src/storage/index.js +3 -0
  205. package/dist/src/storage/index.js.map +1 -0
  206. package/dist/src/storage/local-preset.d.ts +21 -0
  207. package/dist/src/storage/local-preset.d.ts.map +1 -0
  208. package/dist/src/storage/local-preset.js +21 -0
  209. package/dist/src/storage/local-preset.js.map +1 -0
  210. package/dist/src/storage/storage.d.ts +83 -0
  211. package/dist/src/storage/storage.d.ts.map +1 -0
  212. package/dist/src/storage/storage.js +120 -0
  213. package/dist/src/storage/storage.js.map +1 -0
  214. package/dist/src/telemetry/index.d.ts +6 -0
  215. package/dist/src/telemetry/index.d.ts.map +1 -0
  216. package/dist/src/telemetry/index.js +7 -0
  217. package/dist/src/telemetry/index.js.map +1 -0
  218. package/dist/src/telemetry/logger.d.ts +4 -0
  219. package/dist/src/telemetry/logger.d.ts.map +1 -0
  220. package/dist/src/telemetry/logger.js +27 -0
  221. package/dist/src/telemetry/logger.js.map +1 -0
  222. package/dist/src/telemetry/tracer.d.ts +16 -0
  223. package/dist/src/telemetry/tracer.d.ts.map +1 -0
  224. package/dist/src/telemetry/tracer.js +106 -0
  225. package/dist/src/telemetry/tracer.js.map +1 -0
  226. package/dist/src/thumbnail/index.d.ts +4 -0
  227. package/dist/src/thumbnail/index.d.ts.map +1 -0
  228. package/dist/src/thumbnail/index.js +3 -0
  229. package/dist/src/thumbnail/index.js.map +1 -0
  230. package/dist/src/thumbnail/photon.d.ts +9 -0
  231. package/dist/src/thumbnail/photon.d.ts.map +1 -0
  232. package/dist/src/thumbnail/photon.js +73 -0
  233. package/dist/src/thumbnail/photon.js.map +1 -0
  234. package/dist/src/thumbnail/types.d.ts +23 -0
  235. package/dist/src/thumbnail/types.d.ts.map +1 -0
  236. package/dist/src/thumbnail/types.js +22 -0
  237. package/dist/src/thumbnail/types.js.map +1 -0
  238. package/dist/tests/app/app.test.d.ts +2 -0
  239. package/dist/tests/app/app.test.d.ts.map +1 -0
  240. package/dist/tests/app/app.test.js +78 -0
  241. package/dist/tests/app/app.test.js.map +1 -0
  242. package/dist/tests/app/builder.test.d.ts +2 -0
  243. package/dist/tests/app/builder.test.d.ts.map +1 -0
  244. package/dist/tests/app/builder.test.js +69 -0
  245. package/dist/tests/app/builder.test.js.map +1 -0
  246. package/dist/tests/cache/cached-collection.test.d.ts +2 -0
  247. package/dist/tests/cache/cached-collection.test.d.ts.map +1 -0
  248. package/dist/tests/cache/cached-collection.test.js +244 -0
  249. package/dist/tests/cache/cached-collection.test.js.map +1 -0
  250. package/dist/tests/cache/collection-cache.test.d.ts +2 -0
  251. package/dist/tests/cache/collection-cache.test.d.ts.map +1 -0
  252. package/dist/tests/cache/collection-cache.test.js +73 -0
  253. package/dist/tests/cache/collection-cache.test.js.map +1 -0
  254. package/dist/tests/collection-service/collection-service.test.d.ts +2 -0
  255. package/dist/tests/collection-service/collection-service.test.d.ts.map +1 -0
  256. package/dist/tests/collection-service/collection-service.test.js +805 -0
  257. package/dist/tests/collection-service/collection-service.test.js.map +1 -0
  258. package/dist/tests/collection-service/input-schema.test.d.ts +2 -0
  259. package/dist/tests/collection-service/input-schema.test.d.ts.map +1 -0
  260. package/dist/tests/collection-service/input-schema.test.js +260 -0
  261. package/dist/tests/collection-service/input-schema.test.js.map +1 -0
  262. package/dist/tests/config/config.test.d.ts +2 -0
  263. package/dist/tests/config/config.test.d.ts.map +1 -0
  264. package/dist/tests/config/config.test.js +84 -0
  265. package/dist/tests/config/config.test.js.map +1 -0
  266. package/dist/tests/crypto/crypto.test.d.ts +2 -0
  267. package/dist/tests/crypto/crypto.test.d.ts.map +1 -0
  268. package/dist/tests/crypto/crypto.test.js +56 -0
  269. package/dist/tests/crypto/crypto.test.js.map +1 -0
  270. package/dist/tests/http/file-serve.test.d.ts +2 -0
  271. package/dist/tests/http/file-serve.test.d.ts.map +1 -0
  272. package/dist/tests/http/file-serve.test.js +210 -0
  273. package/dist/tests/http/file-serve.test.js.map +1 -0
  274. package/dist/tests/http/file-token.test.d.ts +2 -0
  275. package/dist/tests/http/file-token.test.d.ts.map +1 -0
  276. package/dist/tests/http/file-token.test.js +155 -0
  277. package/dist/tests/http/file-token.test.js.map +1 -0
  278. package/dist/tests/http/ip-middleware.test.d.ts +2 -0
  279. package/dist/tests/http/ip-middleware.test.d.ts.map +1 -0
  280. package/dist/tests/http/ip-middleware.test.js +58 -0
  281. package/dist/tests/http/ip-middleware.test.js.map +1 -0
  282. package/dist/tests/http/router.test.d.ts +2 -0
  283. package/dist/tests/http/router.test.d.ts.map +1 -0
  284. package/dist/tests/http/router.test.js +252 -0
  285. package/dist/tests/http/router.test.js.map +1 -0
  286. package/dist/tests/migrator/dialect-sqlite.test.d.ts +2 -0
  287. package/dist/tests/migrator/dialect-sqlite.test.d.ts.map +1 -0
  288. package/dist/tests/migrator/dialect-sqlite.test.js +90 -0
  289. package/dist/tests/migrator/dialect-sqlite.test.js.map +1 -0
  290. package/dist/tests/migrator/integration.test.d.ts +2 -0
  291. package/dist/tests/migrator/integration.test.d.ts.map +1 -0
  292. package/dist/tests/migrator/integration.test.js +180 -0
  293. package/dist/tests/migrator/integration.test.js.map +1 -0
  294. package/dist/tests/migrator/migrator.test.d.ts +2 -0
  295. package/dist/tests/migrator/migrator.test.d.ts.map +1 -0
  296. package/dist/tests/migrator/migrator.test.js +225 -0
  297. package/dist/tests/migrator/migrator.test.js.map +1 -0
  298. package/dist/tests/migrator/schema-diff.test.d.ts +2 -0
  299. package/dist/tests/migrator/schema-diff.test.d.ts.map +1 -0
  300. package/dist/tests/migrator/schema-diff.test.js +323 -0
  301. package/dist/tests/migrator/schema-diff.test.js.map +1 -0
  302. package/dist/tests/repository/repository.test.d.ts +2 -0
  303. package/dist/tests/repository/repository.test.d.ts.map +1 -0
  304. package/dist/tests/repository/repository.test.js +255 -0
  305. package/dist/tests/repository/repository.test.js.map +1 -0
  306. package/dist/tests/rule/compiler.test.d.ts +2 -0
  307. package/dist/tests/rule/compiler.test.d.ts.map +1 -0
  308. package/dist/tests/rule/compiler.test.js +226 -0
  309. package/dist/tests/rule/compiler.test.js.map +1 -0
  310. package/dist/tests/rule/enforcer.test.d.ts +2 -0
  311. package/dist/tests/rule/enforcer.test.d.ts.map +1 -0
  312. package/dist/tests/rule/enforcer.test.js +190 -0
  313. package/dist/tests/rule/enforcer.test.js.map +1 -0
  314. package/dist/tests/storage/storage.test.d.ts +2 -0
  315. package/dist/tests/storage/storage.test.d.ts.map +1 -0
  316. package/dist/tests/storage/storage.test.js +76 -0
  317. package/dist/tests/storage/storage.test.js.map +1 -0
  318. package/dist/tests/telemetry/instrumentation.test.d.ts +2 -0
  319. package/dist/tests/telemetry/instrumentation.test.d.ts.map +1 -0
  320. package/dist/tests/telemetry/instrumentation.test.js +226 -0
  321. package/dist/tests/telemetry/instrumentation.test.js.map +1 -0
  322. package/dist/tests/telemetry/logger.test.d.ts +2 -0
  323. package/dist/tests/telemetry/logger.test.d.ts.map +1 -0
  324. package/dist/tests/telemetry/logger.test.js +56 -0
  325. package/dist/tests/telemetry/logger.test.js.map +1 -0
  326. package/dist/tests/telemetry/tracer.test.d.ts +2 -0
  327. package/dist/tests/telemetry/tracer.test.d.ts.map +1 -0
  328. package/dist/tests/telemetry/tracer.test.js +63 -0
  329. package/dist/tests/telemetry/tracer.test.js.map +1 -0
  330. package/dist/tests/thumbnail/photon.test.d.ts +2 -0
  331. package/dist/tests/thumbnail/photon.test.d.ts.map +1 -0
  332. package/dist/tests/thumbnail/photon.test.js +478 -0
  333. package/dist/tests/thumbnail/photon.test.js.map +1 -0
  334. package/dist/tests/thumbnail/thumbnail.test.d.ts +2 -0
  335. package/dist/tests/thumbnail/thumbnail.test.d.ts.map +1 -0
  336. package/dist/tests/thumbnail/thumbnail.test.js +35 -0
  337. package/dist/tests/thumbnail/thumbnail.test.js.map +1 -0
  338. package/package.json +46 -0
@@ -0,0 +1,805 @@
1
+ import { SqlClient } from "@effect/sql";
2
+ import { SqliteClient } from "@effect/sql-sqlite-node";
3
+ import { Effect, Either, Layer, Match } from "effect";
4
+ import { describe, it } from "@effect/vitest";
5
+ import { expect } from "vitest";
6
+ import { BaseCollection } from "@gettersethya/mira-client";
7
+ import { Field } from "@gettersethya/mira-client";
8
+ import { ViewCollection } from "@gettersethya/mira-client";
9
+ import { RepositoryLive } from "../../src/repository/repository.js";
10
+ import { CollectionService, makeCollectionServiceLayer } from "../../src/collection-service/collection-service.js";
11
+ import { FileStorage, FileStorageNotFound } from "../../src/storage/storage.js";
12
+ import { NodeCryptoLayer } from "../../src/crypto/node.js";
13
+ // ---------------------------------------------------------------------------
14
+ // Test layer
15
+ // ---------------------------------------------------------------------------
16
+ const FileStorageTest = Layer.succeed(FileStorage, FileStorage.of({
17
+ upload: (key) => Effect.succeed(key),
18
+ delete: () => Effect.void,
19
+ url: (key) => `/files/${key}`,
20
+ read: (key) => Effect.fail(new FileStorageNotFound({ key })),
21
+ exists: () => Effect.succeed(false),
22
+ list: () => Effect.succeed([])
23
+ }));
24
+ const sqliteLayer = SqliteClient.layer({ filename: ":memory:" });
25
+ const collectionServiceWithDeps = makeCollectionServiceLayer([]).pipe(Layer.provide(RepositoryLive), Layer.provide(sqliteLayer), Layer.provide(FileStorageTest), Layer.provide(NodeCryptoLayer));
26
+ const testLayer = Layer.mergeAll(collectionServiceWithDeps, sqliteLayer, FileStorageTest, NodeCryptoLayer);
27
+ const noCtx = { headers: {}, query: {} };
28
+ // ---------------------------------------------------------------------------
29
+ // Table setup helpers
30
+ // seqId as INTEGER PRIMARY KEY AUTOINCREMENT so SQLite fills it on INSERT.
31
+ // ---------------------------------------------------------------------------
32
+ const setupPostsTable = Effect.gen(function* () {
33
+ const sql = yield* SqlClient.SqlClient;
34
+ yield* sql.unsafe(`
35
+ CREATE TABLE IF NOT EXISTS "posts" (
36
+ "seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ "id" TEXT NOT NULL UNIQUE,
38
+ "title" TEXT NOT NULL,
39
+ "published" INTEGER NOT NULL DEFAULT 0,
40
+ "authorId" TEXT,
41
+ "created" TEXT NOT NULL,
42
+ "updated" TEXT NOT NULL
43
+ )
44
+ `);
45
+ yield* sql.unsafe(`DELETE FROM "posts"`);
46
+ });
47
+ // View collection tests: a base table + a SQLite VIEW over it
48
+ const setupViewTables = Effect.gen(function* () {
49
+ const sql = yield* SqlClient.SqlClient;
50
+ yield* sql.unsafe(`
51
+ CREATE TABLE IF NOT EXISTS "posts_base" (
52
+ "seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ "id" TEXT NOT NULL UNIQUE,
54
+ "title" TEXT NOT NULL,
55
+ "status" TEXT NOT NULL DEFAULT 'draft',
56
+ "created" TEXT NOT NULL,
57
+ "updated" TEXT NOT NULL
58
+ )
59
+ `);
60
+ yield* sql.unsafe(`DELETE FROM "posts_base"`);
61
+ yield* sql.unsafe(`DROP VIEW IF EXISTS "active_posts"`);
62
+ yield* sql.unsafe(`
63
+ CREATE VIEW "active_posts" AS
64
+ SELECT seqId, id, title, status, created, updated FROM posts_base WHERE status = 'active'
65
+ `);
66
+ });
67
+ const setupScoredTable = Effect.gen(function* () {
68
+ const sql = yield* SqlClient.SqlClient;
69
+ yield* sql.unsafe(`
70
+ CREATE TABLE IF NOT EXISTS "scored" (
71
+ "seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ "id" TEXT NOT NULL UNIQUE,
73
+ "name" TEXT NOT NULL,
74
+ "score" REAL NOT NULL,
75
+ "created" TEXT NOT NULL,
76
+ "updated" TEXT NOT NULL
77
+ )
78
+ `);
79
+ yield* sql.unsafe(`DELETE FROM "scored"`);
80
+ });
81
+ // ---------------------------------------------------------------------------
82
+ // Collection definitions
83
+ // ---------------------------------------------------------------------------
84
+ // Base collection — all operations public
85
+ const Posts = BaseCollection.define("posts", {
86
+ title: Field.text({ maxLength: 100 }),
87
+ published: Field.boolean({ default: false }),
88
+ authorId: Field.text({ required: false })
89
+ }).rules((R) => ({
90
+ list: R.public(),
91
+ view: R.public(),
92
+ create: R.public(),
93
+ update: R.public(),
94
+ delete: R.public()
95
+ }));
96
+ // Base collection — rule-enforced: only authorId "user1" can access
97
+ const ProtectedPosts = BaseCollection.define("posts", {
98
+ title: Field.text(),
99
+ published: Field.boolean({ default: false }),
100
+ authorId: Field.text({ required: false })
101
+ }).rules((R) => ({
102
+ list: R.field("authorId").eq(R.literal("user1")),
103
+ view: R.field("authorId").eq(R.literal("user1")),
104
+ create: R.field("authorId").eq(R.literal("user1")),
105
+ update: R.field("authorId").eq(R.literal("user1")),
106
+ delete: R.field("authorId").eq(R.literal("user1"))
107
+ }));
108
+ // Base collection — no rules (deny all by default)
109
+ const NoRulePosts = BaseCollection.define("posts", {
110
+ title: Field.text()
111
+ });
112
+ // View collection — public list/view, no create/update/delete
113
+ const ActivePosts = ViewCollection.define("active_posts", "SELECT seqId, id, title, status, created, updated FROM posts_base WHERE status = 'active'", {
114
+ seqId: Field.integer().view(),
115
+ id: Field.text().view(),
116
+ title: Field.text().view(),
117
+ status: Field.text().view()
118
+ }).rules((R) => ({
119
+ list: R.public(),
120
+ view: R.public()
121
+ }));
122
+ // Collection with error callbacks for integration testing
123
+ const Scored = BaseCollection.define("scored", {
124
+ name: Field.text({
125
+ minLength: 2,
126
+ error: (kind) => Match.value(kind).pipe(Match.when("minLength", () => "Name too short"), Match.when("required", () => "Name is required"), Match.orElse(() => "Invalid name"))
127
+ }),
128
+ score: Field.number({
129
+ min: 0,
130
+ max: 100,
131
+ error: (kind) => Match.value(kind).pipe(Match.when("minimum", () => "Score below zero"), Match.when("maximum", () => "Score above 100"), Match.orElse(() => "Invalid score"))
132
+ })
133
+ }).rules((R) => ({
134
+ create: R.public(),
135
+ update: R.public(),
136
+ list: R.public(),
137
+ view: R.public(),
138
+ delete: R.public()
139
+ }));
140
+ // ScoredServiceLayer wires Scored into the collection service
141
+ const scoredServiceWithDeps = makeCollectionServiceLayer([Scored]).pipe(Layer.provide(RepositoryLive), Layer.provide(sqliteLayer), Layer.provide(FileStorageTest), Layer.provide(NodeCryptoLayer));
142
+ const scoredTestLayer = Layer.mergeAll(scoredServiceWithDeps, sqliteLayer, FileStorageTest, NodeCryptoLayer);
143
+ // ---------------------------------------------------------------------------
144
+ // Helpers
145
+ // ---------------------------------------------------------------------------
146
+ function isCursorPage(r) {
147
+ return typeof r === "object" && r !== null && "nextCursor" in r;
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // list
151
+ // ---------------------------------------------------------------------------
152
+ describe("list", () => {
153
+ it.effect("public base collection returns CursorPage shape", () => Effect.gen(function* () {
154
+ yield* setupPostsTable;
155
+ const svc = yield* CollectionService;
156
+ const result = yield* svc.list(Posts, null, 10, noCtx);
157
+ expect(isCursorPage(result)).toBe(true);
158
+ }).pipe(Effect.provide(testLayer)));
159
+ it.effect("returns all inserted rows", () => Effect.gen(function* () {
160
+ yield* setupPostsTable;
161
+ const svc = yield* CollectionService;
162
+ yield* svc.create(Posts, { title: "A" }, noCtx);
163
+ yield* svc.create(Posts, { title: "B" }, noCtx);
164
+ const result = yield* svc.list(Posts, null, 10, noCtx);
165
+ expect(isCursorPage(result) && result.items.length).toBe(2);
166
+ }).pipe(Effect.provide(testLayer)));
167
+ it.effect("seqId is NOT present in list items (x-hidden stripped)", () => Effect.gen(function* () {
168
+ yield* setupPostsTable;
169
+ const svc = yield* CollectionService;
170
+ yield* svc.create(Posts, { title: "X" }, noCtx);
171
+ const result = yield* svc.list(Posts, null, 10, noCtx);
172
+ if (!isCursorPage(result))
173
+ throw new Error("expected CursorPage");
174
+ expect("seqId" in result.items[0]).toBe(false);
175
+ }).pipe(Effect.provide(testLayer)));
176
+ it.effect("nextCursor is null when page is not full", () => Effect.gen(function* () {
177
+ yield* setupPostsTable;
178
+ const svc = yield* CollectionService;
179
+ yield* svc.create(Posts, { title: "A" }, noCtx);
180
+ const result = yield* svc.list(Posts, null, 10, noCtx);
181
+ if (!isCursorPage(result))
182
+ throw new Error("expected CursorPage");
183
+ expect(result.nextCursor).toBeNull();
184
+ }).pipe(Effect.provide(testLayer)));
185
+ it.effect("nextCursor is a number when page is full", () => Effect.gen(function* () {
186
+ yield* setupPostsTable;
187
+ const svc = yield* CollectionService;
188
+ yield* svc.create(Posts, { title: "A" }, noCtx);
189
+ yield* svc.create(Posts, { title: "B" }, noCtx);
190
+ yield* svc.create(Posts, { title: "C" }, noCtx);
191
+ const result = yield* svc.list(Posts, null, 2, noCtx);
192
+ if (!isCursorPage(result))
193
+ throw new Error("expected CursorPage");
194
+ expect(typeof result.nextCursor).toBe("number");
195
+ }).pipe(Effect.provide(testLayer)));
196
+ it.effect("cursor pagination: page 2 fetches rows not in page 1", () => Effect.gen(function* () {
197
+ yield* setupPostsTable;
198
+ const svc = yield* CollectionService;
199
+ yield* svc.create(Posts, { title: "A" }, noCtx);
200
+ yield* svc.create(Posts, { title: "B" }, noCtx);
201
+ yield* svc.create(Posts, { title: "C" }, noCtx);
202
+ yield* svc.create(Posts, { title: "D" }, noCtx);
203
+ yield* svc.create(Posts, { title: "E" }, noCtx);
204
+ const page1 = yield* svc.list(Posts, null, 2, noCtx);
205
+ if (!isCursorPage(page1))
206
+ throw new Error("expected CursorPage");
207
+ expect(page1.items.length).toBe(2);
208
+ expect(page1.nextCursor).not.toBeNull();
209
+ if (page1.nextCursor === null)
210
+ throw new Error("expected nextCursor");
211
+ const page2 = yield* svc.list(Posts, page1.nextCursor, 2, noCtx);
212
+ if (!isCursorPage(page2))
213
+ throw new Error("expected CursorPage");
214
+ expect(page2.items.length).toBe(2);
215
+ if (page2.nextCursor === null)
216
+ throw new Error("expected nextCursor");
217
+ const page3 = yield* svc.list(Posts, page2.nextCursor, 2, noCtx);
218
+ if (!isCursorPage(page3))
219
+ throw new Error("expected CursorPage");
220
+ expect(page3.items.length).toBe(1);
221
+ expect(page3.nextCursor).toBeNull();
222
+ const ids1 = page1.items.map((r) => r["id"]);
223
+ const ids2 = page2.items.map((r) => r["id"]);
224
+ expect(ids1.some((id) => ids2.includes(id))).toBe(false);
225
+ }).pipe(Effect.provide(testLayer)));
226
+ it.effect("rule-enforced list returns only matching rows", () => Effect.gen(function* () {
227
+ yield* setupPostsTable;
228
+ const svc = yield* CollectionService;
229
+ yield* svc.create(Posts, { title: "Mine", authorId: "user1" }, noCtx);
230
+ yield* svc.create(Posts, { title: "Theirs", authorId: "user2" }, noCtx);
231
+ const result = yield* svc.list(ProtectedPosts, null, 10, noCtx);
232
+ if (!isCursorPage(result))
233
+ throw new Error("expected CursorPage");
234
+ expect(result.items.length).toBe(1);
235
+ expect(result.items[0]["title"]).toBe("Mine");
236
+ }).pipe(Effect.provide(testLayer)));
237
+ it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
238
+ yield* setupPostsTable;
239
+ const svc = yield* CollectionService;
240
+ const result = yield* svc.list(NoRulePosts, null, 10, noCtx).pipe(Effect.either);
241
+ expect(Either.isLeft(result)).toBe(true);
242
+ if (Either.isLeft(result)) {
243
+ expect(result.left._tag).toBe("ForbiddenError");
244
+ if (result.left._tag === "ForbiddenError") {
245
+ expect(result.left.action).toBe("list");
246
+ }
247
+ }
248
+ }).pipe(Effect.provide(testLayer)));
249
+ it.effect("view collection returns CursorPage shape", () => Effect.gen(function* () {
250
+ yield* setupViewTables;
251
+ const svc = yield* CollectionService;
252
+ const result = yield* svc.list(ActivePosts, null, 10, noCtx);
253
+ expect(isCursorPage(result)).toBe(true);
254
+ }).pipe(Effect.provide(testLayer)));
255
+ it.effect("view collection list returns only rows from the view", () => Effect.gen(function* () {
256
+ yield* setupViewTables;
257
+ const sql = yield* SqlClient.SqlClient;
258
+ // Insert directly into posts_base since repository uses different table name
259
+ yield* sql.unsafe(`INSERT INTO posts_base (id, title, status, created, updated) VALUES (?, ?, ?, datetime('now'), datetime('now'))`, ["id1", "Active Post", "active"]);
260
+ yield* sql.unsafe(`INSERT INTO posts_base (id, title, status, created, updated) VALUES (?, ?, ?, datetime('now'), datetime('now'))`, ["id2", "Draft Post", "draft"]);
261
+ const svc = yield* CollectionService;
262
+ const result = yield* svc.list(ActivePosts, null, 10, noCtx);
263
+ if (!isCursorPage(result))
264
+ throw new Error("expected CursorPage");
265
+ expect(result.items.length).toBe(1);
266
+ expect(result.items[0]["title"]).toBe("Active Post");
267
+ }).pipe(Effect.provide(testLayer)));
268
+ });
269
+ // ---------------------------------------------------------------------------
270
+ // list with filter
271
+ // ---------------------------------------------------------------------------
272
+ describe("list with filter", () => {
273
+ it.effect("filters by eq on string field", () => Effect.gen(function* () {
274
+ yield* setupPostsTable;
275
+ const svc = yield* CollectionService;
276
+ yield* svc.create(Posts, { title: "Apple" }, noCtx);
277
+ yield* svc.create(Posts, { title: "Banana" }, noCtx);
278
+ yield* svc.create(Posts, { title: "Apple Pie" }, noCtx);
279
+ const result = yield* svc.list(Posts, null, 10, noCtx, {
280
+ op: "like",
281
+ field: "title",
282
+ value: "Apple%"
283
+ });
284
+ if (!isCursorPage(result))
285
+ throw new Error("expected CursorPage");
286
+ expect(result.items.length).toBe(2);
287
+ }).pipe(Effect.provide(testLayer)));
288
+ it.effect("filters by gt on integer field", () => Effect.gen(function* () {
289
+ yield* setupPostsTable;
290
+ const svc = yield* CollectionService;
291
+ yield* svc.create(Posts, { title: "A", authorId: "10" }, noCtx);
292
+ yield* svc.create(Posts, { title: "B", authorId: "20" }, noCtx);
293
+ yield* svc.create(Posts, { title: "C", authorId: "30" }, noCtx);
294
+ // authorId is text but we can still compare with gt textually
295
+ const result = yield* svc.list(Posts, null, 10, noCtx, {
296
+ op: "gt",
297
+ field: "authorId",
298
+ value: "20"
299
+ });
300
+ if (!isCursorPage(result))
301
+ throw new Error("expected CursorPage");
302
+ expect(result.items.length).toBe(1);
303
+ expect(result.items[0]["title"]).toBe("C");
304
+ }).pipe(Effect.provide(testLayer)));
305
+ it.effect("and filter combines conditions", () => Effect.gen(function* () {
306
+ yield* setupPostsTable;
307
+ const svc = yield* CollectionService;
308
+ yield* svc.create(Posts, { title: "Alpha", authorId: "user1" }, noCtx);
309
+ yield* svc.create(Posts, { title: "Beta", authorId: "user2" }, noCtx);
310
+ yield* svc.create(Posts, { title: "Gamma", authorId: "user1" }, noCtx);
311
+ // Filter: title starts with "A" AND authorId = "user1"
312
+ // We can approximate startsWith using like
313
+ const result = yield* svc.list(Posts, null, 10, noCtx, {
314
+ op: "and",
315
+ left: { op: "like", field: "title", value: "A%" },
316
+ right: { op: "eq", field: "authorId", value: "user1" }
317
+ });
318
+ if (!isCursorPage(result))
319
+ throw new Error("expected CursorPage");
320
+ expect(result.items.length).toBe(1);
321
+ expect(result.items[0]["title"]).toBe("Alpha");
322
+ }).pipe(Effect.provide(testLayer)));
323
+ it.effect("filter is ANDed with rule enforcement", () => Effect.gen(function* () {
324
+ yield* setupPostsTable;
325
+ const svc = yield* CollectionService;
326
+ yield* svc.create(Posts, { title: "X", authorId: "user1" }, noCtx);
327
+ yield* svc.create(Posts, { title: "Y", authorId: "user1" }, noCtx);
328
+ yield* svc.create(Posts, { title: "Z", authorId: "user2" }, noCtx);
329
+ // ProtectedPosts rule: authorId = "user1"
330
+ // Filter: title like "X" → should return only "X" (which is user1's)
331
+ const result = yield* svc.list(ProtectedPosts, null, 10, noCtx, {
332
+ op: "like",
333
+ field: "title",
334
+ value: "X"
335
+ });
336
+ if (!isCursorPage(result))
337
+ throw new Error("expected CursorPage");
338
+ expect(result.items.length).toBe(1);
339
+ expect(result.items[0]["title"]).toBe("X");
340
+ }).pipe(Effect.provide(testLayer)));
341
+ it.effect("in filter works", () => Effect.gen(function* () {
342
+ yield* setupPostsTable;
343
+ const svc = yield* CollectionService;
344
+ yield* svc.create(Posts, { title: "Red" }, noCtx);
345
+ yield* svc.create(Posts, { title: "Green" }, noCtx);
346
+ yield* svc.create(Posts, { title: "Blue" }, noCtx);
347
+ const result = yield* svc.list(Posts, null, 10, noCtx, {
348
+ op: "in",
349
+ field: "title",
350
+ values: ["Red", "Blue"]
351
+ });
352
+ if (!isCursorPage(result))
353
+ throw new Error("expected CursorPage");
354
+ expect(result.items.length).toBe(2);
355
+ const titles = result.items.map((r) => r["title"]).sort();
356
+ expect(titles).toEqual(["Blue", "Red"]);
357
+ }).pipe(Effect.provide(testLayer)));
358
+ it.effect("or filter works", () => Effect.gen(function* () {
359
+ yield* setupPostsTable;
360
+ const svc = yield* CollectionService;
361
+ yield* svc.create(Posts, { title: "Cat", authorId: "a" }, noCtx);
362
+ yield* svc.create(Posts, { title: "Dog", authorId: "b" }, noCtx);
363
+ yield* svc.create(Posts, { title: "Fish", authorId: "c" }, noCtx);
364
+ // title = "Cat" OR authorId = "b"
365
+ const result = yield* svc.list(Posts, null, 10, noCtx, {
366
+ op: "or",
367
+ left: { op: "eq", field: "title", value: "Cat" },
368
+ right: { op: "eq", field: "authorId", value: "b" }
369
+ });
370
+ if (!isCursorPage(result))
371
+ throw new Error("expected CursorPage");
372
+ expect(result.items.length).toBe(2);
373
+ const titles = result.items.map((r) => r["title"]).sort();
374
+ expect(titles).toEqual(["Cat", "Dog"]);
375
+ }).pipe(Effect.provide(testLayer)));
376
+ it.effect("not filter works", () => Effect.gen(function* () {
377
+ yield* setupPostsTable;
378
+ const svc = yield* CollectionService;
379
+ yield* svc.create(Posts, { title: "Keep" }, noCtx);
380
+ yield* svc.create(Posts, { title: "Skip" }, noCtx);
381
+ yield* svc.create(Posts, { title: "Keep2" }, noCtx);
382
+ // NOT (title = "Skip")
383
+ const result = yield* svc.list(Posts, null, 10, noCtx, {
384
+ op: "not",
385
+ node: { op: "eq", field: "title", value: "Skip" }
386
+ });
387
+ if (!isCursorPage(result))
388
+ throw new Error("expected CursorPage");
389
+ expect(result.items.length).toBe(2);
390
+ const titles = result.items.map((r) => r["title"]);
391
+ expect(titles).not.toContain("Skip");
392
+ }).pipe(Effect.provide(testLayer)));
393
+ });
394
+ // ---------------------------------------------------------------------------
395
+ // view
396
+ // ---------------------------------------------------------------------------
397
+ describe("view", () => {
398
+ it.effect("returns the row for an existing id", () => Effect.gen(function* () {
399
+ yield* setupPostsTable;
400
+ const svc = yield* CollectionService;
401
+ const created = yield* svc.create(Posts, { title: "Hello" }, noCtx);
402
+ const id = created["id"];
403
+ if (typeof id !== "string")
404
+ throw new Error("expected string id");
405
+ const result = yield* svc.view(Posts, id, noCtx);
406
+ expect(result["title"]).toBe("Hello");
407
+ }).pipe(Effect.provide(testLayer)));
408
+ it.effect("returns NotFoundError for an unknown id", () => Effect.gen(function* () {
409
+ yield* setupPostsTable;
410
+ const svc = yield* CollectionService;
411
+ const result = yield* svc.view(Posts, "nonexistent123", noCtx).pipe(Effect.either);
412
+ expect(Either.isLeft(result)).toBe(true);
413
+ if (Either.isLeft(result)) {
414
+ expect(result.left._tag).toBe("NotFoundError");
415
+ }
416
+ }).pipe(Effect.provide(testLayer)));
417
+ it.effect("seqId is NOT in the returned row", () => Effect.gen(function* () {
418
+ yield* setupPostsTable;
419
+ const svc = yield* CollectionService;
420
+ const created = yield* svc.create(Posts, { title: "Hidden" }, noCtx);
421
+ const id = created["id"];
422
+ if (typeof id !== "string")
423
+ throw new Error("expected string id");
424
+ const result = yield* svc.view(Posts, id, noCtx);
425
+ expect("seqId" in result).toBe(false);
426
+ }).pipe(Effect.provide(testLayer)));
427
+ it.effect("row not matching rule → NotFoundError (indistinguishable from missing)", () => Effect.gen(function* () {
428
+ yield* setupPostsTable;
429
+ const svc = yield* CollectionService;
430
+ const created = yield* svc.create(Posts, { title: "Other", authorId: "user2" }, noCtx);
431
+ const id = created["id"];
432
+ if (typeof id !== "string")
433
+ throw new Error("expected string id");
434
+ const result = yield* svc.view(ProtectedPosts, id, noCtx).pipe(Effect.either);
435
+ expect(Either.isLeft(result)).toBe(true);
436
+ if (Either.isLeft(result)) {
437
+ expect(result.left._tag).toBe("NotFoundError");
438
+ }
439
+ }).pipe(Effect.provide(testLayer)));
440
+ it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
441
+ yield* setupPostsTable;
442
+ const svc = yield* CollectionService;
443
+ const result = yield* svc.view(NoRulePosts, "any", noCtx).pipe(Effect.either);
444
+ expect(Either.isLeft(result)).toBe(true);
445
+ if (Either.isLeft(result)) {
446
+ expect(result.left._tag).toBe("ForbiddenError");
447
+ }
448
+ }).pipe(Effect.provide(testLayer)));
449
+ });
450
+ // ---------------------------------------------------------------------------
451
+ // create
452
+ // ---------------------------------------------------------------------------
453
+ describe("create", () => {
454
+ it.effect("inserts a valid row and returns it with id, created, updated", () => Effect.gen(function* () {
455
+ yield* setupPostsTable;
456
+ const svc = yield* CollectionService;
457
+ const result = yield* svc.create(Posts, { title: "New Post" }, noCtx);
458
+ expect(typeof result["id"]).toBe("string");
459
+ expect(result["title"]).toBe("New Post");
460
+ expect(typeof result["created"]).toBe("string");
461
+ expect(typeof result["updated"]).toBe("string");
462
+ }).pipe(Effect.provide(testLayer)));
463
+ it.effect("seqId is NOT in the returned row", () => Effect.gen(function* () {
464
+ yield* setupPostsTable;
465
+ const svc = yield* CollectionService;
466
+ const result = yield* svc.create(Posts, { title: "X" }, noCtx);
467
+ expect("seqId" in result).toBe(false);
468
+ }).pipe(Effect.provide(testLayer)));
469
+ it.effect("ValidationError when required field is missing", () => Effect.gen(function* () {
470
+ yield* setupPostsTable;
471
+ const svc = yield* CollectionService;
472
+ const result = yield* svc.create(Posts, { published: true }, noCtx).pipe(Effect.either);
473
+ expect(Either.isLeft(result)).toBe(true);
474
+ if (Either.isLeft(result)) {
475
+ expect(result.left._tag).toBe("ValidationError");
476
+ if (result.left._tag === "ValidationError") {
477
+ expect(result.left.issues.some((i) => i.includes("title"))).toBe(true);
478
+ }
479
+ }
480
+ }).pipe(Effect.provide(testLayer)));
481
+ it.effect("ValidationError when x-system field is in input", () => Effect.gen(function* () {
482
+ yield* setupPostsTable;
483
+ const svc = yield* CollectionService;
484
+ const result = yield* svc.create(Posts, { title: "X", id: "hacked" }, noCtx).pipe(Effect.either);
485
+ expect(Either.isLeft(result)).toBe(true);
486
+ if (Either.isLeft(result)) {
487
+ expect(result.left._tag).toBe("ValidationError");
488
+ if (result.left._tag === "ValidationError") {
489
+ expect(result.left.issues.some((i) => i.includes("id"))).toBe(true);
490
+ }
491
+ }
492
+ }).pipe(Effect.provide(testLayer)));
493
+ it.effect("ValidationError when seqId is in input", () => Effect.gen(function* () {
494
+ yield* setupPostsTable;
495
+ const svc = yield* CollectionService;
496
+ const result = yield* svc.create(Posts, { title: "X", seqId: 99 }, noCtx).pipe(Effect.either);
497
+ expect(Either.isLeft(result)).toBe(true);
498
+ if (Either.isLeft(result)) {
499
+ expect(result.left._tag).toBe("ValidationError");
500
+ }
501
+ }).pipe(Effect.provide(testLayer)));
502
+ it.effect("ValidationError when type is wrong", () => Effect.gen(function* () {
503
+ yield* setupPostsTable;
504
+ const svc = yield* CollectionService;
505
+ const result = yield* svc.create(Posts, { title: 42 }, noCtx).pipe(Effect.either);
506
+ expect(Either.isLeft(result)).toBe(true);
507
+ if (Either.isLeft(result)) {
508
+ expect(result.left._tag).toBe("ValidationError");
509
+ }
510
+ }).pipe(Effect.provide(testLayer)));
511
+ it.effect("ValidationError when string exceeds maxLength", () => Effect.gen(function* () {
512
+ yield* setupPostsTable;
513
+ const svc = yield* CollectionService;
514
+ const result = yield* svc
515
+ .create(Posts, { title: "x".repeat(101) }, noCtx)
516
+ .pipe(Effect.either);
517
+ expect(Either.isLeft(result)).toBe(true);
518
+ if (Either.isLeft(result)) {
519
+ expect(result.left._tag).toBe("ValidationError");
520
+ if (result.left._tag === "ValidationError") {
521
+ expect(result.left.issues.some((i) => i.includes("100"))).toBe(true);
522
+ }
523
+ }
524
+ }).pipe(Effect.provide(testLayer)));
525
+ it.effect("ForbiddenError when create rule pre-check fails", () => Effect.gen(function* () {
526
+ yield* setupPostsTable;
527
+ const svc = yield* CollectionService;
528
+ // ProtectedPosts requires authorId = "user1"; supply "user2" → rule fails
529
+ const result = yield* svc
530
+ .create(ProtectedPosts, { title: "X", authorId: "user2" }, noCtx)
531
+ .pipe(Effect.either);
532
+ expect(Either.isLeft(result)).toBe(true);
533
+ if (Either.isLeft(result)) {
534
+ expect(result.left._tag).toBe("ForbiddenError");
535
+ }
536
+ }).pipe(Effect.provide(testLayer)));
537
+ it.effect("create succeeds when rule pre-check passes", () => Effect.gen(function* () {
538
+ yield* setupPostsTable;
539
+ const svc = yield* CollectionService;
540
+ const result = yield* svc.create(ProtectedPosts, { title: "X", authorId: "user1" }, noCtx);
541
+ expect(result["title"]).toBe("X");
542
+ }).pipe(Effect.provide(testLayer)));
543
+ it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
544
+ yield* setupPostsTable;
545
+ const svc = yield* CollectionService;
546
+ const result = yield* svc.create(NoRulePosts, { title: "X" }, noCtx).pipe(Effect.either);
547
+ expect(Either.isLeft(result)).toBe(true);
548
+ if (Either.isLeft(result)) {
549
+ expect(result.left._tag).toBe("ForbiddenError");
550
+ }
551
+ }).pipe(Effect.provide(testLayer)));
552
+ it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
553
+ yield* setupViewTables;
554
+ const svc = yield* CollectionService;
555
+ const result = yield* svc
556
+ .create(ActivePosts, { id: "x", title: "X", status: "active" }, noCtx)
557
+ .pipe(Effect.either);
558
+ expect(Either.isLeft(result)).toBe(true);
559
+ if (Either.isLeft(result)) {
560
+ expect(result.left._tag).toBe("ReadOnlyError");
561
+ }
562
+ }).pipe(Effect.provide(testLayer)));
563
+ });
564
+ // ---------------------------------------------------------------------------
565
+ // update
566
+ // ---------------------------------------------------------------------------
567
+ describe("update", () => {
568
+ it.effect("updates supplied fields, leaves others unchanged", () => Effect.gen(function* () {
569
+ yield* setupPostsTable;
570
+ const svc = yield* CollectionService;
571
+ const created = yield* svc.create(Posts, { title: "Old", authorId: "user1" }, noCtx);
572
+ const id = created["id"];
573
+ if (typeof id !== "string")
574
+ throw new Error("expected string id");
575
+ const updated = yield* svc.update(Posts, id, { title: "New" }, noCtx);
576
+ expect(updated["title"]).toBe("New");
577
+ expect(updated["authorId"]).toBe("user1");
578
+ }).pipe(Effect.provide(testLayer)));
579
+ it.live("updated timestamp is refreshed; created is unchanged", () => Effect.gen(function* () {
580
+ yield* setupPostsTable;
581
+ const svc = yield* CollectionService;
582
+ const created = yield* svc.create(Posts, { title: "Time" }, noCtx);
583
+ yield* Effect.sleep(5);
584
+ const id = created["id"];
585
+ if (typeof id !== "string")
586
+ throw new Error("expected string id");
587
+ const updated = yield* svc.update(Posts, id, { title: "Time2" }, noCtx);
588
+ expect(updated["created"]).toBe(created["created"]);
589
+ expect(updated["updated"]).not.toBe(created["updated"]);
590
+ }).pipe(Effect.provide(testLayer)));
591
+ it.effect("ValidationError when x-system field is in patch", () => Effect.gen(function* () {
592
+ yield* setupPostsTable;
593
+ const svc = yield* CollectionService;
594
+ const created = yield* svc.create(Posts, { title: "X" }, noCtx);
595
+ const id = created["id"];
596
+ if (typeof id !== "string")
597
+ throw new Error("expected string id");
598
+ const result = yield* svc
599
+ .update(Posts, id, { title: "Y", id: "hacked", created: "1970" }, noCtx)
600
+ .pipe(Effect.either);
601
+ expect(Either.isLeft(result)).toBe(true);
602
+ if (Either.isLeft(result)) {
603
+ expect(result.left._tag).toBe("ValidationError");
604
+ }
605
+ }).pipe(Effect.provide(testLayer)));
606
+ it.effect("NotFoundError for unknown id", () => Effect.gen(function* () {
607
+ yield* setupPostsTable;
608
+ const svc = yield* CollectionService;
609
+ const result = yield* svc
610
+ .update(Posts, "nosuchid1234567", { title: "X" }, noCtx)
611
+ .pipe(Effect.either);
612
+ expect(Either.isLeft(result)).toBe(true);
613
+ if (Either.isLeft(result)) {
614
+ expect(result.left._tag).toBe("NotFoundError");
615
+ }
616
+ }).pipe(Effect.provide(testLayer)));
617
+ it.effect("ForbiddenError when rule blocks update", () => Effect.gen(function* () {
618
+ yield* setupPostsTable;
619
+ const svc = yield* CollectionService;
620
+ // Insert row with authorId user2 using public Posts, then try to update via ProtectedPosts
621
+ const created = yield* svc.create(Posts, { title: "X", authorId: "user2" }, noCtx);
622
+ const id = created["id"];
623
+ if (typeof id !== "string")
624
+ throw new Error("expected string id");
625
+ const result = yield* svc
626
+ .update(ProtectedPosts, id, { title: "Y" }, noCtx)
627
+ .pipe(Effect.either);
628
+ expect(Either.isLeft(result)).toBe(true);
629
+ if (Either.isLeft(result)) {
630
+ expect(result.left._tag).toBe("ForbiddenError");
631
+ if (result.left._tag === "ForbiddenError") {
632
+ expect(result.left.action).toBe("update");
633
+ }
634
+ }
635
+ }).pipe(Effect.provide(testLayer)));
636
+ it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
637
+ yield* setupPostsTable;
638
+ const svc = yield* CollectionService;
639
+ const result = yield* svc
640
+ .update(NoRulePosts, "anyid", { title: "X" }, noCtx)
641
+ .pipe(Effect.either);
642
+ expect(Either.isLeft(result)).toBe(true);
643
+ if (Either.isLeft(result)) {
644
+ expect(result.left._tag).toBe("ForbiddenError");
645
+ }
646
+ }).pipe(Effect.provide(testLayer)));
647
+ it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
648
+ yield* setupViewTables;
649
+ const svc = yield* CollectionService;
650
+ const result = yield* svc
651
+ .update(ActivePosts, "anyid", { title: "X" }, noCtx)
652
+ .pipe(Effect.either);
653
+ expect(Either.isLeft(result)).toBe(true);
654
+ if (Either.isLeft(result)) {
655
+ expect(result.left._tag).toBe("ReadOnlyError");
656
+ }
657
+ }).pipe(Effect.provide(testLayer)));
658
+ });
659
+ // ---------------------------------------------------------------------------
660
+ // delete
661
+ // ---------------------------------------------------------------------------
662
+ describe("delete", () => {
663
+ it.effect("deletes an existing row and returns void", () => Effect.gen(function* () {
664
+ yield* setupPostsTable;
665
+ const svc = yield* CollectionService;
666
+ const created = yield* svc.create(Posts, { title: "Gone" }, noCtx);
667
+ const id = created["id"];
668
+ if (typeof id !== "string")
669
+ throw new Error("expected string id");
670
+ yield* svc.delete(Posts, id, noCtx);
671
+ }).pipe(Effect.provide(testLayer)));
672
+ it.effect("row is gone after delete (confirmed via view)", () => Effect.gen(function* () {
673
+ yield* setupPostsTable;
674
+ const svc = yield* CollectionService;
675
+ const created = yield* svc.create(Posts, { title: "Gone" }, noCtx);
676
+ const id = created["id"];
677
+ if (typeof id !== "string")
678
+ throw new Error("expected string id");
679
+ yield* svc.delete(Posts, id, noCtx);
680
+ const result = yield* svc.view(Posts, id, noCtx).pipe(Effect.either);
681
+ expect(Either.isLeft(result)).toBe(true);
682
+ if (Either.isLeft(result)) {
683
+ expect(result.left._tag).toBe("NotFoundError");
684
+ }
685
+ }).pipe(Effect.provide(testLayer)));
686
+ it.effect("NotFoundError for unknown id", () => Effect.gen(function* () {
687
+ yield* setupPostsTable;
688
+ const svc = yield* CollectionService;
689
+ const result = yield* svc.delete(Posts, "nosuchid1234567", noCtx).pipe(Effect.either);
690
+ expect(Either.isLeft(result)).toBe(true);
691
+ if (Either.isLeft(result)) {
692
+ expect(result.left._tag).toBe("NotFoundError");
693
+ }
694
+ }).pipe(Effect.provide(testLayer)));
695
+ it.effect("ForbiddenError when rule blocks delete", () => Effect.gen(function* () {
696
+ yield* setupPostsTable;
697
+ const svc = yield* CollectionService;
698
+ const created = yield* svc.create(Posts, { title: "X", authorId: "user2" }, noCtx);
699
+ const id = created["id"];
700
+ if (typeof id !== "string")
701
+ throw new Error("expected string id");
702
+ const result = yield* svc
703
+ .delete(ProtectedPosts, id, noCtx)
704
+ .pipe(Effect.either);
705
+ expect(Either.isLeft(result)).toBe(true);
706
+ if (Either.isLeft(result)) {
707
+ expect(result.left._tag).toBe("ForbiddenError");
708
+ if (result.left._tag === "ForbiddenError") {
709
+ expect(result.left.action).toBe("delete");
710
+ }
711
+ }
712
+ }).pipe(Effect.provide(testLayer)));
713
+ it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
714
+ yield* setupPostsTable;
715
+ const svc = yield* CollectionService;
716
+ const result = yield* svc.delete(NoRulePosts, "anyid", noCtx).pipe(Effect.either);
717
+ expect(Either.isLeft(result)).toBe(true);
718
+ if (Either.isLeft(result)) {
719
+ expect(result.left._tag).toBe("ForbiddenError");
720
+ }
721
+ }).pipe(Effect.provide(testLayer)));
722
+ it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
723
+ yield* setupViewTables;
724
+ const svc = yield* CollectionService;
725
+ const result = yield* svc.delete(ActivePosts, "anyid", noCtx).pipe(Effect.either);
726
+ expect(Either.isLeft(result)).toBe(true);
727
+ if (Either.isLeft(result)) {
728
+ expect(result.left._tag).toBe("ReadOnlyError");
729
+ }
730
+ }).pipe(Effect.provide(testLayer)));
731
+ });
732
+ // ---------------------------------------------------------------------------
733
+ // error callback — integration tests
734
+ // ---------------------------------------------------------------------------
735
+ describe("error callback — integration", () => {
736
+ it.effect("minLength message propagates from create", () => Effect.gen(function* () {
737
+ yield* setupScoredTable;
738
+ const svc = yield* CollectionService;
739
+ const result = yield* svc.create(Scored, { name: "x", score: 50 }, noCtx).pipe(Effect.either);
740
+ expect(Either.isLeft(result)).toBe(true);
741
+ if (Either.isLeft(result) && result.left._tag === "ValidationError") {
742
+ expect(result.left.issues.some((i) => i.includes("Name too short"))).toBe(true);
743
+ }
744
+ }).pipe(Effect.provide(scoredTestLayer)));
745
+ it.effect("minimum message propagates from create", () => Effect.gen(function* () {
746
+ yield* setupScoredTable;
747
+ const svc = yield* CollectionService;
748
+ const result = yield* svc.create(Scored, { name: "ok", score: -1 }, noCtx).pipe(Effect.either);
749
+ expect(Either.isLeft(result)).toBe(true);
750
+ if (Either.isLeft(result) && result.left._tag === "ValidationError") {
751
+ expect(result.left.issues.some((i) => i.includes("Score below zero"))).toBe(true);
752
+ }
753
+ }).pipe(Effect.provide(scoredTestLayer)));
754
+ it.effect("maximum message propagates from create", () => Effect.gen(function* () {
755
+ yield* setupScoredTable;
756
+ const svc = yield* CollectionService;
757
+ const result = yield* svc.create(Scored, { name: "ok", score: 200 }, noCtx).pipe(Effect.either);
758
+ expect(Either.isLeft(result)).toBe(true);
759
+ if (Either.isLeft(result) && result.left._tag === "ValidationError") {
760
+ expect(result.left.issues.some((i) => i.includes("Score above 100"))).toBe(true);
761
+ }
762
+ }).pipe(Effect.provide(scoredTestLayer)));
763
+ it.effect("required message propagates from create", () => Effect.gen(function* () {
764
+ yield* setupScoredTable;
765
+ const svc = yield* CollectionService;
766
+ const result = yield* svc.create(Scored, { score: 50 }, noCtx).pipe(Effect.either);
767
+ expect(Either.isLeft(result)).toBe(true);
768
+ if (Either.isLeft(result) && result.left._tag === "ValidationError") {
769
+ expect(result.left.issues.some((i) => i.includes("Name is required"))).toBe(true);
770
+ }
771
+ }).pipe(Effect.provide(scoredTestLayer)));
772
+ it.effect("Match.orElse fallback fires for type mismatch", () => Effect.gen(function* () {
773
+ yield* setupScoredTable;
774
+ const svc = yield* CollectionService;
775
+ const result = yield* svc.create(Scored, { name: 999, score: 50 }, noCtx).pipe(Effect.either);
776
+ expect(Either.isLeft(result)).toBe(true);
777
+ if (Either.isLeft(result) && result.left._tag === "ValidationError") {
778
+ expect(result.left.issues.some((i) => i.includes("Invalid name"))).toBe(true);
779
+ }
780
+ }).pipe(Effect.provide(scoredTestLayer)));
781
+ it.effect("update with too-short name produces default message (no custom error on update path)", () => Effect.gen(function* () {
782
+ yield* setupScoredTable;
783
+ const svc = yield* CollectionService;
784
+ const created = yield* svc.create(Scored, { name: "ok", score: 50 }, noCtx);
785
+ const id = created["id"];
786
+ if (typeof id !== "string")
787
+ throw new Error("expected string id");
788
+ const result = yield* svc.update(Scored, id, { name: "x" }, noCtx).pipe(Effect.either);
789
+ expect(Either.isLeft(result)).toBe(true);
790
+ if (Either.isLeft(result)) {
791
+ expect(result.left._tag).toBe("ValidationError");
792
+ if (result.left._tag === "ValidationError") {
793
+ expect(result.left.issues.length).toBeGreaterThan(0);
794
+ }
795
+ }
796
+ }).pipe(Effect.provide(scoredTestLayer)));
797
+ it.effect("valid data succeeds", () => Effect.gen(function* () {
798
+ yield* setupScoredTable;
799
+ const svc = yield* CollectionService;
800
+ const result = yield* svc.create(Scored, { name: "ok", score: 50 }, noCtx);
801
+ expect(result["name"]).toBe("ok");
802
+ expect(result["score"]).toBe(50);
803
+ }).pipe(Effect.provide(scoredTestLayer)));
804
+ });
805
+ //# sourceMappingURL=collection-service.test.js.map