@effect-app/infra 2.61.8 → 2.63.0

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.
@@ -3,7 +3,7 @@
3
3
  import { Array, Effect, type NonEmptyReadonlyArray } from "effect-app"
4
4
  import { assertUnreachable } from "effect-app/utils"
5
5
  import { InfraLogger } from "../../logger.js"
6
- import type { FilterR, FilterResult } from "../../Model/filter/filterApi.js"
6
+ import type { FilterR, FilterResult, Ops } from "../../Model/filter/filterApi.js"
7
7
  import { isRelationCheck } from "../codeFilter.js"
8
8
  import type { SupportedValues } from "../service.js"
9
9
 
@@ -125,7 +125,48 @@ export function buildWhereCosmosQuery3(
125
125
 
126
126
  let i = 0
127
127
 
128
- const print = (state: readonly FilterResult[], values: any[], isRelation: string | null = null) => {
128
+ const flipOps = {
129
+ gt: "lt",
130
+ lt: "gt",
131
+ gte: "lte",
132
+ lte: "gte",
133
+ contains: "notContains",
134
+ notContains: "contains",
135
+ startsWith: "notStartsWith",
136
+ notStartsWith: "startsWith",
137
+ endsWith: "notEndsWith",
138
+ notEndsWith: "endsWith",
139
+ eq: "neq",
140
+ neq: "eq",
141
+ includes: "notIncludes",
142
+ notIncludes: "includes",
143
+ "includes-any": "notIncludes-any",
144
+ "notIncludes-any": "includes-any",
145
+ "includes-all": "notIncludes-all",
146
+ "notIncludes-all": "includes-all",
147
+ in: "notIn",
148
+ notIn: "in"
149
+ } satisfies Record<Ops, Ops>
150
+
151
+ const flippies = {
152
+ and: "or",
153
+ or: "and"
154
+ } satisfies Record<"and" | "or", "and" | "or">
155
+
156
+ const flip = (every: boolean) => (_: FilterResult): FilterResult =>
157
+ every
158
+ ? _.t === "where" || _.t === "or" || _.t === "and"
159
+ ? {
160
+ ..._,
161
+ t: _.t === "where"
162
+ ? _.t
163
+ : flippies[_.t],
164
+ op: flipOps[_.op]
165
+ }
166
+ : _
167
+ : _
168
+
169
+ const print = (state: readonly FilterResult[], values: any[], isRelation: string | null, every: boolean) => {
129
170
  let s = ""
130
171
  let l = 0
131
172
  const printN = (n: number) => {
@@ -144,48 +185,62 @@ export function buildWhereCosmosQuery3(
144
185
  break
145
186
  case "or-scope": {
146
187
  ++l
188
+ if (!every) every = e.relation === "every"
147
189
  const rel = isRelationCheck(e.result, isRelation)
148
190
  if (rel) {
149
191
  const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
150
192
  s += isRelation
151
- ? ` OR (\n${printN(l + 1)}${print(e.result, values)}\n${printN(l)})`
152
- : ` OR (\n${printN(l + 1)}EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
153
- print(e.result, values, rel)
193
+ ? ` OR (\n${printN(l + 1)}${print(e.result, values, rel, every)}\n${printN(l)})`
194
+ : ` OR (\n${printN(l + 1)}${
195
+ every ? "NOT " : ""
196
+ }EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
197
+ print(
198
+ e
199
+ .result
200
+ .map(flip(every)),
201
+ values,
202
+ rel,
203
+ every
204
+ )
154
205
  }))`
155
206
  } else {
156
- s += ` OR (\n${printN(l + 1)}${print(e.result, values)}\n${printN(l)})`
207
+ s += ` OR (\n${printN(l + 1)}${print(e.result, values, null, every)}\n${printN(l)})`
157
208
  }
158
209
  --l
159
210
  break
160
211
  }
161
212
  case "and-scope": {
162
213
  ++l
214
+ if (!every) every = e.relation === "every"
163
215
  const rel = isRelationCheck(e.result, isRelation)
164
216
  if (rel) {
165
217
  const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
166
218
  s += isRelation
167
- ? ` AND (\n${printN(l + 1)}${print(e.result, values)}\n${printN(l)})`
168
- : ` AND (\n${printN(l + 1)}EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
169
- print(e.result, values, rel)
219
+ ? ` AND (\n${printN(l + 1)}${print(e.result, values, rel, every)}\n${printN(l)})`
220
+ : ` AND (\n${printN(l + 1)}${
221
+ every ? "NOT " : ""
222
+ }EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
223
+ print(e.result.map(flip(every)), values, rel, every)
170
224
  }))`
171
225
  } else {
172
- s += ` AND (\n${printN(l + 1)}${print(e.result, values)}\n${printN(l)})`
226
+ s += ` AND (\n${printN(l + 1)}${print(e.result, values, null, every)}\n${printN(l)})`
173
227
  }
174
228
  --l
175
229
  break
176
230
  }
177
231
  case "where-scope": {
178
232
  // ;++l
233
+ if (!every) every = e.relation === "every"
179
234
  const rel = isRelationCheck(e.result, isRelation)
180
235
  if (rel) {
181
236
  const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
182
237
  s += isRelation
183
- ? `(\n${printN(l + 1)}${print(e.result, values)}\n${printN(l)})`
184
- : `(\n${printN(l + 1)}EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
185
- print(e.result, values, rel)
238
+ ? `(\n${printN(l + 1)}${print(e.result, values, rel, every)}\n${printN(l)})`
239
+ : `(\n${printN(l + 1)}${every ? "NOT " : ""}EXISTS(SELECT VALUE ${rel} FROM ${rel} IN f.${rel} WHERE ${
240
+ print(e.result.map(flip(every)), values, rel, every)
186
241
  }))`
187
242
  } else {
188
- s += `(\n${printN(l + 1)}${print(e.result, values)}\n${printN(l)})`
243
+ s += `(\n${printN(l + 1)}${print(e.result, values, null, every)}\n${printN(l)})`
189
244
  }
190
245
  // ;--l
191
246
  break
@@ -239,7 +294,7 @@ export function buildWhereCosmosQuery3(
239
294
  }
240
295
  FROM ${name} f
241
296
 
242
- WHERE f.id != @id ${filter.length ? `AND (${print(filter, values.map((_) => _.value))})` : ""}
297
+ WHERE f.id != @id ${filter.length ? `AND (${print(filter, values.map((_) => _.value), null, false)})` : ""}
243
298
  ${order ? `ORDER BY ${order.map((_) => `${dottedToAccess(`f.${_.key}`)} ${_.direction}`).join(", ")}` : ""}
244
299
  ${skip !== undefined || limit !== undefined ? `OFFSET ${skip ?? 0} LIMIT ${limit ?? 999999}` : ""}`,
245
300
  parameters: [
@@ -54,6 +54,8 @@ function makeCosmosStore({ prefix }: StorageConfig) {
54
54
  const container = db.container(containerId)
55
55
  const bulk = container.items.bulk.bind(container.items)
56
56
  const execBatch = container.items.batch.bind(container.items)
57
+ // TODO: move the marker to a separate container and get rid of the checks on every query
58
+ // then need to clean up the actual data.. perhaps first do with a config toggle to prescribe to it.
57
59
  const importedMarkerId = containerId
58
60
 
59
61
  const bulkSet = (items: NonEmptyReadonlyArray<PM>) =>
@@ -288,7 +290,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
288
290
  .sync(() =>
289
291
  buildWhereCosmosQuery3(
290
292
  idKey,
291
- filter ? [{ t: "where-scope", result: filter }] : [],
293
+ filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
292
294
  name,
293
295
  importedMarkerId,
294
296
  defaultValues,
@@ -93,7 +93,8 @@ const codeFilter3__ = <E>(
93
93
  state: readonly FilterResult[],
94
94
  sut: E,
95
95
  statements: any[],
96
- isRelation: string | null = null
96
+ isRelation: string | null,
97
+ every: boolean
97
98
  ): string => {
98
99
  let s = ""
99
100
  let l = 0
@@ -126,32 +127,34 @@ const codeFilter3__ = <E>(
126
127
  break
127
128
  case "or-scope": {
128
129
  ++l
130
+ if (!every) every = e.relation === "every"
129
131
  const rel = isRelationCheck(e.result, isRelation)
130
132
  if (rel) {
131
133
  const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
132
134
  s += isRelation
133
- ? ` || (\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, rel)}\n${printN(l)})`
134
- : ` || (\n${printN(l + 1)}sut.${rel}.some(el => ${codeFilter3__(e.result, sut, statements, rel)})\n${
135
- printN(l)
136
- })`
135
+ ? ` || (\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, rel, every)}\n${printN(l)})`
136
+ : ` || (\n${printN(l + 1)}sut.${rel}.${every ? "every" : "some"}(el => ${
137
+ codeFilter3__(e.result, sut, statements, rel, every)
138
+ })\n${printN(l)})`
137
139
  } else {
138
- s += ` || (\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements)}\n${printN(l)})`
140
+ s += ` || (\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, null, every)}\n${printN(l)})`
139
141
  }
140
142
  --l
141
143
  break
142
144
  }
143
145
  case "and-scope": {
144
146
  ++l
147
+ if (!every) every = e.relation === "every"
145
148
  const rel = isRelationCheck(e.result, isRelation)
146
149
  if (rel) {
147
150
  const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
148
151
  s += isRelation
149
- ? ` && (\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, rel)}\n${printN(l)})`
150
- : ` && (\n${printN(l + 1)}sut.${rel}.some(el => ${codeFilter3__(e.result, sut, statements, rel)})\n${
151
- printN(l)
152
- })`
152
+ ? ` && (\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, rel, every)}\n${printN(l)})`
153
+ : ` && (\n${printN(l + 1)}sut.${rel}.${every ? "every" : "some"}(el => ${
154
+ codeFilter3__(e.result, sut, statements, rel, every)
155
+ })\n${printN(l)})`
153
156
  } else {
154
- s += ` && (\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements)}\n${printN(l)})`
157
+ s += ` && (\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, null, every)}\n${printN(l)})`
155
158
  }
156
159
  --l
157
160
 
@@ -159,16 +162,17 @@ const codeFilter3__ = <E>(
159
162
  }
160
163
  case "where-scope": {
161
164
  // ;++l
165
+ if (!every) every = e.relation === "every"
162
166
  const rel = isRelationCheck(e.result, isRelation)
163
167
  if (rel) {
164
168
  const rel = (e.result[0]! as { path: string }).path.split(".-1.")[0]
165
169
  s += isRelation
166
- ? `(\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, rel)}\n${printN(l)})`
167
- : `(\n${printN(l + 1)}sut.${rel}.some(el => ${codeFilter3__(e.result, sut, statements, rel)})\n${
168
- printN(l)
169
- })`
170
+ ? `(\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, rel, every)}\n${printN(l)})`
171
+ : `(\n${printN(l + 1)}sut.${rel}.${every ? "every" : "some"}(el => ${
172
+ codeFilter3__(e.result, sut, statements, rel, every)
173
+ })\n${printN(l)})`
170
174
  } else {
171
- s += `(\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements)}\n${printN(l)})`
175
+ s += `(\n${printN(l + 1)}${codeFilter3__(e.result, sut, statements, null, every)}\n${printN(l)})`
172
176
  }
173
177
  // ;--l
174
178
  break
@@ -181,7 +185,7 @@ const codeFilter3__ = <E>(
181
185
  export const codeFilter3_ = <E>(state: readonly FilterResult[], sut: E): boolean => {
182
186
  const statements: any[] = [] // must be defined here to be used by eval.
183
187
  // always put everything inside a root scope.
184
- const s = codeFilter3__([{ t: "where-scope", result: state }], sut, statements)
188
+ const s = codeFilter3__([{ t: "where-scope", result: state, relation: "some" }], sut, statements, null, false)
185
189
  return eval(s)
186
190
  }
187
191
 
@@ -1 +1 @@
1
- {"version":3,"file":"rawQuery.test.d.ts","sourceRoot":"","sources":["../rawQuery.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAkD,cAAc,EAAuB,MAAM,YAAY,CAAA;AAChH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAO5C,eAAO,MAAM,EAAE,iDAUb,CAAA"}
1
+ {"version":3,"file":"rawQuery.test.d.ts","sourceRoot":"","sources":["../rawQuery.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAwD,cAAc,EAAuB,MAAM,YAAY,CAAA;AACtH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAO5C,eAAO,MAAM,EAAE,iDAUb,CAAA"}
@@ -1,8 +1,8 @@
1
1
  import { describe, expect, it } from "@effect/vitest"
2
- import { Array, Config, Effect, Layer, Logger, LogLevel, ManagedRuntime, Option, Redacted, S } from "effect-app"
2
+ import { Array, Config, Effect, flow, Layer, Logger, LogLevel, ManagedRuntime, Option, Redacted, S } from "effect-app"
3
3
  import { LogLevels } from "effect-app/utils"
4
4
  import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js"
5
- import { and, or, project, where } from "../src/Model/query.js"
5
+ import { and, or, project, where, whereEvery, whereSome } from "../src/Model/query.js"
6
6
  import { makeRepo } from "../src/Model/Repository/makeRepo.js"
7
7
  import { CosmosStoreLayer } from "../src/Store/Cosmos.js"
8
8
  import { MemoryStoreLive } from "../src/Store/Memory.js"
@@ -146,6 +146,35 @@ describe("select first-level array fields", () => {
146
146
  .pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
147
147
  })
148
148
 
149
+ const projected = S.Struct({ name: S.String, items: S.Array(S.Struct({ id: S.String, value: S.Number })) })
150
+
151
+ const expected = [
152
+ {
153
+ name: "Item 2",
154
+ items: [
155
+ { id: "2-1", value: 30 },
156
+ { id: "2-2", value: 40 }
157
+ ]
158
+ }
159
+ ]
160
+
161
+ const both = [
162
+ {
163
+ name: "Item 1",
164
+ items: [
165
+ { id: "1-1", value: 10 },
166
+ { id: "1-2", value: 20 }
167
+ ]
168
+ },
169
+ {
170
+ name: "Item 2",
171
+ items: [
172
+ { id: "2-1", value: 30 },
173
+ { id: "2-2", value: 40 }
174
+ ]
175
+ }
176
+ ]
177
+
149
178
  // NOTE: right now we cannot specify if all/"every" items must match the filter, or if at least one item (any/"some") must match the filter.
150
179
  // the current implementation is any/some, so we can always filter down in the code to narrow further..
151
180
  describe("filter first-level array fields as groups", () => {
@@ -153,8 +182,6 @@ describe("filter first-level array fields as groups", () => {
153
182
  .gen(function*() {
154
183
  const repo = yield* SomethingRepo
155
184
 
156
- const projected = S.Struct({ name: S.String, items: S.Array(S.Struct({ id: S.String, value: S.Number })) })
157
-
158
185
  // ok crazy lol, "value" is a reserved word in CosmosDB, so we have to use t["value"] as a field name instead of t.value
159
186
  // deprecated; joins should be avoided because they're very expensive, and require DISTINCT to avoid duplicates
160
187
  // which might affect results in unexpected ways?
@@ -200,73 +227,104 @@ describe("filter first-level array fields as groups", () => {
200
227
  )
201
228
  })
202
229
 
230
+ expect(items).toStrictEqual(expected)
231
+ expect(itemsExists).toStrictEqual(expected)
232
+ })
233
+ .pipe(setupRequestContextFromCurrent())
234
+
235
+ it("works well in CosmosDB", () =>
236
+ test
237
+ .pipe(Effect.provide(SomethingRepo.TestCosmos), rt.runPromise))
238
+
239
+ it("works well in Memory", () =>
240
+ test
241
+ .pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
242
+ })
243
+
244
+ describe("1", () => {
245
+ const test = Effect
246
+ .gen(function*() {
247
+ const repo = yield* SomethingRepo
203
248
  const items2 = yield* repo.query(
204
- where("items.-1.value", "gt", 20),
205
- and("items.-1.description", "contains", "d item"),
249
+ whereSome(
250
+ "items",
251
+ flow(
252
+ where("value", "gt", 20),
253
+ and("description", "contains", "d item")
254
+ )
255
+ ),
206
256
  project(projected)
207
257
  )
258
+ expect(items2).toStrictEqual(expected)
208
259
 
209
260
  const items2Or = yield* repo.query(
210
- where("items.-1.value", "gt", 20),
211
- or("items.-1.description", "contains", "d item"),
261
+ whereSome(
262
+ "items",
263
+ flow(
264
+ where("value", "gt", 20),
265
+ or("description", "contains", "d item")
266
+ )
267
+ ),
212
268
  project(projected)
213
269
  )
214
270
 
271
+ expect(items2Or).toStrictEqual(both)
215
272
  // mixing relation check with scoped relationcheck
216
273
  const items3 = yield* repo.query(
217
- where("items.-1.value", "gt", 20),
218
- and(where("items.-1.description", "contains", "d item")),
274
+ whereSome(
275
+ "items",
276
+ flow(where("value", "gt", 20), and(where("description", "contains", "d item")))
277
+ ),
219
278
  project(projected)
220
279
  )
221
280
 
281
+ expect(items3).toStrictEqual(expected)
222
282
  const items3Or = yield* repo.query(
223
- where("items.-1.value", "gt", 20),
224
- or(where("items.-1.description", "contains", "d item")),
283
+ whereSome(
284
+ "items",
285
+ flow(
286
+ where("value", "gt", 20),
287
+ or(where("description", "contains", "d item"))
288
+ )
289
+ ),
225
290
  project(projected)
226
291
  )
227
292
 
228
- // broken in cosmos db somehow... returns twice record 2??
229
- // need to use DISTINCT..
230
- // https://stackoverflow.com/questions/51855660/cosmos-db-joins-give-duplicate-results
293
+ expect(items3Or).toStrictEqual(both)
231
294
  const items4 = yield* repo.query(
232
- where("items.-1.value", "gt", 10),
295
+ whereSome("items", where("value", "gt", 10)),
233
296
  project(projected)
234
297
  )
235
298
 
236
- const expected = [
237
- {
238
- name: "Item 2",
239
- items: [
240
- { id: "2-1", value: 30 },
241
- { id: "2-2", value: 40 }
242
- ]
243
- }
244
- ]
299
+ expect(items4).toStrictEqual(both)
300
+ })
301
+ .pipe(setupRequestContextFromCurrent())
245
302
 
246
- const both = [
247
- {
248
- name: "Item 1",
249
- items: [
250
- { id: "1-1", value: 10 },
251
- { id: "1-2", value: 20 }
252
- ]
253
- },
254
- {
255
- name: "Item 2",
256
- items: [
257
- { id: "2-1", value: 30 },
258
- { id: "2-2", value: 40 }
259
- ]
260
- }
261
- ]
303
+ it("works well in CosmosDB", () =>
304
+ test
305
+ .pipe(Effect.provide(SomethingRepo.TestCosmos), rt.runPromise))
262
306
 
263
- expect(items).toStrictEqual(expected)
264
- expect(itemsExists).toStrictEqual(expected)
265
- expect(items2).toStrictEqual(expected)
266
- expect(items2Or).toStrictEqual(both)
267
- expect(items3).toStrictEqual(expected)
268
- expect(items3Or).toStrictEqual(both)
269
- expect(items4).toStrictEqual(both)
307
+ it("works well in Memory", () =>
308
+ test
309
+ .pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
310
+ })
311
+
312
+ describe("multi-level", () => {
313
+ const test = Effect
314
+ .gen(function*() {
315
+ const repo = yield* SomethingRepo
316
+ const itemsCheckWithEvery = yield* repo.query(
317
+ whereEvery(
318
+ "items",
319
+ flow(
320
+ where("value", "gt", 20),
321
+ and("description", "contains", "d item")
322
+ )
323
+ ),
324
+ project(projected)
325
+ )
326
+
327
+ expect(itemsCheckWithEvery).toStrictEqual([])
270
328
  })
271
329
  .pipe(setupRequestContextFromCurrent())
272
330