effect-app 4.0.0-beta.99 → 4.0.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 (391) hide show
  1. package/CHANGELOG.md +2042 -0
  2. package/dist/Array.d.ts +1 -17
  3. package/dist/Array.d.ts.map +1 -1
  4. package/dist/Array.js +3 -27
  5. package/dist/Chunk.d.ts +3 -27
  6. package/dist/Chunk.d.ts.map +1 -1
  7. package/dist/Chunk.js +4 -39
  8. package/dist/Config/SecretURL.d.ts +3 -1
  9. package/dist/Config/SecretURL.d.ts.map +1 -1
  10. package/dist/Config/SecretURL.js +3 -6
  11. package/dist/Config/internal/configSecretURL.d.ts.map +1 -1
  12. package/dist/Config/internal/configSecretURL.js +2 -2
  13. package/dist/Config.d.ts +0 -5
  14. package/dist/Config.d.ts.map +1 -1
  15. package/dist/Config.js +7 -5
  16. package/dist/ConfigProvider.d.ts +0 -37
  17. package/dist/ConfigProvider.d.ts.map +1 -1
  18. package/dist/ConfigProvider.js +5 -41
  19. package/dist/Context.d.ts +6 -4
  20. package/dist/Context.d.ts.map +1 -1
  21. package/dist/Context.js +4 -3
  22. package/dist/DataDependencies.d.ts +136 -0
  23. package/dist/DataDependencies.d.ts.map +1 -0
  24. package/dist/DataDependencies.js +112 -0
  25. package/dist/DateTime.d.ts +33 -0
  26. package/dist/DateTime.d.ts.map +1 -0
  27. package/dist/DateTime.js +18 -0
  28. package/dist/Effect.d.ts +3 -14
  29. package/dist/Effect.d.ts.map +1 -1
  30. package/dist/Effect.js +8 -26
  31. package/dist/Emailer.d.ts +51 -0
  32. package/dist/Emailer.d.ts.map +1 -0
  33. package/dist/Emailer.js +7 -0
  34. package/dist/Function.d.ts +0 -4
  35. package/dist/Function.d.ts.map +1 -1
  36. package/dist/Function.js +1 -7
  37. package/dist/Layer.d.ts +6 -3
  38. package/dist/Layer.d.ts.map +1 -1
  39. package/dist/Layer.js +3 -2
  40. package/dist/Model/Repository/Registry.d.ts +22 -0
  41. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  42. package/dist/Model/Repository/Registry.js +18 -0
  43. package/dist/Model/Repository/dependency.d.ts +6 -0
  44. package/dist/Model/Repository/dependency.d.ts.map +1 -0
  45. package/dist/Model/Repository/dependency.js +29 -0
  46. package/dist/Model/Repository/ext.d.ts +60 -0
  47. package/dist/Model/Repository/ext.d.ts.map +1 -0
  48. package/dist/Model/Repository/ext.js +124 -0
  49. package/dist/Model/Repository/internal/internal.d.ts +68 -0
  50. package/dist/Model/Repository/internal/internal.d.ts.map +1 -0
  51. package/dist/Model/Repository/internal/internal.js +610 -0
  52. package/dist/Model/Repository/legacy.d.ts +21 -0
  53. package/dist/Model/Repository/legacy.d.ts.map +1 -0
  54. package/dist/Model/Repository/legacy.js +2 -0
  55. package/dist/Model/Repository/makeRepo.d.ts +71 -0
  56. package/dist/Model/Repository/makeRepo.d.ts.map +1 -0
  57. package/dist/Model/Repository/makeRepo.js +30 -0
  58. package/dist/Model/Repository/service.d.ts +121 -0
  59. package/dist/Model/Repository/service.d.ts.map +1 -0
  60. package/dist/Model/Repository/service.js +2 -0
  61. package/dist/Model/Repository/validation.d.ts +72 -0
  62. package/dist/Model/Repository/validation.d.ts.map +1 -0
  63. package/dist/Model/Repository/validation.js +39 -0
  64. package/dist/Model/Repository.d.ts +8 -0
  65. package/dist/Model/Repository.d.ts.map +1 -0
  66. package/dist/Model/Repository.js +8 -0
  67. package/dist/Model/dsl.d.ts +34 -0
  68. package/dist/Model/dsl.d.ts.map +1 -0
  69. package/dist/Model/dsl.js +45 -0
  70. package/dist/Model/filter/filterApi.d.ts +30 -0
  71. package/dist/Model/filter/filterApi.d.ts.map +1 -0
  72. package/dist/Model/filter/filterApi.js +2 -0
  73. package/dist/Model/filter/types/errors.d.ts +29 -0
  74. package/dist/Model/filter/types/errors.d.ts.map +1 -0
  75. package/dist/Model/filter/types/errors.js +2 -0
  76. package/dist/Model/filter/types/fields.d.ts +15 -0
  77. package/dist/Model/filter/types/fields.d.ts.map +1 -0
  78. package/dist/Model/filter/types/fields.js +2 -0
  79. package/dist/Model/filter/types/path/common.d.ts +316 -0
  80. package/dist/Model/filter/types/path/common.d.ts.map +1 -0
  81. package/dist/Model/filter/types/path/common.js +2 -0
  82. package/dist/Model/filter/types/path/eager.d.ts +94 -0
  83. package/dist/Model/filter/types/path/eager.d.ts.map +1 -0
  84. package/dist/Model/filter/types/path/eager.js +36 -0
  85. package/dist/Model/filter/types/path/index.d.ts +4 -0
  86. package/dist/Model/filter/types/path/index.d.ts.map +1 -0
  87. package/dist/Model/filter/types/path/index.js +3 -0
  88. package/dist/Model/filter/types/utils.d.ts +79 -0
  89. package/dist/Model/filter/types/utils.d.ts.map +1 -0
  90. package/dist/Model/filter/types/utils.js +2 -0
  91. package/dist/Model/filter/types/validator.d.ts +30 -0
  92. package/dist/Model/filter/types/validator.d.ts.map +1 -0
  93. package/dist/Model/filter/types/validator.js +2 -0
  94. package/dist/Model/filter/types.d.ts +5 -0
  95. package/dist/Model/filter/types.d.ts.map +1 -0
  96. package/dist/Model/filter/types.js +7 -0
  97. package/dist/Model/query/dsl.d.ts +630 -0
  98. package/dist/Model/query/dsl.d.ts.map +1 -0
  99. package/dist/Model/query/dsl.js +376 -0
  100. package/dist/Model/query/new-kid-interpreter.d.ts +132 -0
  101. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -0
  102. package/dist/Model/query/new-kid-interpreter.js +369 -0
  103. package/dist/Model/query.d.ts +15 -0
  104. package/dist/Model/query.d.ts.map +1 -0
  105. package/dist/Model/query.js +3 -0
  106. package/dist/Model.d.ts +6 -0
  107. package/dist/Model.d.ts.map +1 -0
  108. package/dist/Model.js +6 -0
  109. package/dist/NonEmptySet.d.ts +3 -1
  110. package/dist/NonEmptySet.d.ts.map +1 -1
  111. package/dist/NonEmptySet.js +2 -2
  112. package/dist/Option.d.ts +1 -2
  113. package/dist/Option.d.ts.map +1 -1
  114. package/dist/Option.js +3 -7
  115. package/dist/Pure.d.ts +9 -7
  116. package/dist/Pure.d.ts.map +1 -1
  117. package/dist/Pure.js +20 -17
  118. package/dist/QueueMaker.d.ts +11 -0
  119. package/dist/QueueMaker.d.ts.map +1 -0
  120. package/dist/QueueMaker.js +11 -0
  121. package/dist/RequestContext.d.ts +87 -0
  122. package/dist/RequestContext.d.ts.map +1 -0
  123. package/dist/RequestContext.js +45 -0
  124. package/dist/RequestScopedDependencies.d.ts +21 -0
  125. package/dist/RequestScopedDependencies.d.ts.map +1 -0
  126. package/dist/RequestScopedDependencies.js +16 -0
  127. package/dist/Schema/Class.d.ts +163 -16
  128. package/dist/Schema/Class.d.ts.map +1 -1
  129. package/dist/Schema/Class.js +193 -21
  130. package/dist/Schema/FastCheck.d.ts.map +1 -1
  131. package/dist/Schema/SchemaParser.d.ts +5 -0
  132. package/dist/Schema/SchemaParser.d.ts.map +1 -0
  133. package/dist/Schema/SchemaParser.js +6 -0
  134. package/dist/Schema/SpecialJsonSchema.d.ts +15 -2
  135. package/dist/Schema/SpecialJsonSchema.d.ts.map +1 -1
  136. package/dist/Schema/SpecialJsonSchema.js +70 -12
  137. package/dist/Schema/SpecialOpenApi.d.ts +1 -1
  138. package/dist/Schema/SpecialOpenApi.js +3 -3
  139. package/dist/Schema/brand.d.ts +5 -5
  140. package/dist/Schema/brand.d.ts.map +1 -1
  141. package/dist/Schema/brand.js +6 -4
  142. package/dist/Schema/email.d.ts.map +1 -1
  143. package/dist/Schema/email.js +8 -5
  144. package/dist/Schema/ext.d.ts +279 -75
  145. package/dist/Schema/ext.d.ts.map +1 -1
  146. package/dist/Schema/ext.js +330 -72
  147. package/dist/Schema/moreStrings.d.ts +82 -48
  148. package/dist/Schema/moreStrings.d.ts.map +1 -1
  149. package/dist/Schema/moreStrings.js +85 -51
  150. package/dist/Schema/numbers.d.ts +33 -21
  151. package/dist/Schema/numbers.d.ts.map +1 -1
  152. package/dist/Schema/numbers.js +64 -12
  153. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  154. package/dist/Schema/phoneNumber.js +6 -6
  155. package/dist/Schema/schema.d.ts +1 -1
  156. package/dist/Schema/schema.d.ts.map +1 -1
  157. package/dist/Schema/schema.js +2 -2
  158. package/dist/Schema/strings.d.ts +18 -4
  159. package/dist/Schema/strings.d.ts.map +1 -1
  160. package/dist/Schema/strings.js +1 -5
  161. package/dist/Schema.d.ts +224 -7
  162. package/dist/Schema.d.ts.map +1 -1
  163. package/dist/Schema.js +190 -24
  164. package/dist/SchemaAST.d.ts +2 -0
  165. package/dist/SchemaAST.d.ts.map +1 -0
  166. package/dist/SchemaAST.js +2 -0
  167. package/dist/Set.d.ts +4 -1
  168. package/dist/Set.d.ts.map +1 -1
  169. package/dist/Set.js +8 -6
  170. package/dist/Store.d.ts +215 -0
  171. package/dist/Store.d.ts.map +1 -0
  172. package/dist/Store.js +113 -0
  173. package/dist/_ext/Array.d.ts +1 -1
  174. package/dist/_ext/Array.d.ts.map +1 -1
  175. package/dist/_ext/Array.js +4 -2
  176. package/dist/_ext/ord.ext.d.ts +2 -1
  177. package/dist/_ext/ord.ext.d.ts.map +1 -1
  178. package/dist/_ext/ord.ext.js +2 -2
  179. package/dist/builtin.d.ts +2 -14
  180. package/dist/builtin.d.ts.map +1 -1
  181. package/dist/builtin.js +1 -1
  182. package/dist/client/InvalidationKeys.d.ts +30 -0
  183. package/dist/client/InvalidationKeys.d.ts.map +1 -0
  184. package/dist/client/InvalidationKeys.js +37 -0
  185. package/dist/client/apiClientFactory.d.ts +13 -8
  186. package/dist/client/apiClientFactory.d.ts.map +1 -1
  187. package/dist/client/apiClientFactory.js +154 -34
  188. package/dist/client/clientFor.d.ts +48 -13
  189. package/dist/client/clientFor.d.ts.map +1 -1
  190. package/dist/client/clientFor.js +9 -1
  191. package/dist/client/errors.d.ts +74 -28
  192. package/dist/client/errors.d.ts.map +1 -1
  193. package/dist/client/errors.js +41 -10
  194. package/dist/client/makeClient.d.ts +487 -32
  195. package/dist/client/makeClient.d.ts.map +1 -1
  196. package/dist/client/makeClient.js +47 -21
  197. package/dist/client.d.ts +2 -0
  198. package/dist/client.d.ts.map +1 -1
  199. package/dist/client.js +3 -1
  200. package/dist/faker.d.ts.map +1 -1
  201. package/dist/http/Request.d.ts +2 -2
  202. package/dist/http/Request.d.ts.map +1 -1
  203. package/dist/http/Request.js +2 -2
  204. package/dist/http/internal/lib.d.ts +12 -12
  205. package/dist/http/internal/lib.d.ts.map +1 -1
  206. package/dist/http/internal/lib.js +13 -13
  207. package/dist/ids.d.ts +32 -14
  208. package/dist/ids.d.ts.map +1 -1
  209. package/dist/ids.js +38 -7
  210. package/dist/index.d.ts +3 -0
  211. package/dist/index.d.ts.map +1 -1
  212. package/dist/index.js +5 -1
  213. package/dist/middleware.d.ts +12 -6
  214. package/dist/middleware.d.ts.map +1 -1
  215. package/dist/middleware.js +13 -7
  216. package/dist/migration/nativeEffect.d.ts +21 -0
  217. package/dist/migration/nativeEffect.d.ts.map +1 -0
  218. package/dist/migration/nativeEffect.js +598 -0
  219. package/dist/rpc/Invalidation.d.ts +1124 -0
  220. package/dist/rpc/Invalidation.d.ts.map +1 -0
  221. package/dist/rpc/Invalidation.js +178 -0
  222. package/dist/rpc/MiddlewareMaker.d.ts +10 -7
  223. package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
  224. package/dist/rpc/MiddlewareMaker.js +57 -37
  225. package/dist/rpc/RpcContextMap.d.ts +2 -2
  226. package/dist/rpc/RpcContextMap.d.ts.map +1 -1
  227. package/dist/rpc/RpcMiddleware.d.ts +13 -10
  228. package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
  229. package/dist/rpc/RpcMiddleware.js +2 -2
  230. package/dist/rpc.d.ts +1 -1
  231. package/dist/rpc.d.ts.map +1 -1
  232. package/dist/rpc.js +2 -2
  233. package/dist/runtime.d.ts +19 -0
  234. package/dist/runtime.d.ts.map +1 -0
  235. package/dist/runtime.js +40 -0
  236. package/dist/setupRequest.d.ts +20 -0
  237. package/dist/setupRequest.d.ts.map +1 -0
  238. package/dist/setupRequest.js +72 -0
  239. package/dist/toast.d.ts +51 -0
  240. package/dist/toast.d.ts.map +1 -0
  241. package/dist/toast.js +34 -0
  242. package/dist/transform.d.ts +1 -1
  243. package/dist/transform.d.ts.map +1 -1
  244. package/dist/transform.js +4 -5
  245. package/dist/utils/effectify.d.ts +1 -1
  246. package/dist/utils/effectify.d.ts.map +1 -1
  247. package/dist/utils/effectify.js +2 -2
  248. package/dist/utils/extend.d.ts +6 -1
  249. package/dist/utils/extend.d.ts.map +1 -1
  250. package/dist/utils/extend.js +24 -6
  251. package/dist/utils/gen.d.ts +4 -4
  252. package/dist/utils/gen.d.ts.map +1 -1
  253. package/dist/utils/logLevel.d.ts +2 -2
  254. package/dist/utils/logLevel.d.ts.map +1 -1
  255. package/dist/utils/logger.d.ts +2 -1
  256. package/dist/utils/logger.d.ts.map +1 -1
  257. package/dist/utils/logger.js +2 -2
  258. package/dist/utils.d.ts +34 -45
  259. package/dist/utils.d.ts.map +1 -1
  260. package/dist/utils.js +33 -49
  261. package/dist/validation/validators.d.ts.map +1 -1
  262. package/dist/validation.d.ts.map +1 -1
  263. package/dist/withToast.d.ts +30 -0
  264. package/dist/withToast.d.ts.map +1 -0
  265. package/dist/withToast.js +64 -0
  266. package/package.json +25 -266
  267. package/src/Array.ts +7 -39
  268. package/src/Chunk.ts +6 -55
  269. package/src/Config/SecretURL.ts +6 -3
  270. package/src/Config/internal/configSecretURL.ts +2 -2
  271. package/src/Config.ts +6 -13
  272. package/src/ConfigProvider.ts +4 -47
  273. package/src/Context.ts +9 -8
  274. package/src/DataDependencies.ts +171 -0
  275. package/src/DateTime.ts +473 -0
  276. package/src/Effect.ts +10 -32
  277. package/src/Emailer.ts +51 -0
  278. package/src/Function.ts +0 -7
  279. package/src/Inputify.type.ts +1 -1
  280. package/src/Layer.ts +8 -5
  281. package/src/Model/Repository/Registry.ts +35 -0
  282. package/src/Model/Repository/dependency.ts +32 -0
  283. package/src/Model/Repository/ext.ts +374 -0
  284. package/src/Model/Repository/internal/internal.ts +987 -0
  285. package/src/Model/Repository/legacy.ts +29 -0
  286. package/src/Model/Repository/makeRepo.ts +168 -0
  287. package/src/Model/Repository/service.ts +676 -0
  288. package/src/Model/Repository/validation.ts +38 -0
  289. package/src/Model/Repository.ts +7 -0
  290. package/src/Model/dsl.ts +130 -0
  291. package/src/Model/filter/filterApi.ts +78 -0
  292. package/src/Model/filter/types/errors.ts +47 -0
  293. package/src/Model/filter/types/fields.ts +50 -0
  294. package/src/Model/filter/types/path/common.ts +404 -0
  295. package/src/Model/filter/types/path/eager.ts +329 -0
  296. package/src/Model/filter/types/path/index.ts +4 -0
  297. package/src/Model/filter/types/utils.ts +128 -0
  298. package/src/Model/filter/types/validator.ts +46 -0
  299. package/src/Model/filter/types.ts +6 -0
  300. package/src/Model/query/dsl.ts +3092 -0
  301. package/src/Model/query/new-kid-interpreter.ts +531 -0
  302. package/src/Model/query.ts +13 -0
  303. package/src/Model.ts +5 -0
  304. package/src/NonEmptySet.ts +6 -4
  305. package/src/Option.ts +3 -8
  306. package/src/Pure.ts +20 -17
  307. package/src/QueueMaker.ts +11 -0
  308. package/src/RequestContext.ts +48 -0
  309. package/src/RequestScopedDependencies.ts +45 -0
  310. package/src/Schema/Class.ts +550 -56
  311. package/src/Schema/SchemaParser.ts +12 -0
  312. package/src/Schema/SpecialJsonSchema.ts +75 -7
  313. package/src/Schema/SpecialOpenApi.ts +5 -5
  314. package/src/Schema/brand.ts +27 -12
  315. package/src/Schema/email.ts +9 -7
  316. package/src/Schema/ext.ts +405 -95
  317. package/src/Schema/moreStrings.ts +213 -88
  318. package/src/Schema/numbers.ts +107 -29
  319. package/src/Schema/phoneNumber.ts +8 -9
  320. package/src/Schema/schema.ts +1 -1
  321. package/src/Schema/strings.ts +23 -14
  322. package/src/Schema.ts +403 -46
  323. package/src/SchemaAST.ts +1 -0
  324. package/src/Set.ts +10 -5
  325. package/src/Store.ts +325 -0
  326. package/src/_ext/Array.ts +4 -2
  327. package/src/_ext/ord.ext.ts +2 -1
  328. package/src/builtin-json.d.ts +24 -0
  329. package/src/builtin.ts +16 -21
  330. package/src/client/InvalidationKeys.ts +55 -0
  331. package/src/client/apiClientFactory.ts +295 -129
  332. package/src/client/clientFor.ts +101 -33
  333. package/src/client/errors.ts +66 -20
  334. package/src/client/makeClient.ts +519 -60
  335. package/src/client.ts +6 -4
  336. package/src/http/Request.ts +4 -4
  337. package/src/http/internal/lib.ts +12 -12
  338. package/src/http.ts +1 -1
  339. package/src/ids.ts +45 -8
  340. package/src/index.ts +20 -16
  341. package/src/middleware.ts +12 -8
  342. package/src/migration/nativeEffect.ts +645 -0
  343. package/src/rpc/Invalidation.ts +278 -0
  344. package/src/rpc/MiddlewareMaker.ts +86 -79
  345. package/src/rpc/README.md +2 -2
  346. package/src/rpc/RpcContextMap.ts +4 -4
  347. package/src/rpc/RpcMiddleware.ts +18 -13
  348. package/src/rpc.ts +4 -4
  349. package/src/runtime.ts +57 -0
  350. package/src/setupRequest.ts +138 -0
  351. package/src/toast.ts +54 -0
  352. package/src/transform.ts +4 -4
  353. package/src/utils/effectify.ts +1 -1
  354. package/src/utils/extend.ts +24 -5
  355. package/src/utils/gen.ts +7 -7
  356. package/src/utils/logLevel.ts +1 -1
  357. package/src/utils/logger.ts +3 -2
  358. package/src/utils.ts +85 -175
  359. package/src/validation.ts +2 -2
  360. package/src/withToast.ts +133 -0
  361. package/test/dateTimeLocal.test.ts +55 -0
  362. package/test/dist/dateTimeLocal.test.d.ts.map +1 -0
  363. package/test/dist/nativeEffectMigration.test.d.ts.map +1 -0
  364. package/test/dist/rpc.test.d.ts.map +1 -1
  365. package/test/dist/rpcMiddlewareContext.test.d.ts.map +1 -0
  366. package/test/moreStrings.test.ts +55 -10
  367. package/test/nativeEffectMigration.test.ts +77 -0
  368. package/test/rpc.test.ts +28 -11
  369. package/test/rpcMiddlewareContext.test.ts +63 -0
  370. package/test/schema.test.ts +318 -55
  371. package/test/secretURL.test.ts +4 -1
  372. package/test/special.test.ts +663 -131
  373. package/test/utils.test.ts +5 -5
  374. package/tsconfig.base.json +6 -4
  375. package/tsconfig.json +9 -1
  376. package/tsconfig.src.json +29 -29
  377. package/tsconfig.test.json +2 -2
  378. package/dist/Operations.d.ts +0 -124
  379. package/dist/Operations.d.ts.map +0 -1
  380. package/dist/Operations.js +0 -29
  381. package/dist/_ext/date.d.ts +0 -15
  382. package/dist/_ext/date.d.ts.map +0 -1
  383. package/dist/_ext/date.js +0 -16
  384. package/dist/_ext/misc.d.ts +0 -14
  385. package/dist/_ext/misc.d.ts.map +0 -1
  386. package/dist/_ext/misc.js +0 -17
  387. package/eslint.config.mjs +0 -26
  388. package/src/Operations.ts +0 -55
  389. package/src/_ext/date.ts +0 -27
  390. package/src/_ext/misc.ts +0 -46
  391. package/tsconfig.json.bak +0 -12
@@ -0,0 +1,987 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import * as Equivalence from "effect/Equivalence"
4
+ import { flow, pipe } from "effect/Function"
5
+ import * as HashMap from "effect/HashMap"
6
+ import * as HashSet from "effect/HashSet"
7
+ import * as Metric from "effect/Metric"
8
+ import * as Pipeable from "effect/Pipeable"
9
+ import * as Ref from "effect/Ref"
10
+ import * as Result from "effect/Result"
11
+ import type * as Scope from "effect/Scope"
12
+ import * as Unify from "effect/Unify"
13
+ import * as Array from "../../../Array.ts"
14
+ import type { NonEmptyReadonlyArray } from "../../../Array.ts"
15
+ import * as Chunk from "../../../Chunk.ts"
16
+ import { NotFoundError } from "../../../client/errors.ts"
17
+ import * as Context from "../../../Context.ts"
18
+ import * as DataDependencies from "../../../DataDependencies.ts"
19
+ import * as Effect from "../../../Effect.ts"
20
+ import { flatMapOption } from "../../../Effect.ts"
21
+ import * as Option from "../../../Option.ts"
22
+ import * as S from "../../../Schema.ts"
23
+ import { type Codec, NonNegativeInt } from "../../../Schema.ts"
24
+ import * as SchemaAST from "../../../SchemaAST.ts"
25
+ import { setupRequestContextFromCurrent } from "../../../setupRequest.ts"
26
+ import { type FilterArgs, getContextMap, type PersistenceModelType, type StoreConfig, storeId, StoreMaker } from "../../../Store.ts"
27
+ import type { FilterResult } from "../../filter/filterApi.ts"
28
+ import type { FieldValues } from "../../filter/types.ts"
29
+ import * as Q from "../../query.ts"
30
+ import { repositoryDependencyPaths } from "../dependency.ts"
31
+ import type { ChangeFeed, ChangeFeedEvent, Repository } from "../service.ts"
32
+ import { ValidationError, ValidationResult } from "../validation.ts"
33
+
34
+ const dedupe = Array.dedupeWith(Equivalence.String)
35
+
36
+ // ms buckets: dense under 100ms (common path), then mid-tail and multi-second stalls.
37
+ // Rare 0.5–1s+ encodes (fat aggregates) must not collapse into a single overflow bin.
38
+ const schemaDurationBoundaries = [
39
+ 0.1,
40
+ 0.5,
41
+ 1,
42
+ 2,
43
+ 5,
44
+ 10,
45
+ 25,
46
+ 50,
47
+ 100,
48
+ 150,
49
+ 200,
50
+ 300,
51
+ 500,
52
+ 750,
53
+ 1_000,
54
+ 1_500,
55
+ 2_000,
56
+ 5_000
57
+ ]
58
+ // Event-loop-relevant stall floor: encodes/decodes above this are rare but block Node.
59
+ const SCHEMA_SLOW_MS = 100
60
+ const schemaDecodeDuration = Metric.histogram("app.schema.decode.duration", { boundaries: schemaDurationBoundaries })
61
+ const schemaEncodeDuration = Metric.histogram("app.schema.encode.duration", { boundaries: schemaDurationBoundaries })
62
+ const schemaItemCount = Metric.histogram("app.schema.item_count", {
63
+ boundaries: [0, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1_000, 5_000]
64
+ })
65
+ const schemaSlow = Metric.counter("app.schema.slow", {
66
+ description: `Repository schema encode/decode slower than ${SCHEMA_SLOW_MS}ms`,
67
+ incremental: true
68
+ })
69
+
70
+ const entityStateFromItems = (items: readonly unknown[]): string | undefined => {
71
+ const first = items[0]
72
+ if (first === null || typeof first !== "object" || !("_tag" in first)) return undefined
73
+ const tag = (first as { readonly _tag: unknown })._tag
74
+ return typeof tag === "string" ? tag : undefined
75
+ }
76
+
77
+ const timeSchema = (
78
+ operation: "decode" | "encode",
79
+ entity: string,
80
+ queryMode: "aggregate" | "collect" | "project" | "transform" | undefined,
81
+ itemCount: number,
82
+ entityState?: string
83
+ ) =>
84
+ <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
85
+ Effect.clockWith((clock) => {
86
+ const startedAt = clock.currentTimeNanosUnsafe()
87
+ const attributes = {
88
+ "app.entity": entity,
89
+ "app.schema.operation": operation,
90
+ ...(queryMode !== undefined && { "app.query.mode": queryMode }),
91
+ ...(entityState !== undefined && { "app.entity.state": entityState })
92
+ }
93
+ return Effect.onExit(self, () => {
94
+ const durationMs = Number(clock.currentTimeNanosUnsafe() - startedAt) / 1_000_000
95
+ const slow = durationMs >= SCHEMA_SLOW_MS
96
+ return Effect.all([
97
+ Effect.annotateCurrentSpan({
98
+ [`app.schema.${operation}.duration_ms`]: durationMs,
99
+ "app.schema.item_count": itemCount,
100
+ "app.schema.slow": slow,
101
+ ...(queryMode !== undefined && { "app.query.mode": queryMode }),
102
+ ...(entityState !== undefined && { "app.entity.state": entityState })
103
+ }),
104
+ Metric.update(
105
+ Metric.withAttributes(
106
+ operation === "decode" ? schemaDecodeDuration : schemaEncodeDuration,
107
+ attributes
108
+ ),
109
+ durationMs
110
+ ),
111
+ Metric.update(Metric.withAttributes(schemaItemCount, attributes), itemCount),
112
+ ...(slow
113
+ ? [Metric.update(Metric.withAttributes(schemaSlow, attributes), 1)] as const
114
+ : [])
115
+ ], { discard: true })
116
+ })
117
+ })
118
+
119
+ /**
120
+ * A base implementation to create a repository.
121
+ */
122
+ export function makeRepoInternal<
123
+ Evt = never
124
+ >() {
125
+ return <
126
+ ItemType extends string,
127
+ R,
128
+ Encoded extends FieldValues,
129
+ T,
130
+ IdKey extends keyof T & keyof Encoded
131
+ >(
132
+ name: ItemType,
133
+ schema: S.Codec<T, Encoded, R>,
134
+ mapTo: (e: Encoded, etag: string | undefined) => PersistenceModelType<Encoded>,
135
+ idKey: IdKey
136
+ ) => {
137
+ type PM = PersistenceModelType<Encoded>
138
+ function mapToPersistenceModel(
139
+ e: Encoded,
140
+ getEtag: (id: string) => string | undefined
141
+ ): PM {
142
+ return mapTo(e, getEtag(e[idKey]))
143
+ }
144
+
145
+ function mapReverse(
146
+ { _etag, ...e }: PM,
147
+ setEtag: (id: string, eTag: string | undefined) => void
148
+ ): Encoded {
149
+ setEtag((e as any)[idKey], _etag)
150
+ // the store already applied `jitM` and decoded JSON→Encoded
151
+ return e as unknown as Encoded
152
+ }
153
+
154
+ const mkStore = makeStore<Encoded>()(name, schema, mapTo, idKey)
155
+
156
+ function make<RInitial = never, E = never, RPublish = never, RCtx = never>(
157
+ args: [Evt] extends [never] ? {
158
+ schemaContext?: Context.Context<RCtx>
159
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
160
+ dependencyIds?: (item: T) => NonEmptyReadonlyArray<string>
161
+ additionalWriteDependencies?: (item: T) => ReadonlyArray<DataDependencies.DataDependency>
162
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
163
+ partitionValue?: (e?: Encoded) => string
164
+ }
165
+ }
166
+ : {
167
+ schemaContext?: Context.Context<RCtx>
168
+ publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, RPublish>
169
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
170
+ dependencyIds?: (item: T) => NonEmptyReadonlyArray<string>
171
+ additionalWriteDependencies?: (item: T) => ReadonlyArray<DataDependencies.DataDependency>
172
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
173
+ partitionValue?: (e?: Encoded) => string
174
+ }
175
+ }
176
+ ) {
177
+ return Effect
178
+ .gen(function*() {
179
+ const rctx: Context.Context<RCtx> = args.schemaContext ?? Context.empty() as any
180
+ const provideRctx = Effect.provide(rctx)
181
+ const encodeMany = (items: readonly T[]) =>
182
+ S.encodeEffect(S.Array(schema))(items).pipe(
183
+ provideRctx,
184
+ timeSchema("encode", name, undefined, items.length, entityStateFromItems(items))
185
+ )
186
+ const decode = flow(S.decodeEffectConcurrently(schema), provideRctx)
187
+ const decodeMany = flow(
188
+ S.decodeEffectConcurrently(S.Array(schema)),
189
+ provideRctx
190
+ )
191
+
192
+ const store = yield* mkStore(args.makeInitial, args.config)
193
+ const dependencyPaths = repositoryDependencyPaths(schema)
194
+ const recordRead = DataDependencies.readRepo(name)
195
+ const entityDependency = (ids: NonEmptyReadonlyArray<T[IdKey]>) =>
196
+ DataDependencies.repo(name, [String(ids[0]), ...ids.slice(1).map(String)])
197
+ const valuesAtPath = (value: unknown, path: string): readonly unknown[] => {
198
+ if (path.length === 0) return [value]
199
+ const [head, ...tail] = path.split(".")
200
+ if (head === undefined) return [value]
201
+ if (head === "-1") {
202
+ return globalThis.Array.isArray(value)
203
+ ? value.flatMap((item) => valuesAtPath(item, tail.join(".")))
204
+ : []
205
+ }
206
+ return typeof value === "object" && value !== null
207
+ ? valuesAtPath((value as Record<string, unknown>)[head], tail.join("."))
208
+ : []
209
+ }
210
+ const dependencyIds = (item: T): NonEmptyReadonlyArray<string> => {
211
+ const configured = dependencyPaths
212
+ .flatMap((path) => valuesAtPath(item, String(path)))
213
+ .filter((value): value is string => typeof value === "string") ?? []
214
+ const ids = args.dependencyIds?.(item) ?? [String(item[idKey]), ...configured]
215
+ return [ids[0], ...ids.slice(1)]
216
+ }
217
+ const itemDependencies = (item: T) => {
218
+ const ids = dependencyIds(item)
219
+ return [
220
+ DataDependencies.repo(name, ids),
221
+ ...(args.additionalWriteDependencies?.(item) ?? [])
222
+ ]
223
+ }
224
+ const queryDependencyIds = (filter: readonly FilterResult[] | undefined): readonly string[] => {
225
+ if (!filter) return []
226
+ const paths = new Set([String(idKey), ...dependencyPaths])
227
+ const visit = (items: readonly FilterResult[]): readonly string[] => {
228
+ if (items.some((item) => item.t === "or" || item.t === "or-scope")) return []
229
+ return items.flatMap((item) => {
230
+ if ("result" in item) return visit(item.result)
231
+ if (!paths.has(item.path)) return []
232
+ const value: unknown = item.value
233
+ if (item.op === "eq" && typeof value === "string") return [value]
234
+ if (item.op === "in" && globalThis.Array.isArray(value)) {
235
+ return value.filter((entry): entry is string => typeof entry === "string")
236
+ }
237
+ return []
238
+ })
239
+ }
240
+ return [...new Set(visit(filter))]
241
+ }
242
+ const recordEntityRead = (id: T[IdKey]) => DataDependencies.read(entityDependency([id]))
243
+ const recordEntityWrite = (ids: NonEmptyReadonlyArray<T[IdKey]>) =>
244
+ DataDependencies.write(entityDependency(ids))
245
+ const recordItemWrite = (items: NonEmptyReadonlyArray<T>) =>
246
+ Effect.forEach(
247
+ DataDependencies.merge(new Set(items.flatMap(itemDependencies))),
248
+ DataDependencies.write,
249
+ { discard: true }
250
+ )
251
+ const cms = Effect.map(getContextMap.pipe(Effect.orDie), (_) => ({
252
+ get: (id: string) => _.get(`${name}.${id}`),
253
+ set: (id: string, etag: string | undefined) => _.set(`${name}.${id}`, etag)
254
+ }))
255
+
256
+ const pub = "publishEvents" in args
257
+ ? args.publishEvents
258
+ : () => Effect.void
259
+
260
+ type ChangeFeedHandler = (evt: ChangeFeedEvent<T>) => Effect.Effect<void>
261
+ const changeFeedByNamespace = yield* Ref.make(HashMap.empty<string, HashSet.HashSet<ChangeFeedHandler>>())
262
+ const changeFeedWildcard = yield* Ref.make(HashSet.empty<ChangeFeedHandler>())
263
+ // clear all handlers when the repository's scope closes
264
+ yield* Effect.addFinalizer(() =>
265
+ Effect.all([
266
+ Ref.set(changeFeedByNamespace, HashMap.empty<string, HashSet.HashSet<ChangeFeedHandler>>()),
267
+ Ref.set(changeFeedWildcard, HashSet.empty<ChangeFeedHandler>())
268
+ ], { discard: true })
269
+ )
270
+ const changeFeed: ChangeFeed<T> = {
271
+ publish: ([items, op]) =>
272
+ Effect.gen(function*() {
273
+ const ns = yield* storeId
274
+ const evt: ChangeFeedEvent<T> = [items, op, ns]
275
+ const map = yield* Ref.get(changeFeedByNamespace)
276
+ const wild = yield* Ref.get(changeFeedWildcard)
277
+ const targeted = Option.getOrElse(HashMap.get(map, ns), () => HashSet.empty<ChangeFeedHandler>())
278
+ const all = HashSet.union(targeted, wild)
279
+ yield* Effect.forEach(all, (h) => h(evt), { concurrency: "unbounded", discard: true })
280
+ }),
281
+ subscribe: (handler, options) => {
282
+ const ns = options?.namespace
283
+ if (ns === undefined) {
284
+ return Effect.acquireRelease(
285
+ Ref.update(changeFeedWildcard, HashSet.add(handler)),
286
+ () => Ref.update(changeFeedWildcard, HashSet.remove(handler))
287
+ )
288
+ }
289
+ return Effect.acquireRelease(
290
+ Ref.update(changeFeedByNamespace, (m) => {
291
+ const cur = Option.getOrElse(HashMap.get(m, ns), () => HashSet.empty<ChangeFeedHandler>())
292
+ return HashMap.set(m, ns, HashSet.add(cur, handler))
293
+ }),
294
+ () =>
295
+ Ref.update(changeFeedByNamespace, (m) => {
296
+ const cur = Option.getOrElse(HashMap.get(m, ns), () => HashSet.empty<ChangeFeedHandler>())
297
+ const next = HashSet.remove(cur, handler)
298
+ return HashSet.size(next) === 0 ? HashMap.remove(m, ns) : HashMap.set(m, ns, next)
299
+ })
300
+ )
301
+ }
302
+ }
303
+
304
+ const allE = cms
305
+ .pipe(Effect.flatMap((cm) => Effect.map(store.all, (_) => _.map((_) => mapReverse(_, cm.set)))))
306
+
307
+ const all = Effect
308
+ .flatMap(
309
+ allE,
310
+ (items) =>
311
+ decodeMany(items).pipe(
312
+ Effect.orDie,
313
+ timeSchema("decode", name, undefined, items.length)
314
+ )
315
+ )
316
+ .pipe(
317
+ Effect.tap(() => recordRead),
318
+ Effect.map((_) => _ as T[]),
319
+ Effect.withSpan("Repository.all", {
320
+ kind: "client",
321
+ attributes: { "app.entity": name }
322
+ }, { captureStackTrace: false })
323
+ )
324
+
325
+ const fieldsSchema = schema as unknown as { fields: any }
326
+ // assumes the id field never needs a service...
327
+ const i = ("fields" in fieldsSchema ? S.Struct(fieldsSchema["fields"]) as unknown as typeof schema : schema)
328
+ .pipe((_) => {
329
+ let ast = _.ast
330
+ if (ast._tag === "Declaration") ast = ast.typeParameters[0]!
331
+
332
+ const pickIdFromAst = (a: SchemaAST.AST) => {
333
+ // Unwrap Declaration (e.g. TaggedClass) to get the underlying Objects AST
334
+ let inner = a
335
+ if (inner._tag === "Declaration") inner = inner.typeParameters[0]!
336
+ // Pick from the original AST to preserve the full encoding chain (e.g. decodeTo transformations).
337
+ // Using toEncoded would lose transformation info needed to encode Type -> Encoded.
338
+ if (SchemaAST.isObjects(inner)) {
339
+ const field = inner.propertySignatures.find((_) => _.name === idKey)
340
+ if (field) {
341
+ return S.Struct({ [idKey]: S.make(field.type) }) as unknown as Codec<T, Encoded>
342
+ }
343
+ }
344
+ return S.make(a) as unknown as Codec<T, Encoded>
345
+ }
346
+
347
+ return ast._tag === "Union"
348
+ // we need to get the Objects (TypeLiteral), in case of class it has encoding chain...
349
+ ? S.Union(
350
+ ast.types.map((_) => pickIdFromAst(_))
351
+ )
352
+ : pickIdFromAst(ast)
353
+ })
354
+ const encodeId = flow(S.encodeEffect(i), provideRctx)
355
+ const encodeIdOnly = (id: string) =>
356
+ encodeId({ [idKey]: id } as any).pipe(
357
+ Effect.map((_: Record<string, unknown>) => _[idKey as string] as Encoded[IdKey])
358
+ )
359
+ const findEId = Effect.fnUntraced(function*(id: Encoded[IdKey]) {
360
+ yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
361
+
362
+ return yield* Effect.flatMap(
363
+ store.find(id),
364
+ (item) =>
365
+ Effect.gen(function*() {
366
+ const { set } = yield* cms
367
+ return item.pipe(Option.map((_) => mapReverse(_, set)))
368
+ })
369
+ )
370
+ })
371
+ // TODO: select the particular field, instead of as struct
372
+ const findE = Effect.fnUntraced(function*(id: T[IdKey]) {
373
+ yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
374
+
375
+ return yield* pipe(
376
+ encodeId({ [idKey]: id } as any),
377
+ Effect.orDie,
378
+ Effect.map((_) => (_ as any)[idKey]),
379
+ Effect.flatMap(findEId)
380
+ )
381
+ })
382
+
383
+ const loadExistingItems = (ids: ReadonlyArray<T[IdKey]>) =>
384
+ Effect
385
+ .forEach(
386
+ ids,
387
+ (id) =>
388
+ findE(id).pipe(
389
+ Effect.flatMap(Option.match({
390
+ onNone: () => Effect.succeed(Option.none<T>()),
391
+ onSome: (encoded) => decode(encoded).pipe(Effect.orDie, Effect.map(Option.some))
392
+ }))
393
+ )
394
+ )
395
+ .pipe(Effect.map(Array.getSomes))
396
+
397
+ const find = Effect.fn("Repository.find", {
398
+ kind: "client",
399
+ attributes: { "app.entity": name }
400
+ })(function*(id: T[IdKey]) {
401
+ yield* recordEntityRead(id)
402
+ yield* Effect.annotateCurrentSpan({ "app.entity.id": id })
403
+ return yield* flatMapOption(findE(id), (_) => Effect.orDie(decode(_)))
404
+ })
405
+
406
+ const saveAllE = (a: Iterable<Encoded>) =>
407
+ flatMapOption(
408
+ Effect
409
+ .sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)([...a])),
410
+ (a) =>
411
+ Effect.gen(function*() {
412
+ const { get, set } = yield* cms
413
+ const items = a.map((_) => mapToPersistenceModel(_, get))
414
+ const ret = yield* store.batchSet(items)
415
+ ret.forEach((_) => set(_[idKey], _._etag))
416
+ })
417
+ )
418
+ .pipe(Effect.asVoid)
419
+
420
+ const saveAll = (a: Iterable<T>) =>
421
+ encodeMany(Array.fromIterable(a))
422
+ .pipe(
423
+ Effect.orDie,
424
+ Effect.andThen(saveAllE)
425
+ )
426
+
427
+ const saveAndPublish = Effect.fn("Repository.saveAndPublish", { attributes: { "app.entity": name } })(
428
+ function*(items: Iterable<T>, events: Iterable<Evt> = []) {
429
+ const it = Chunk.fromIterable(items)
430
+ if (Chunk.isNonEmpty(it)) {
431
+ const values = Chunk.toReadonlyArray(it)
432
+ const previous = args.additionalWriteDependencies || args.dependencyIds || dependencyPaths.length > 0
433
+ ? yield* loadExistingItems(values.map((item) => item[idKey]))
434
+ : []
435
+ yield* recordItemWrite([values[0], ...values.slice(1), ...previous])
436
+ }
437
+ const evts = [...events]
438
+ yield* Effect.annotateCurrentSpan({
439
+ "app.entity.ids": Chunk.map(it, (_) => _[idKey]),
440
+ "app.event.count": evts.length
441
+ })
442
+ return yield* saveAll(it)
443
+ .pipe(
444
+ Effect.andThen(Effect.sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(evts))),
445
+ // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
446
+ (_) => flatMapOption(_, pub),
447
+ Effect.andThen(changeFeed.publish([Chunk.toArray(it), "save"] as [T[], "save" | "remove"])),
448
+ Effect.asVoid
449
+ )
450
+ }
451
+ )
452
+
453
+ const removeAndPublish = Effect.fn("Repository.removeAndPublish", { attributes: { "app.entity": name } })(
454
+ function*(a: Iterable<T>, events: Iterable<Evt> = []) {
455
+ const { set } = yield* cms
456
+ const it = [...a]
457
+ if (Array.isReadonlyArrayNonEmpty(it)) {
458
+ yield* recordItemWrite(it)
459
+ }
460
+ const evts = [...events]
461
+ yield* Effect.annotateCurrentSpan({
462
+ "app.entity.ids": it.map((_) => _[idKey]),
463
+ "app.event.count": evts.length
464
+ })
465
+ const items = yield* encodeMany(it).pipe(Effect.orDie)
466
+ if (Array.isReadonlyArrayNonEmpty(items)) {
467
+ yield* store.batchRemove(
468
+ items.map((_) => (_[idKey])),
469
+ args.config?.partitionValue?.(items[0])
470
+ )
471
+ for (const e of items) {
472
+ set(e[idKey], undefined)
473
+ }
474
+ yield* Effect
475
+ .sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(evts))
476
+ // TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
477
+ .pipe((_) => flatMapOption(_, pub))
478
+
479
+ yield* changeFeed.publish([it, "remove"] as [T[], "save" | "remove"])
480
+ }
481
+ }
482
+ )
483
+
484
+ const removeById = Effect.fn("Repository.removeById", { attributes: { "app.entity": name } })(
485
+ function*(idOrIds: T[IdKey] | ReadonlyArray<T[IdKey]>) {
486
+ const ids = globalThis.Array.isArray(idOrIds)
487
+ ? idOrIds as readonly T[IdKey][]
488
+ : [idOrIds as T[IdKey]]
489
+ if (!Array.isReadonlyArrayNonEmpty(ids)) {
490
+ return
491
+ }
492
+ yield* recordEntityWrite(ids)
493
+ if (args.additionalWriteDependencies || args.dependencyIds || dependencyPaths.length > 0) {
494
+ const previous = yield* loadExistingItems(ids)
495
+ if (Array.isReadonlyArrayNonEmpty(previous)) yield* recordItemWrite(previous)
496
+ }
497
+ const { set } = yield* cms
498
+ const eids = yield* Effect.forEach(ids, (_) => encodeIdOnly(_ as any)).pipe(Effect.orDie)
499
+ yield* Effect.annotateCurrentSpan({ "app.entity.ids": eids })
500
+ yield* store.batchRemove(eids)
501
+ for (const id of eids) {
502
+ set(id, undefined)
503
+ }
504
+ yield* changeFeed.publish([[], "remove"] as [T[], "save" | "remove"])
505
+ }
506
+ )
507
+
508
+ const parseMany = Effect.fnUntraced(
509
+ function*(items: readonly PM[]) {
510
+ const cm = yield* cms
511
+ return yield* decodeMany(items.map((_) => mapReverse(_, cm.set))).pipe(
512
+ Effect.orDie,
513
+ timeSchema("decode", name, "transform", items.length)
514
+ )
515
+ }
516
+ )
517
+ const decodeManyCache = new WeakMap<
518
+ S.Codec<any, any, any>,
519
+ (i: readonly any[]) => Effect.Effect<any, any, any>
520
+ >()
521
+ const getDecodeMany = (s: S.Codec<any, Encoded, any>) => {
522
+ let dec = decodeManyCache.get(s)
523
+ if (!dec) {
524
+ dec = S.decodeEffectConcurrently(S.Array(s))
525
+ decodeManyCache.set(s, dec)
526
+ }
527
+ return dec
528
+ }
529
+ const parseMany2 = Effect.fnUntraced(
530
+ function*<A, R>(items: readonly PM[], schema: S.Codec<A, Encoded, R>) {
531
+ const cm = yield* cms
532
+ return yield* getDecodeMany(schema)(items.map((_) => mapReverse(_, cm.set))).pipe(
533
+ Effect.orDie,
534
+ timeSchema("decode", name, "transform", items.length)
535
+ )
536
+ }
537
+ )
538
+ const filter = <U extends keyof Encoded = keyof Encoded>(args: FilterArgs<Encoded, U>) =>
539
+ store
540
+ .filter(
541
+ // always enforce id and _etag because they are system fields, required for etag tracking etc
542
+ {
543
+ ...args,
544
+ select: args.select
545
+ ? dedupe([...args.select, idKey, "_etag" as any])
546
+ : undefined
547
+ } as typeof args
548
+ )
549
+ .pipe(
550
+ Effect.tap((items) =>
551
+ Effect.map(cms, ({ set }) => items.forEach((_) => set((_ as Encoded)[idKey], (_ as PM)._etag)))
552
+ )
553
+ )
554
+
555
+ // TODO: For raw we should use S.from, and drop the R...
556
+ const query: {
557
+ <A, R, From extends FieldValues>(
558
+ q: Q.QueryProjection<Encoded extends From ? From : never, A, R>
559
+ ): Effect.Effect<readonly A[], S.SchemaError, Exclude<R, RCtx>>
560
+ <A, R, EncodedRefined extends Encoded = Encoded>(
561
+ q: Q.QAll<NoInfer<Encoded>, NoInfer<EncodedRefined>, A, R>
562
+ ): Effect.Effect<readonly A[], never, Exclude<R, RCtx>>
563
+ } = (<A, R, EncodedRefined extends Encoded = Encoded>(q: Q.QAll<Encoded, EncodedRefined, A, R>) => {
564
+ const a = Q.toFilter(q, schema)
565
+ const scopedIds = queryDependencyIds(a.filter)
566
+ const recordQueryRead = Array.isReadonlyArrayNonEmpty(scopedIds)
567
+ ? DataDependencies.read(DataDependencies.repo(name, scopedIds))
568
+ : recordRead
569
+ // Mode dispatch — see `Q.project` JSDoc for the contract:
570
+ // aggregate: GROUP BY + aggregate functions at DB level; decode raw rows with schema; SchemaError surfaces.
571
+ // project : decode raw encoded rows with schema; no PM reverse-mapping; SchemaError surfaces.
572
+ // collect : same as project, but schema yields Option and None rows are dropped.
573
+ // transform: PM reverse-map (re-inject _etag/PM state from cms cache) then decode; orDie.
574
+ const eff = a.mode === "aggregate"
575
+ ? store
576
+ // `a.select` contains `{ key, aggregate }` items not expressible in FilterFunc<Encoded, U>'s
577
+ // `U extends keyof Encoded` generic. Cast is unavoidable until FilterFunc supports aggregate mode.
578
+ .filter(a as any)
579
+ // Decode raw aggregate rows directly — no PM reverse-mapping, no id/_etag needed.
580
+ .pipe(
581
+ Effect.andThen(
582
+ (items) =>
583
+ S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema ?? schema)))(items).pipe(
584
+ provideRctx,
585
+ timeSchema("decode", name, "aggregate", items.length)
586
+ )
587
+ )
588
+ )
589
+ : a.mode === "project"
590
+ ? filter(a)
591
+ // TODO: mapFrom but need to support per field and dependencies
592
+ .pipe(
593
+ Effect.andThen(
594
+ (items) =>
595
+ S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema ?? schema)))(items).pipe(
596
+ provideRctx,
597
+ timeSchema("decode", name, "project", items.length)
598
+ )
599
+ )
600
+ )
601
+ : a.mode === "collect"
602
+ ? filter(a)
603
+ // TODO: mapFrom but need to support per field and dependencies
604
+ .pipe(
605
+ Effect.flatMap((items) =>
606
+ S.decodeEffectConcurrently(S.Array(S.toCodecJson(a.schema)))(items).pipe(
607
+ Effect.map(Array.getSomes),
608
+ provideRctx,
609
+ timeSchema("decode", name, "collect", items.length)
610
+ )
611
+ )
612
+ )
613
+ : Effect.flatMap(
614
+ filter(a),
615
+ (_) =>
616
+ Unify.unify(
617
+ a.schema
618
+ // TODO: partial may not match?
619
+ ? parseMany2(_ as any, a.schema as any)
620
+ : parseMany(_ as any)
621
+ )
622
+ )
623
+ return pipe(
624
+ a.ttype === "one"
625
+ ? Effect.flatMap(
626
+ eff,
627
+ flow(
628
+ Array.head,
629
+ Option.match({
630
+ onNone: () => Effect.fail(new NotFoundError({ id: "query", /* TODO */ type: name })),
631
+ onSome: Effect.succeed
632
+ })
633
+ )
634
+ )
635
+ : a.ttype === "count"
636
+ ? Effect
637
+ .map(eff, (_) => NonNegativeInt(_.length))
638
+ .pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e)))
639
+ : eff,
640
+ Effect.tap((r) =>
641
+ Effect.annotateCurrentSpan({
642
+ "app.query.ttype": a.ttype,
643
+ "app.query.mode": a.mode,
644
+ "db.response.returned_rows": Array.isArray(r) ? r.length : 1
645
+ })
646
+ ),
647
+ Effect.tap(() => recordQueryRead),
648
+ Effect.withSpan("Repository.query", {
649
+ kind: "client",
650
+ attributes: { "app.entity": name }
651
+ }, { captureStackTrace: false })
652
+ )
653
+ }) as any
654
+
655
+ const validateSample = Effect.fn("Repository.validateSample", { attributes: { "app.entity": name } })(
656
+ function*(options?: {
657
+ percentage?: number
658
+ maxItems?: number
659
+ }) {
660
+ const percentage = options?.percentage ?? 0.1 // default 10%
661
+ const maxItems = options?.maxItems
662
+
663
+ // 1. get all IDs with projection (bypasses main schema decode)
664
+ const allIds = yield* store
665
+ .filter({
666
+ t: null as unknown as Encoded,
667
+ select: [idKey as keyof Encoded]
668
+ })
669
+ .pipe(
670
+ // a projection never decodes a whole document, so the store's
671
+ // document decode cannot fail here
672
+ Effect.catchTag("SchemaError", (e) => Effect.die(e)),
673
+ Effect.withSpan("Repository.filter", {
674
+ kind: "client",
675
+ attributes: { "app.entity": name }
676
+ }, { captureStackTrace: false })
677
+ )
678
+
679
+ // 2. random subset
680
+ const shuffled = [...allIds].sort(() => Math.random() - 0.5)
681
+ const sampleSize = Math.min(
682
+ maxItems ?? Infinity,
683
+ Math.ceil(allIds.length * percentage)
684
+ )
685
+ const sample = shuffled.slice(0, sampleSize)
686
+
687
+ // 3. validate each item
688
+ const errors: ValidationError[] = []
689
+
690
+ for (const item of sample) {
691
+ const id = item[idKey]
692
+ const stored = yield* store.find(id).pipe(
693
+ Effect.result,
694
+ Effect.withSpan("Repository.find", {
695
+ kind: "client",
696
+ attributes: { "app.entity": name, "app.entity.id": id }
697
+ }, { captureStackTrace: false })
698
+ )
699
+
700
+ // a store-boundary failure: `defaultValues` + `jitM` did not
701
+ // produce a document the Encoded shape accepts, so the store
702
+ // failed with `S.SchemaError` instead of returning one. Report
703
+ // that document and keep sampling - it used to kill the run.
704
+ if (Result.isFailure(stored)) {
705
+ const failure = stored.failure
706
+ if (!S.isSchemaError(failure)) return yield* Effect.fail(failure)
707
+ errors.push(
708
+ ValidationError.make({
709
+ id,
710
+ // the store failed before it could hand back a document
711
+ rawData: undefined,
712
+ jitMResult: undefined,
713
+ error: failure
714
+ })
715
+ )
716
+ continue
717
+ }
718
+
719
+ const rawResult = stored.success
720
+ if (Option.isNone(rawResult)) continue
721
+
722
+ // the store merged `defaultValues`, applied `jitM` on the raw
723
+ // JSON document and decoded JSON→Encoded, so this is exactly
724
+ // what the repository decodes into the domain type.
725
+ const rawData = rawResult.value as Encoded
726
+
727
+ // the repository's own decode of a document the store accepted
728
+ const decodeResult = yield* S.decodeEffectConcurrently(schema)(rawData).pipe(
729
+ Effect.result,
730
+ provideRctx
731
+ )
732
+
733
+ if (Result.isFailure(decodeResult)) {
734
+ errors.push(
735
+ ValidationError.make({
736
+ id,
737
+ rawData,
738
+ jitMResult: rawData,
739
+ error: decodeResult.failure
740
+ })
741
+ )
742
+ }
743
+ }
744
+
745
+ return ValidationResult.make({
746
+ total: NonNegativeInt(allIds.length),
747
+ sampled: NonNegativeInt(sample.length),
748
+ valid: NonNegativeInt(sample.length - errors.length),
749
+ errors
750
+ })
751
+ }
752
+ )
753
+
754
+ // The store fails with `S.SchemaError` when a stored document does not
755
+ // decode - see `Store.all` / `Store.find`. The internals above let that
756
+ // failure propagate; producing the public members is the single
757
+ // boundary where it becomes a defect, because these signatures declare
758
+ // only `DatabaseError`. `queryRaw`, `query` and `mapped` already
759
+ // declare `S.SchemaError`, and `validateSample` reports it per
760
+ // document instead of dying.
761
+ const r = {
762
+ changeFeed,
763
+ itemType: name,
764
+ idKey,
765
+ find: (id: T[IdKey]) => find(id).pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))),
766
+ all: all.pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))),
767
+ saveAndPublish: (items: Iterable<T>, events?: Iterable<Evt>) =>
768
+ saveAndPublish(items, events).pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))),
769
+ removeAndPublish,
770
+ removeById: (idOrIds: T[IdKey] | ReadonlyArray<T[IdKey]>) =>
771
+ removeById(idOrIds).pipe(Effect.catchTag("SchemaError", (e) => Effect.die(e))),
772
+ seedNamespace: (namespace: string) => store.seedNamespace(namespace),
773
+ validateSample,
774
+ queryRaw<A, Out, QR>(schema: S.Codec<A, Out, QR>, q: Q.RawQuery<Encoded, Out>) {
775
+ return store.queryRaw(q).pipe(
776
+ Effect.flatMap((items) =>
777
+ S.decodeEffectConcurrently(S.Array(S.toCodecJson(schema)))(items as readonly S.Json[]).pipe(
778
+ timeSchema("decode", name, undefined, items.length)
779
+ )
780
+ ),
781
+ Effect.tap(() => recordRead),
782
+ Effect.withSpan("Repository.queryRaw", {
783
+ kind: "client",
784
+ attributes: { "app.entity": name }
785
+ }, { captureStackTrace: false })
786
+ )
787
+ },
788
+ query(q: any) {
789
+ // eslint-disable-next-line prefer-rest-params
790
+ return query(typeof q === "function" ? Pipeable.pipeArguments(Q.make(), arguments) : q) as any
791
+ },
792
+ /**
793
+ * @internal
794
+ */
795
+ mapped: <A, R>(schema: S.Codec<A, any, R>) => {
796
+ const dec = S.decodeEffectConcurrently(schema)
797
+ const encMany = S.encodeEffect(S.Array(schema))
798
+ const decMany = S.decodeEffectConcurrently(S.Array(schema))
799
+ const spanAttrs = { kind: "client" as const, attributes: { "app.entity": name } }
800
+ return {
801
+ all: allE.pipe(
802
+ Effect.flatMap((items) =>
803
+ decMany(items).pipe(
804
+ timeSchema("decode", name, undefined, items.length)
805
+ )
806
+ ),
807
+ Effect.tap(() => recordRead),
808
+ Effect.map((_) => _ as any[]),
809
+ Effect.withSpan("Repository.mapped.all", spanAttrs, { captureStackTrace: false })
810
+ ),
811
+ find: (id: T[IdKey]) =>
812
+ flatMapOption(findE(id), dec).pipe(
813
+ Effect.tap(() => recordEntityRead(id)),
814
+ Effect.withSpan("Repository.mapped.find", {
815
+ ...spanAttrs,
816
+ attributes: { ...spanAttrs.attributes, "app.entity.id": id }
817
+ }, { captureStackTrace: false })
818
+ ),
819
+ // query: (q: any) => {
820
+ // const a = Q.toFilter(q)
821
+
822
+ // return filter(a)
823
+ // .pipe(
824
+ // Effect.flatMap(decMany),
825
+ // Effect.map((_) => _ as any[]),
826
+ // Effect.withSpan("Repository.mapped.query [effect-app/infra]", {
827
+ // captureStackTrace: false,
828
+ // attributes: {
829
+ // "repository.model_name": name,
830
+ // query: { ...a, schema: a.schema ? "__SCHEMA__" : a.schema, filter: a.filter.build() }
831
+ // }
832
+ // })
833
+ // )
834
+ // },
835
+ save: (...xes: any[]) =>
836
+ Effect
837
+ .flatMap(
838
+ encMany(xes).pipe(
839
+ timeSchema("encode", name, undefined, xes.length, entityStateFromItems(xes))
840
+ ),
841
+ (_) => saveAllE(_)
842
+ )
843
+ .pipe(
844
+ Effect.tap(() =>
845
+ Array.isReadonlyArrayNonEmpty(xes)
846
+ ? recordEntityWrite([xes[0][idKey], ...xes.slice(1).map((_) => _[idKey])])
847
+ : Effect.void
848
+ ),
849
+ Effect.withSpan("Repository.mapped.save", spanAttrs, { captureStackTrace: false })
850
+ )
851
+ }
852
+ }
853
+ }
854
+ return r as Repository<T, Encoded, Evt, ItemType, IdKey, Exclude<R, RCtx>, RPublish, RCtx>
855
+ })
856
+ .pipe(Effect
857
+ // .withSpan("Repository.make [effect-app/infra]", { attributes: { "repository.model_name": name } })
858
+ .withLogSpan("Repository.make: " + name))
859
+ }
860
+
861
+ return {
862
+ make,
863
+ Q: Q.make<Encoded>()
864
+ }
865
+ }
866
+ }
867
+
868
+ const pluralize = (s: string) =>
869
+ s.endsWith("s")
870
+ ? s + "es"
871
+ : s.endsWith("y")
872
+ ? s.substring(0, s.length - 1) + "ies"
873
+ : s + "s"
874
+
875
+ export function makeStore<Encoded extends FieldValues>() {
876
+ return <
877
+ ItemType extends string,
878
+ R,
879
+ E,
880
+ T,
881
+ IdKey extends keyof Encoded
882
+ >(
883
+ name: ItemType,
884
+ schema: S.Codec<T, E, R>,
885
+ mapTo: (e: E, etag: string | undefined) => Encoded,
886
+ idKey: IdKey
887
+ ) => {
888
+ function makeStore<RInitial = never, EInitial = never>(
889
+ makeInitial?: Effect.Effect<readonly T[], EInitial, RInitial>,
890
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
891
+ partitionValue?: (e?: Encoded) => string
892
+ }
893
+ ) {
894
+ function encodeToEncoded() {
895
+ const getEtag = () => undefined
896
+ return (t: T) =>
897
+ S.encodeEffect(schema)(t).pipe(
898
+ Effect.orDie,
899
+ Effect.map((_) => mapToPersistenceModel(_, getEtag))
900
+ )
901
+ }
902
+
903
+ function mapToPersistenceModel(
904
+ e: E,
905
+ getEtag: (id: string) => string | undefined
906
+ ): Encoded {
907
+ return mapTo(e, getEtag((e as any)[idKey] as string))
908
+ }
909
+
910
+ return Effect.gen(function*() {
911
+ const { make } = yield* StoreMaker
912
+
913
+ const store = yield* make<IdKey, Encoded, RInitial | R, EInitial>(
914
+ pluralize(name),
915
+ idKey,
916
+ makeInitial
917
+ ? makeInitial
918
+ .pipe(
919
+ Effect.flatMap(Effect.forEach(encodeToEncoded())),
920
+ setupRequestContextFromCurrent("Repository.makeInitial [effect-app/infra]", {
921
+ attributes: { "app.entity": name }
922
+ })
923
+ )
924
+ : undefined,
925
+ {
926
+ ...config,
927
+ schema,
928
+ partitionValue: config?.partitionValue
929
+ ?? ((_) => "primary") /*(isIntegrationEvent(r) ? r.companyId : r.id*/
930
+ }
931
+ )
932
+
933
+ return store
934
+ })
935
+ }
936
+
937
+ return makeStore
938
+ }
939
+ }
940
+
941
+ export interface Repos<
942
+ T,
943
+ Encoded extends { id: string },
944
+ RSchema,
945
+ Evt,
946
+ ItemType extends string,
947
+ IdKey extends keyof T,
948
+ RPublish
949
+ > {
950
+ make<RInitial = never, E = never, R2 = never>(
951
+ args: [Evt] extends [never] ? {
952
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
953
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
954
+ partitionValue?: (e?: Encoded) => string
955
+ }
956
+ }
957
+ : {
958
+ publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, R2>
959
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
960
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
961
+ partitionValue?: (e?: Encoded) => string
962
+ }
963
+ }
964
+ ): Effect.Effect<
965
+ Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>,
966
+ E,
967
+ StoreMaker | RInitial | R2 | Scope.Scope
968
+ >
969
+ makeWith<Out, RInitial = never, E = never, R2 = never>(
970
+ args: [Evt] extends [never] ? {
971
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
972
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
973
+ partitionValue?: (e?: Encoded) => string
974
+ }
975
+ }
976
+ : {
977
+ publishEvents: (evt: NonEmptyReadonlyArray<Evt>) => Effect.Effect<void, never, R2>
978
+ makeInitial?: Effect.Effect<readonly T[], E, RInitial> | undefined
979
+ config?: Omit<StoreConfig<Encoded>, "partitionValue"> & {
980
+ partitionValue?: (e?: Encoded) => string
981
+ }
982
+ },
983
+ f: (r: Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>) => Out
984
+ ): Effect.Effect<Out, E, StoreMaker | RInitial | R2 | Scope.Scope>
985
+ readonly Q: ReturnType<typeof Q.make<Encoded>>
986
+ readonly type: Repository<T, Encoded, Evt, ItemType, IdKey, RSchema, RPublish>
987
+ }