effect-app 4.0.0-beta.31 → 4.0.0-beta.311

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