@effect-app/infra 4.0.0-beta.8 → 4.0.0-beta.82

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 (177) hide show
  1. package/CHANGELOG.md +540 -0
  2. package/dist/CUPS.d.ts +3 -3
  3. package/dist/CUPS.d.ts.map +1 -1
  4. package/dist/CUPS.js +3 -3
  5. package/dist/Emailer/Sendgrid.js +1 -1
  6. package/dist/Emailer/service.d.ts +3 -3
  7. package/dist/Emailer/service.d.ts.map +1 -1
  8. package/dist/Emailer/service.js +3 -3
  9. package/dist/MainFiberSet.d.ts +2 -2
  10. package/dist/MainFiberSet.d.ts.map +1 -1
  11. package/dist/MainFiberSet.js +3 -3
  12. package/dist/Model/Repository/internal/internal.d.ts +3 -3
  13. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  14. package/dist/Model/Repository/internal/internal.js +11 -7
  15. package/dist/Model/Repository/makeRepo.d.ts +2 -2
  16. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  17. package/dist/Model/Repository/makeRepo.js +1 -1
  18. package/dist/Model/Repository/validation.d.ts +5 -4
  19. package/dist/Model/Repository/validation.d.ts.map +1 -1
  20. package/dist/Model/query/dsl.d.ts +9 -9
  21. package/dist/Operations.d.ts +2 -2
  22. package/dist/Operations.d.ts.map +1 -1
  23. package/dist/Operations.js +3 -3
  24. package/dist/OperationsRepo.d.ts +2 -2
  25. package/dist/OperationsRepo.d.ts.map +1 -1
  26. package/dist/OperationsRepo.js +3 -3
  27. package/dist/QueueMaker/SQLQueue.d.ts +3 -5
  28. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  29. package/dist/QueueMaker/SQLQueue.js +9 -7
  30. package/dist/QueueMaker/errors.d.ts +1 -1
  31. package/dist/QueueMaker/errors.d.ts.map +1 -1
  32. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  33. package/dist/QueueMaker/memQueue.js +10 -9
  34. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  35. package/dist/QueueMaker/sbqueue.js +11 -9
  36. package/dist/RequestContext.d.ts +19 -14
  37. package/dist/RequestContext.d.ts.map +1 -1
  38. package/dist/RequestContext.js +5 -5
  39. package/dist/RequestFiberSet.d.ts +2 -2
  40. package/dist/RequestFiberSet.d.ts.map +1 -1
  41. package/dist/RequestFiberSet.js +5 -5
  42. package/dist/Store/ContextMapContainer.d.ts +14 -3
  43. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  44. package/dist/Store/ContextMapContainer.js +64 -3
  45. package/dist/Store/Cosmos.d.ts.map +1 -1
  46. package/dist/Store/Cosmos.js +55 -32
  47. package/dist/Store/Disk.d.ts.map +1 -1
  48. package/dist/Store/Disk.js +3 -4
  49. package/dist/Store/Memory.d.ts +2 -2
  50. package/dist/Store/Memory.d.ts.map +1 -1
  51. package/dist/Store/Memory.js +4 -4
  52. package/dist/Store/SQL/Pg.d.ts +4 -0
  53. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  54. package/dist/Store/SQL/Pg.js +174 -0
  55. package/dist/Store/SQL/query.d.ts +34 -0
  56. package/dist/Store/SQL/query.d.ts.map +1 -0
  57. package/dist/Store/SQL/query.js +326 -0
  58. package/dist/Store/SQL.d.ts +4 -0
  59. package/dist/Store/SQL.d.ts.map +1 -0
  60. package/dist/Store/SQL.js +203 -0
  61. package/dist/Store/index.d.ts +1 -1
  62. package/dist/Store/index.d.ts.map +1 -1
  63. package/dist/Store/index.js +11 -1
  64. package/dist/Store/service.d.ts +8 -5
  65. package/dist/Store/service.d.ts.map +1 -1
  66. package/dist/Store/service.js +14 -6
  67. package/dist/adapters/SQL/Model.d.ts +2 -5
  68. package/dist/adapters/SQL/Model.d.ts.map +1 -1
  69. package/dist/adapters/SQL/Model.js +21 -13
  70. package/dist/adapters/ServiceBus.d.ts +6 -6
  71. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  72. package/dist/adapters/ServiceBus.js +9 -9
  73. package/dist/adapters/cosmos-client.d.ts +2 -2
  74. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  75. package/dist/adapters/cosmos-client.js +3 -3
  76. package/dist/adapters/logger.d.ts.map +1 -1
  77. package/dist/adapters/memQueue.d.ts +2 -2
  78. package/dist/adapters/memQueue.d.ts.map +1 -1
  79. package/dist/adapters/memQueue.js +3 -3
  80. package/dist/adapters/mongo-client.d.ts +2 -2
  81. package/dist/adapters/mongo-client.d.ts.map +1 -1
  82. package/dist/adapters/mongo-client.js +3 -3
  83. package/dist/adapters/redis-client.d.ts +3 -3
  84. package/dist/adapters/redis-client.d.ts.map +1 -1
  85. package/dist/adapters/redis-client.js +3 -3
  86. package/dist/api/ContextProvider.d.ts +6 -6
  87. package/dist/api/ContextProvider.d.ts.map +1 -1
  88. package/dist/api/ContextProvider.js +6 -6
  89. package/dist/api/internal/RequestContextMiddleware.d.ts +1 -1
  90. package/dist/api/internal/auth.d.ts +1 -1
  91. package/dist/api/internal/events.d.ts +2 -2
  92. package/dist/api/internal/events.d.ts.map +1 -1
  93. package/dist/api/internal/events.js +7 -5
  94. package/dist/api/layerUtils.d.ts +5 -5
  95. package/dist/api/layerUtils.d.ts.map +1 -1
  96. package/dist/api/layerUtils.js +5 -5
  97. package/dist/api/routing/middleware/RouterMiddleware.d.ts +3 -3
  98. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  99. package/dist/api/routing/middleware/middleware.d.ts +35 -1
  100. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  101. package/dist/api/routing/middleware/middleware.js +39 -1
  102. package/dist/api/routing/schema/jwt.d.ts +1 -1
  103. package/dist/api/routing/schema/jwt.d.ts.map +1 -1
  104. package/dist/api/routing/schema/jwt.js +1 -1
  105. package/dist/api/routing.d.ts +1 -5
  106. package/dist/api/routing.d.ts.map +1 -1
  107. package/dist/api/routing.js +3 -2
  108. package/dist/api/setupRequest.d.ts +6 -3
  109. package/dist/api/setupRequest.d.ts.map +1 -1
  110. package/dist/api/setupRequest.js +11 -6
  111. package/dist/errorReporter.d.ts +1 -1
  112. package/dist/errorReporter.d.ts.map +1 -1
  113. package/dist/errorReporter.js +1 -1
  114. package/dist/fileUtil.js +1 -1
  115. package/dist/logger.d.ts.map +1 -1
  116. package/dist/rateLimit.js +1 -1
  117. package/examples/query.ts +29 -25
  118. package/package.json +32 -18
  119. package/src/CUPS.ts +2 -2
  120. package/src/Emailer/Sendgrid.ts +1 -1
  121. package/src/Emailer/service.ts +2 -2
  122. package/src/MainFiberSet.ts +2 -2
  123. package/src/Model/Repository/internal/internal.ts +11 -8
  124. package/src/Model/Repository/makeRepo.ts +2 -2
  125. package/src/Operations.ts +2 -2
  126. package/src/OperationsRepo.ts +2 -2
  127. package/src/QueueMaker/SQLQueue.ts +10 -10
  128. package/src/QueueMaker/memQueue.ts +41 -42
  129. package/src/QueueMaker/sbqueue.ts +65 -62
  130. package/src/RequestContext.ts +4 -4
  131. package/src/RequestFiberSet.ts +4 -4
  132. package/src/Store/ContextMapContainer.ts +98 -2
  133. package/src/Store/Cosmos.ts +207 -172
  134. package/src/Store/Disk.ts +2 -3
  135. package/src/Store/Memory.ts +4 -6
  136. package/src/Store/SQL/Pg.ts +294 -0
  137. package/src/Store/SQL/query.ts +372 -0
  138. package/src/Store/SQL.ts +327 -0
  139. package/src/Store/index.ts +10 -0
  140. package/src/Store/service.ts +16 -7
  141. package/src/adapters/SQL/Model.ts +76 -71
  142. package/src/adapters/ServiceBus.ts +8 -8
  143. package/src/adapters/cosmos-client.ts +2 -2
  144. package/src/adapters/memQueue.ts +2 -2
  145. package/src/adapters/mongo-client.ts +2 -2
  146. package/src/adapters/redis-client.ts +2 -2
  147. package/src/api/ContextProvider.ts +11 -11
  148. package/src/api/internal/events.ts +7 -6
  149. package/src/api/layerUtils.ts +8 -8
  150. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  151. package/src/api/routing/middleware/middleware.ts +43 -0
  152. package/src/api/routing/schema/jwt.ts +2 -3
  153. package/src/api/routing.ts +7 -6
  154. package/src/api/setupRequest.ts +27 -7
  155. package/src/errorReporter.ts +1 -1
  156. package/src/fileUtil.ts +1 -1
  157. package/src/rateLimit.ts +2 -2
  158. package/test/contextProvider.test.ts +5 -5
  159. package/test/controller.test.ts +12 -9
  160. package/test/dist/contextProvider.test.d.ts.map +1 -1
  161. package/test/dist/controller.test.d.ts.map +1 -1
  162. package/test/dist/fixtures.d.ts +18 -8
  163. package/test/dist/fixtures.d.ts.map +1 -1
  164. package/test/dist/fixtures.js +11 -9
  165. package/test/dist/query.test.d.ts.map +1 -1
  166. package/test/dist/rawQuery.test.d.ts.map +1 -1
  167. package/test/dist/requires.test.d.ts.map +1 -1
  168. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  169. package/test/dist/sql-store.test.d.ts.map +1 -0
  170. package/test/fixtures.ts +10 -8
  171. package/test/query.test.ts +160 -14
  172. package/test/rawQuery.test.ts +19 -17
  173. package/test/requires.test.ts +6 -5
  174. package/test/rpc-multi-middleware.test.ts +73 -4
  175. package/test/sql-store.test.ts +444 -0
  176. package/test/validateSample.test.ts +1 -1
  177. package/tsconfig.json +0 -1
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, expectTypeOf, it } from "@effect/vitest"
2
- import { Effect, Layer, Result, S, ServiceMap } from "effect-app"
2
+ import { Context, Effect, Layer, Result, S } from "effect-app"
3
3
  import { NotLoggedInError, UnauthorizedError } from "effect-app/client"
4
4
  import { HttpHeaders } from "effect-app/http"
5
5
  import * as RpcX from "effect-app/rpc"
@@ -63,11 +63,12 @@ const testSuite = (_mw: typeof middleware3) =>
63
63
  "works",
64
64
  Effect.fn(function*() {
65
65
  const defaultOpts = {
66
+ client: null as any, // TODO?
66
67
  headers: HttpHeaders.fromRecordUnsafe({}),
67
68
  payload: { _tag: "Test" },
68
69
  clientId: 0,
69
70
  requestId: "test-id" as any,
70
- rpc: { ...TestRpc, annotations: ServiceMap.make(_mw.requestContext, {}) }
71
+ rpc: { ...TestRpc, annotations: Context.make(_mw.requestContext, {}) }
71
72
  }
72
73
  const next = Effect.void as unknown as Effect.Effect<SuccessValue, unhandled, never>
73
74
  const layer = _mw.layer.pipe(
@@ -89,7 +90,7 @@ const testSuite = (_mw: typeof middleware3) =>
89
90
  headers: HttpHeaders.fromRecordUnsafe({ "x-user": "test-user", "x-is-manager": "true" }),
90
91
  rpc: {
91
92
  ...defaultOpts.rpc,
92
- annotations: ServiceMap.make(_mw.requestContext, { requireRoles: ["manager"] })
93
+ annotations: Context.make(_mw.requestContext, { requireRoles: ["manager"] })
93
94
  }
94
95
  })
95
96
  )
@@ -127,7 +128,7 @@ const testSuite = (_mw: typeof middleware3) =>
127
128
  Object.assign({ ...defaultOpts }, {
128
129
  rpc: {
129
130
  ...defaultOpts.rpc,
130
- annotations: ServiceMap.make(_mw.requestContext, { requireRoles: ["manager"] })
131
+ annotations: Context.make(_mw.requestContext, { requireRoles: ["manager"] })
131
132
  }
132
133
  })
133
134
  )
@@ -153,7 +154,7 @@ const testSuite = (_mw: typeof middleware3) =>
153
154
  {
154
155
  rpc: {
155
156
  ...defaultOpts.rpc,
156
- annotations: ServiceMap.make(_mw.requestContext, { requireRoles: ["manager"] })
157
+ annotations: Context.make(_mw.requestContext, { requireRoles: ["manager"] })
157
158
  }
158
159
  }
159
160
  )
@@ -1,14 +1,14 @@
1
1
  import { NodeHttpServer } from "@effect/platform-node"
2
2
  import { expect, expectTypeOf, it } from "@effect/vitest"
3
- import { Console, Effect, Layer, Result } from "effect"
4
- import { S } from "effect-app"
3
+ import { Console, Effect, Layer, Ref, Result } from "effect"
4
+ import { Context, S } from "effect-app"
5
5
  import { NotLoggedInError } from "effect-app/client"
6
6
  import { HttpRouter } from "effect-app/http"
7
7
  import { DefaultGenericMiddlewares } from "effect-app/middleware"
8
8
  import { MiddlewareMaker } from "effect-app/rpc"
9
9
  import { middlewareGroup } from "effect-app/rpc/MiddlewareMaker"
10
10
  import { FetchHttpClient } from "effect/unstable/http"
11
- import { RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
11
+ import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
12
12
  import { createServer } from "http"
13
13
  import { DefaultGenericMiddlewaresLive } from "../src/api/routing.js"
14
14
  import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, Some, SomeElseMiddleware, SomeElseMiddlewareLive, SomeMiddleware, SomeMiddlewareLive, SomeService, Test, TestLive, UserProfile } from "./fixtures.js"
@@ -109,7 +109,7 @@ export const RpcRealLayer = Layer
109
109
  Layer.provide(FetchHttpClient.layer)
110
110
  )
111
111
  )
112
- .pipe(Layer.provide(RpcSerialization.layerJson))
112
+ .pipe(Layer.provide(RpcSerialization.layerNdjson))
113
113
 
114
114
  it.live(
115
115
  "require login",
@@ -136,3 +136,72 @@ it.live(
136
136
  Effect.provide(RpcTestLayer)
137
137
  )
138
138
  )
139
+
140
+ // Per-request service isolation test
141
+
142
+ class PerRequestCounter extends Context.Service<PerRequestCounter>()(
143
+ "PerRequestCounter",
144
+ { make: Effect.sync(() => ({ a: 0 })) }
145
+ ) {
146
+ static Default = Layer.effect(this, this.make)
147
+ }
148
+
149
+ class GlobalCounter extends Context.Service<GlobalCounter, {
150
+ readonly ref: Ref.Ref<number>
151
+ }>()("GlobalCounter") {}
152
+
153
+ const CounterRpcs = RpcGroup.make(
154
+ Rpc.make("incrementA", {
155
+ success: S.Number
156
+ }),
157
+ Rpc.make("incrementB", {
158
+ success: S.Number
159
+ })
160
+ )
161
+
162
+ const counterImpl = CounterRpcs
163
+ .toLayer({
164
+ incrementA: Effect.fn(function*() {
165
+ const counter = yield* PerRequestCounter
166
+ counter.a++
167
+ const global = yield* GlobalCounter
168
+ yield* Ref.update(global.ref, (n) => n + 1)
169
+ return counter.a
170
+ }, Effect.provide(PerRequestCounter.Default)),
171
+ incrementB: Effect.fn(function*() {
172
+ const counter = yield* PerRequestCounter
173
+ counter.a++
174
+ const global = yield* GlobalCounter
175
+ yield* Ref.update(global.ref, (n) => n + 1)
176
+ return counter.a
177
+ }, Effect.provide(PerRequestCounter.Default))
178
+ })
179
+
180
+ const GlobalCounterLive = Layer.effect(
181
+ GlobalCounter,
182
+ Ref.make(0).pipe(Effect.map((ref) => ({ ref })))
183
+ )
184
+
185
+ const CounterTestLayer = counterImpl.pipe(Layer.provideMerge(GlobalCounterLive))
186
+
187
+ it.live(
188
+ "per-request service isolation with shared global counter",
189
+ Effect.fnUntraced(
190
+ function*() {
191
+ const client = yield* RpcTest.makeClient(CounterRpcs)
192
+ const global = yield* GlobalCounter
193
+
194
+ const r1 = yield* client.incrementA()
195
+ const r2 = yield* client.incrementB()
196
+
197
+ // per-request counter is fresh each time → both return 1
198
+ expect(r1).toBe(1)
199
+ expect(r2).toBe(1)
200
+
201
+ // global counter is shared across requests → accumulates to 2
202
+ const globalCount = yield* Ref.get(global.ref)
203
+ expect(globalCount).toBe(2)
204
+ },
205
+ Effect.provide(CounterTestLayer)
206
+ )
207
+ )
@@ -0,0 +1,444 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type Sqlite from "better-sqlite3"
3
+ import BetterSqlite from "better-sqlite3"
4
+ import { describe, expect, it } from "vitest"
5
+ import { buildWhereSQLQuery, pgDialect, sqliteDialect } from "../src/Store/SQL/query.js"
6
+
7
+ const query = (db: Sqlite.Database, sql: string, params: unknown[] = []) =>
8
+ db.prepare(sql).all(...params as any[]) as any[]
9
+
10
+ // --- Query builder unit tests ---
11
+
12
+ describe("SQL query builder (SQLite dialect)", () => {
13
+ it("where eq string", () => {
14
+ const result = buildWhereSQLQuery(
15
+ sqliteDialect,
16
+ "id",
17
+ [{ t: "where", path: "name", op: "eq", value: "John" }],
18
+ "users",
19
+ {}
20
+ )
21
+ expect(result.sql).toContain("json_extract(data, '$.name') = ?")
22
+ expect(result.params).toContain("John")
23
+ })
24
+
25
+ it("where eq number", () => {
26
+ const result = buildWhereSQLQuery(
27
+ sqliteDialect,
28
+ "id",
29
+ [{ t: "where", path: "age", op: "eq", value: 25 as any }],
30
+ "users",
31
+ {}
32
+ )
33
+ expect(result.sql).toContain("json_extract(data, '$.age') = ?")
34
+ expect(result.params).toContain(25)
35
+ })
36
+
37
+ it("where gt", () => {
38
+ const result = buildWhereSQLQuery(
39
+ sqliteDialect,
40
+ "id",
41
+ [{ t: "where", path: "age", op: "gt", value: 18 as any }],
42
+ "users",
43
+ {}
44
+ )
45
+ expect(result.sql).toContain("json_extract(data, '$.age') > ?")
46
+ expect(result.params).toContain(18)
47
+ })
48
+
49
+ it("where or", () => {
50
+ const result = buildWhereSQLQuery(
51
+ sqliteDialect,
52
+ "id",
53
+ [
54
+ { t: "where", path: "name", op: "eq", value: "Alice" },
55
+ { t: "or", path: "name", op: "eq", value: "Bob" }
56
+ ],
57
+ "users",
58
+ {}
59
+ )
60
+ expect(result.sql).toContain("= ?")
61
+ expect(result.sql).toContain("OR")
62
+ expect(result.params).toEqual(expect.arrayContaining(["Alice", "Bob"]))
63
+ })
64
+
65
+ it("where and", () => {
66
+ const result = buildWhereSQLQuery(
67
+ sqliteDialect,
68
+ "id",
69
+ [
70
+ { t: "where", path: "name", op: "eq", value: "Alice" },
71
+ { t: "and", path: "age", op: "gt", value: 18 as any }
72
+ ],
73
+ "users",
74
+ {}
75
+ )
76
+ expect(result.sql).toContain("AND")
77
+ expect(result.params).toEqual(expect.arrayContaining(["Alice", 18]))
78
+ })
79
+
80
+ it("where in", () => {
81
+ const result = buildWhereSQLQuery(
82
+ sqliteDialect,
83
+ "id",
84
+ [{ t: "where", path: "id", op: "in", value: ["a", "b", "c"] as any }],
85
+ "users",
86
+ {}
87
+ )
88
+ expect(result.sql).toContain("id IN (?, ?, ?)")
89
+ expect(result.params).toEqual(expect.arrayContaining(["a", "b", "c"]))
90
+ })
91
+
92
+ it("where null", () => {
93
+ const result = buildWhereSQLQuery(
94
+ sqliteDialect,
95
+ "id",
96
+ [{ t: "where", path: "status", op: "eq", value: null as any }],
97
+ "users",
98
+ {}
99
+ )
100
+ expect(result.sql).toContain("IS NULL")
101
+ })
102
+
103
+ it("where neq null", () => {
104
+ const result = buildWhereSQLQuery(
105
+ sqliteDialect,
106
+ "id",
107
+ [{ t: "where", path: "status", op: "neq", value: null as any }],
108
+ "users",
109
+ {}
110
+ )
111
+ expect(result.sql).toContain("IS NOT NULL")
112
+ })
113
+
114
+ it("where contains", () => {
115
+ const result = buildWhereSQLQuery(
116
+ sqliteDialect,
117
+ "id",
118
+ [{ t: "where", path: "name", op: "contains", value: "oh" }],
119
+ "users",
120
+ {}
121
+ )
122
+ expect(result.sql).toContain("LIKE")
123
+ expect(result.sql).toContain("LOWER")
124
+ expect(result.params).toContain("%oh%")
125
+ })
126
+
127
+ it("where startsWith", () => {
128
+ const result = buildWhereSQLQuery(
129
+ sqliteDialect,
130
+ "id",
131
+ [{ t: "where", path: "name", op: "startsWith", value: "Jo" }],
132
+ "users",
133
+ {}
134
+ )
135
+ expect(result.sql).toContain("LIKE")
136
+ expect(result.params).toContain("Jo%")
137
+ })
138
+
139
+ it("where endsWith", () => {
140
+ const result = buildWhereSQLQuery(
141
+ sqliteDialect,
142
+ "id",
143
+ [{ t: "where", path: "name", op: "endsWith", value: "hn" }],
144
+ "users",
145
+ {}
146
+ )
147
+ expect(result.sql).toContain("LIKE")
148
+ expect(result.params).toContain("%hn")
149
+ })
150
+
151
+ it("where includes (array contains)", () => {
152
+ const result = buildWhereSQLQuery(
153
+ sqliteDialect,
154
+ "id",
155
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
156
+ "users",
157
+ {}
158
+ )
159
+ expect(result.sql).toContain("json_each")
160
+ expect(result.sql).toContain("value = ?")
161
+ expect(result.params).toContain("admin")
162
+ })
163
+
164
+ it("where includes-any (array contains any)", () => {
165
+ const result = buildWhereSQLQuery(
166
+ sqliteDialect,
167
+ "id",
168
+ [{ t: "where", path: "tags", op: "includes-any", value: ["admin", "user"] as any }],
169
+ "users",
170
+ {}
171
+ )
172
+ expect(result.sql).toContain("json_each")
173
+ expect(result.sql).toContain("IN")
174
+ })
175
+
176
+ it("nested scopes", () => {
177
+ const result = buildWhereSQLQuery(
178
+ sqliteDialect,
179
+ "id",
180
+ [
181
+ { t: "where", path: "a", op: "eq", value: "1" },
182
+ {
183
+ t: "or-scope",
184
+ result: [
185
+ { t: "where", path: "b", op: "eq", value: "2" },
186
+ { t: "and", path: "c", op: "eq", value: "3" }
187
+ ],
188
+ relation: "some" as const
189
+ }
190
+ ],
191
+ "test",
192
+ {}
193
+ )
194
+ expect(result.sql).toContain("OR (")
195
+ expect(result.sql).toContain("AND")
196
+ expect(result.params).toEqual(expect.arrayContaining(["1", "2", "3"]))
197
+ })
198
+
199
+ it("id key maps to id column", () => {
200
+ const result = buildWhereSQLQuery(
201
+ sqliteDialect,
202
+ "myId",
203
+ [{ t: "where", path: "myId", op: "eq", value: "123" }],
204
+ "users",
205
+ {}
206
+ )
207
+ expect(result.sql).toContain("id = ?")
208
+ expect(result.sql).not.toContain("json_extract")
209
+ expect(result.params).toContain("123")
210
+ })
211
+
212
+ it("order + limit + skip", () => {
213
+ const result = buildWhereSQLQuery(
214
+ sqliteDialect,
215
+ "id",
216
+ [],
217
+ "users",
218
+ {},
219
+ undefined,
220
+ [{ key: "name", direction: "ASC" }] as any,
221
+ 5,
222
+ 10
223
+ )
224
+ expect(result.sql).toContain("ORDER BY")
225
+ expect(result.sql).toContain("ASC")
226
+ expect(result.sql).toContain("LIMIT")
227
+ expect(result.sql).toContain("OFFSET")
228
+ })
229
+ })
230
+
231
+ describe("SQL query builder (PostgreSQL dialect)", () => {
232
+ it("where eq string uses ->> operator", () => {
233
+ const result = buildWhereSQLQuery(
234
+ pgDialect,
235
+ "id",
236
+ [{ t: "where", path: "name", op: "eq", value: "John" }],
237
+ "users",
238
+ {}
239
+ )
240
+ expect(result.sql).toContain("data->>'name'")
241
+ expect(result.sql).toContain("$1")
242
+ expect(result.params).toContain("John")
243
+ })
244
+
245
+ it("where contains uses ILIKE", () => {
246
+ const result = buildWhereSQLQuery(
247
+ pgDialect,
248
+ "id",
249
+ [{ t: "where", path: "name", op: "contains", value: "oh" }],
250
+ "users",
251
+ {}
252
+ )
253
+ expect(result.sql).toContain("ILIKE")
254
+ expect(result.params).toContain("%oh%")
255
+ })
256
+
257
+ it("where in uses $N placeholders", () => {
258
+ const result = buildWhereSQLQuery(
259
+ pgDialect,
260
+ "id",
261
+ [{ t: "where", path: "status", op: "in", value: ["active", "pending"] as any }],
262
+ "users",
263
+ {}
264
+ )
265
+ expect(result.sql).toContain("$1")
266
+ expect(result.sql).toContain("$2")
267
+ expect(result.params).toEqual(expect.arrayContaining(["active", "pending"]))
268
+ })
269
+
270
+ it("where includes uses @> jsonb operator", () => {
271
+ const result = buildWhereSQLQuery(
272
+ pgDialect,
273
+ "id",
274
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
275
+ "users",
276
+ {}
277
+ )
278
+ expect(result.sql).toContain("@>")
279
+ expect(result.sql).toContain("jsonb")
280
+ })
281
+
282
+ it("nested path uses chained -> operators", () => {
283
+ const result = buildWhereSQLQuery(
284
+ pgDialect,
285
+ "id",
286
+ [{ t: "where", path: "address.city", op: "eq", value: "NYC" }],
287
+ "users",
288
+ {}
289
+ )
290
+ expect(result.sql).toContain("data->'address'->>'city'")
291
+ })
292
+ })
293
+
294
+ // --- Integration tests with in-memory SQLite (direct, no Effect SQL client) ---
295
+
296
+ describe("SQL Store (SQLite integration)", () => {
297
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
298
+ const db = new BetterSqlite(":memory:")
299
+ db.pragma("journal_mode = WAL")
300
+ try {
301
+ fn(db)
302
+ } finally {
303
+ db.close()
304
+ }
305
+ }
306
+
307
+ it("creates table and seeds data", () =>
308
+ withDb((db) => {
309
+ db.exec(
310
+ `CREATE TABLE IF NOT EXISTS "test_items" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
311
+ )
312
+ db.prepare(`INSERT INTO "test_items" (id, _etag, data) VALUES (?, ?, ?)`)
313
+ .run("1", "etag1", JSON.stringify({ id: "1", name: "Alice", age: 30 }))
314
+
315
+ const rows = db.prepare(`SELECT * FROM "test_items"`).all()
316
+ expect(rows.length).toBe(1)
317
+ expect((rows[0] as any).id).toBe("1")
318
+ }))
319
+
320
+ it("query builder generates valid SQL for SQLite", () =>
321
+ withDb((db) => {
322
+ db.exec(
323
+ `CREATE TABLE IF NOT EXISTS "test_people" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
324
+ )
325
+
326
+ const people = [
327
+ { id: "1", name: "Alice", age: 30, tags: ["admin", "user"] },
328
+ { id: "2", name: "Bob", age: 25, tags: ["user"] },
329
+ { id: "3", name: "Charlie", age: 35, tags: ["admin"] },
330
+ { id: "4", name: "Diana", age: 28, tags: ["user", "editor"] }
331
+ ]
332
+
333
+ const insert = db.prepare(
334
+ `INSERT INTO "test_people" (id, _etag, data) VALUES (?, ?, ?)`
335
+ )
336
+ for (const p of people) {
337
+ insert.run(p.id, `etag_${p.id}`, JSON.stringify(p))
338
+ }
339
+
340
+ // Test eq
341
+ const q1 = buildWhereSQLQuery(
342
+ sqliteDialect, "id",
343
+ [{ t: "where", path: "name", op: "eq", value: "Alice" }],
344
+ "test_people", {}
345
+ )
346
+ expect(query(db, q1.sql, q1.params).length).toBe(1)
347
+ expect((JSON.parse((query(db, q1.sql, q1.params)[0] as any).data) as any).name).toBe("Alice")
348
+
349
+ // Test gt
350
+ const q2 = buildWhereSQLQuery(
351
+ sqliteDialect, "id",
352
+ [{ t: "where", path: "age", op: "gt", value: 28 as any }],
353
+ "test_people", {}
354
+ )
355
+ expect(query(db, q2.sql, q2.params).length).toBe(2)
356
+
357
+ // Test OR
358
+ const q3 = buildWhereSQLQuery(
359
+ sqliteDialect, "id",
360
+ [
361
+ { t: "where", path: "name", op: "eq", value: "Alice" },
362
+ { t: "or", path: "name", op: "eq", value: "Bob" }
363
+ ],
364
+ "test_people", {}
365
+ )
366
+ expect(query(db, q3.sql, q3.params).length).toBe(2)
367
+
368
+ // Test AND
369
+ const q4 = buildWhereSQLQuery(
370
+ sqliteDialect, "id",
371
+ [
372
+ { t: "where", path: "name", op: "eq", value: "Alice" },
373
+ { t: "and", path: "age", op: "gt", value: 25 as any }
374
+ ],
375
+ "test_people", {}
376
+ )
377
+ const r4 = query(db, q4.sql, q4.params)
378
+ expect(r4.length).toBe(1)
379
+ expect((JSON.parse((r4[0] as any).data) as any).name).toBe("Alice")
380
+
381
+ // Test IN
382
+ const q5 = buildWhereSQLQuery(
383
+ sqliteDialect, "id",
384
+ [{ t: "where", path: "id", op: "in", value: ["1", "3"] as any }],
385
+ "test_people", {}
386
+ )
387
+ expect(query(db, q5.sql, q5.params).length).toBe(2)
388
+
389
+ // Test contains (string)
390
+ const q6 = buildWhereSQLQuery(
391
+ sqliteDialect, "id",
392
+ [{ t: "where", path: "name", op: "contains", value: "li" }],
393
+ "test_people", {}
394
+ )
395
+ expect(query(db, q6.sql, q6.params).length).toBe(2) // Alice, Charlie
396
+
397
+ // Test startsWith
398
+ const q7 = buildWhereSQLQuery(
399
+ sqliteDialect, "id",
400
+ [{ t: "where", path: "name", op: "startsWith", value: "Al" }],
401
+ "test_people", {}
402
+ )
403
+ const r7 = query(db, q7.sql, q7.params)
404
+ expect(r7.length).toBe(1)
405
+ expect((JSON.parse((r7[0] as any).data) as any).name).toBe("Alice")
406
+
407
+ // Test includes (array)
408
+ const q8 = buildWhereSQLQuery(
409
+ sqliteDialect, "id",
410
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
411
+ "test_people", {}
412
+ )
413
+ expect(query(db, q8.sql, q8.params).length).toBe(2) // Alice, Charlie
414
+
415
+ // Test nested scope: where name = Alice OR (age > 30 AND name contains 'ar')
416
+ const q9 = buildWhereSQLQuery(
417
+ sqliteDialect, "id",
418
+ [
419
+ { t: "where", path: "name", op: "eq", value: "Alice" },
420
+ {
421
+ t: "or-scope",
422
+ result: [
423
+ { t: "where", path: "age", op: "gt", value: 30 as any },
424
+ { t: "and", path: "name", op: "contains", value: "ar" }
425
+ ],
426
+ relation: "some"
427
+ }
428
+ ],
429
+ "test_people", {}
430
+ )
431
+ expect(query(db, q9.sql, q9.params).length).toBe(2) // Alice + Charlie
432
+
433
+ // Test order + limit
434
+ const q10 = buildWhereSQLQuery(
435
+ sqliteDialect, "id", [], "test_people", {},
436
+ undefined,
437
+ [{ key: "age", direction: "DESC" }] as any,
438
+ undefined, 2
439
+ )
440
+ const r10 = query(db, q10.sql, q10.params)
441
+ expect(r10.length).toBe(2)
442
+ expect((JSON.parse((r10[0] as any).data) as any).name).toBe("Charlie") // oldest first
443
+ }))
444
+ })
@@ -138,7 +138,7 @@ describe("validateSample", () => {
138
138
  // schema that expects a 'status' field
139
139
  class ItemWithStatus extends S.Class<ItemWithStatus>("ItemWithStatus")({
140
140
  id: S.String,
141
- status: S.Literal("active", "inactive")
141
+ status: S.Literals(["active", "inactive"])
142
142
  }) {}
143
143
 
144
144
  // jitM that adds default status for items
package/tsconfig.json CHANGED
@@ -32,7 +32,6 @@
32
32
  "outDir": "build/dist",
33
33
  "resolveJsonModule": true,
34
34
  "moduleResolution": "Node16",
35
- "downlevelIteration": true,
36
35
  "noErrorTruncation": true,
37
36
  "forceConsistentCasingInFileNames": true,
38
37
  "types": [