@reventlessdev/reventless-postgres 3.0.0-alpha.13 → 3.0.0-alpha.131

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 (143) hide show
  1. package/CHANGELOG.md +949 -0
  2. package/README.md +83 -0
  3. package/lib/bs/.compiler.log +2 -2
  4. package/lib/bs/ReventlessPostgres.cmt +0 -0
  5. package/lib/bs/ReventlessPostgres.mlmap +0 -2
  6. package/lib/bs/compiler-info.json +2 -2
  7. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  8. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  9. package/lib/bs/src/DcbEventLogStorage_Postgres.ast +0 -0
  10. package/lib/bs/src/DcbEventLogStorage_Postgres.res +1 -1
  11. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  12. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  13. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops.ast +0 -0
  14. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops.res +43 -42
  15. package/lib/bs/src/DcbEventLogStorage_Postgres_Ops.res.mjs +4 -4
  16. package/lib/bs/src/EventLogChangeFeed-ReventlessPostgres.cmi +0 -0
  17. package/lib/bs/src/EventLogChangeFeed-ReventlessPostgres.cmt +0 -0
  18. package/lib/bs/src/EventLogChangeFeed.ast +0 -0
  19. package/lib/bs/src/EventLogChangeFeed.res +15 -6
  20. package/lib/bs/src/EventLogChangeFeed.res.mjs +3 -3
  21. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  22. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  23. package/lib/bs/src/EventLogStorage_Postgres.ast +0 -0
  24. package/lib/bs/src/EventLogStorage_Postgres.res +1 -1
  25. package/lib/bs/src/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  26. package/lib/bs/src/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  27. package/lib/bs/src/EventLogStorage_Postgres_Ops.ast +0 -0
  28. package/lib/bs/src/EventLogStorage_Postgres_Ops.res +24 -14
  29. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmi +0 -0
  30. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmt +0 -0
  31. package/lib/bs/src/PgChangeFeed.ast +0 -0
  32. package/lib/bs/src/PgChangeFeed.res +21 -9
  33. package/lib/bs/src/PgChangeFeed.res.mjs +6 -6
  34. package/lib/bs/src/PgDriver-ReventlessPostgres.cmi +0 -0
  35. package/lib/bs/src/PgDriver-ReventlessPostgres.cmt +0 -0
  36. package/lib/bs/src/PgDriver-ReventlessPostgres.cmti +0 -0
  37. package/lib/bs/src/PgDriver.ast +0 -0
  38. package/lib/bs/src/PgDriver.iast +0 -0
  39. package/lib/bs/src/PgDriver.res +1 -2
  40. package/lib/bs/src/PgSchema-ReventlessPostgres.cmi +0 -0
  41. package/lib/bs/src/PgSchema-ReventlessPostgres.cmt +0 -0
  42. package/lib/bs/src/PgSchema.ast +0 -0
  43. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
  44. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
  45. package/lib/bs/src/QueryDbStorage_Postgres.ast +0 -0
  46. package/lib/bs/src/QueryDbStorage_Postgres.res +7 -2
  47. package/lib/bs/src/QueryDbStorage_Postgres.res.mjs +1 -1
  48. package/lib/bs/src/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  49. package/lib/bs/src/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  50. package/lib/bs/src/QueryDbStorage_Postgres_Ops.ast +0 -0
  51. package/lib/bs/src/QueryDbStorage_Postgres_Ops.res +14 -14
  52. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
  53. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
  54. package/lib/bs/src/QueryEnginePostgres.ast +0 -0
  55. package/lib/bs/src/QueryEnginePostgres.res +141 -42
  56. package/lib/bs/src/QueryEnginePostgres.res.mjs +53 -30
  57. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
  58. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
  59. package/lib/bs/tests/PostgresIntegrationTest.ast +0 -0
  60. package/lib/bs/tests/PostgresIntegrationTest.res +157 -104
  61. package/lib/bs/tests/QueryEnginePostgresListPageTest-ReventlessPostgres.cmi +0 -0
  62. package/lib/bs/tests/QueryEnginePostgresListPageTest-ReventlessPostgres.cmt +0 -0
  63. package/lib/bs/tests/QueryEnginePostgresListPageTest.ast +0 -0
  64. package/lib/bs/tests/QueryEnginePostgresListPageTest.res +29 -6
  65. package/lib/bs/tests/QueryEnginePostgresListPageTest.res.mjs +4 -4
  66. package/lib/ocaml/.compiler.log +2 -2
  67. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  68. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  69. package/lib/ocaml/DcbEventLogStorage_Postgres.ast +0 -0
  70. package/lib/ocaml/DcbEventLogStorage_Postgres.res +1 -1
  71. package/lib/ocaml/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  72. package/lib/ocaml/DcbEventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  73. package/lib/ocaml/DcbEventLogStorage_Postgres_Ops.ast +0 -0
  74. package/lib/ocaml/DcbEventLogStorage_Postgres_Ops.res +43 -42
  75. package/lib/ocaml/EventLogChangeFeed-ReventlessPostgres.cmi +0 -0
  76. package/lib/ocaml/EventLogChangeFeed-ReventlessPostgres.cmt +0 -0
  77. package/lib/ocaml/EventLogChangeFeed.ast +0 -0
  78. package/lib/ocaml/EventLogChangeFeed.res +15 -6
  79. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  80. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  81. package/lib/ocaml/EventLogStorage_Postgres.ast +0 -0
  82. package/lib/ocaml/EventLogStorage_Postgres.res +1 -1
  83. package/lib/ocaml/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  84. package/lib/ocaml/EventLogStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  85. package/lib/ocaml/EventLogStorage_Postgres_Ops.ast +0 -0
  86. package/lib/ocaml/EventLogStorage_Postgres_Ops.res +24 -14
  87. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmi +0 -0
  88. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmt +0 -0
  89. package/lib/ocaml/PgChangeFeed.ast +0 -0
  90. package/lib/ocaml/PgChangeFeed.res +21 -9
  91. package/lib/ocaml/PgDriver-ReventlessPostgres.cmi +0 -0
  92. package/lib/ocaml/PgDriver-ReventlessPostgres.cmt +0 -0
  93. package/lib/ocaml/PgDriver-ReventlessPostgres.cmti +0 -0
  94. package/lib/ocaml/PgDriver.ast +0 -0
  95. package/lib/ocaml/PgDriver.iast +0 -0
  96. package/lib/ocaml/PgDriver.res +1 -2
  97. package/lib/ocaml/PgSchema-ReventlessPostgres.cmi +0 -0
  98. package/lib/ocaml/PgSchema-ReventlessPostgres.cmt +0 -0
  99. package/lib/ocaml/PgSchema.ast +0 -0
  100. package/lib/ocaml/PostgresIntegrationTest.res +157 -104
  101. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
  102. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
  103. package/lib/ocaml/QueryDbStorage_Postgres.ast +0 -0
  104. package/lib/ocaml/QueryDbStorage_Postgres.res +7 -2
  105. package/lib/ocaml/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmi +0 -0
  106. package/lib/ocaml/QueryDbStorage_Postgres_Ops-ReventlessPostgres.cmt +0 -0
  107. package/lib/ocaml/QueryDbStorage_Postgres_Ops.ast +0 -0
  108. package/lib/ocaml/QueryDbStorage_Postgres_Ops.res +14 -14
  109. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
  110. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
  111. package/lib/ocaml/QueryEnginePostgres.ast +0 -0
  112. package/lib/ocaml/QueryEnginePostgres.res +141 -42
  113. package/lib/ocaml/QueryEnginePostgresListPageTest.res +29 -6
  114. package/lib/ocaml/ReventlessPostgres.cmt +0 -0
  115. package/package.json +13 -13
  116. package/rescript.json +2 -1
  117. package/src/DcbEventLogStorage_Postgres.res +1 -1
  118. package/src/DcbEventLogStorage_Postgres_Ops.res +43 -42
  119. package/src/DcbEventLogStorage_Postgres_Ops.res.mjs +4 -4
  120. package/src/EventLogChangeFeed.res +15 -6
  121. package/src/EventLogChangeFeed.res.mjs +3 -3
  122. package/src/EventLogStorage_Postgres.res +1 -1
  123. package/src/EventLogStorage_Postgres_Ops.res +24 -14
  124. package/src/PgChangeFeed.res +21 -9
  125. package/src/PgChangeFeed.res.mjs +6 -6
  126. package/src/PgDriver.res +1 -2
  127. package/src/QueryDbStorage_Postgres.res +7 -2
  128. package/src/QueryDbStorage_Postgres.res.mjs +1 -1
  129. package/src/QueryDbStorage_Postgres_Ops.res +14 -14
  130. package/src/QueryEnginePostgres.res +141 -42
  131. package/src/QueryEnginePostgres.res.mjs +53 -30
  132. package/tests/PostgresIntegrationTest.res +157 -104
  133. package/tests/QueryEnginePostgresListPageTest.res +29 -6
  134. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
  135. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmj +0 -0
  136. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
  137. package/lib/ocaml/PostgresIntegrationTest.ast +0 -0
  138. package/lib/ocaml/QueryEnginePostgresListPageTest-ReventlessPostgres.cmi +0 -0
  139. package/lib/ocaml/QueryEnginePostgresListPageTest-ReventlessPostgres.cmj +0 -0
  140. package/lib/ocaml/QueryEnginePostgresListPageTest-ReventlessPostgres.cmt +0 -0
  141. package/lib/ocaml/QueryEnginePostgresListPageTest.ast +0 -0
  142. package/tests/PostgresIntegrationTest.res.mjs +0 -377
  143. package/tests/QueryEnginePostgresListPageTest.res.mjs +0 -461
@@ -28,7 +28,12 @@ let param = (b: paramBuilder, v: JSON.t): string => {
28
28
  let col = (field: string): string => `(item->>'${field->String.replaceAll("'", "''")}')`
29
29
 
30
30
  // One Filter.config → SQL predicate, appending any bound params.
31
- let filterSql = (b: paramBuilder, field: string, cmp: QueryEngine.Filter.comparator, v: QueryEngine.value): string => {
31
+ let filterSql = (
32
+ b: paramBuilder,
33
+ field: string,
34
+ cmp: QueryEngine.Filter.comparator,
35
+ v: QueryEngine.value,
36
+ ): string => {
32
37
  let c = col(field)
33
38
  let bind = () => b->param(JSON.Encode.string(valueToText(v)))
34
39
  let cmpSql = op =>
@@ -48,10 +53,16 @@ let filterSql = (b: paramBuilder, field: string, cmp: QueryEngine.Filter.compara
48
53
  }
49
54
  }
50
55
 
51
- let subIdSql = (b: paramBuilder, field: string, cmp: QueryEngine.SubId.comparator, v: QueryEngine.value): string => {
56
+ let subIdSql = (
57
+ b: paramBuilder,
58
+ field: string,
59
+ cmp: QueryEngine.SubId.comparator,
60
+ v: QueryEngine.value,
61
+ ): string => {
52
62
  let c = col(field)
53
63
  let bind = () => b->param(JSON.Encode.string(valueToText(v)))
54
- let cmpSql = op => isNumeric(v) ? `${c}::numeric ${op} ${bind()}::numeric` : `${c} ${op} ${bind()}`
64
+ let cmpSql = op =>
65
+ isNumeric(v) ? `${c}::numeric ${op} ${bind()}::numeric` : `${c} ${op} ${bind()}`
55
66
  switch cmp {
56
67
  | QueryEngine.SubId.Equal => cmpSql("=")
57
68
  | Unequal => cmpSql("<>")
@@ -63,24 +74,28 @@ let subIdSql = (b: paramBuilder, field: string, cmp: QueryEngine.SubId.comparato
63
74
  }
64
75
  }
65
76
 
66
- let notExpired = QueryDbStorage_Postgres.notExpiredClause
77
+ let notExpired = QueryDbStorage_Postgres_Ops.notExpiredClause
67
78
 
68
79
  // Add `id` (from partition_key) when absent — matches allRows/scan in the SQLite
69
80
  // engine; partition-key queries return items as stored.
70
81
  let decodeRow = (~withId, row: dict<JSON.t>): JSON.t => {
71
- let item = QueryDbStorage_Postgres.decodeItem(row)
82
+ let item = QueryDbStorage_Postgres_Ops.decodeItem(row)
72
83
  if withId {
73
84
  let pk = switch row->Dict.get("partition_key") {
74
85
  | Some(JSON.String(s)) => s
75
86
  | _ => ""
76
87
  }
77
- QueryDbStorage_Postgres.withId(item, pk)
88
+ QueryDbStorage_Postgres_Ops.withId(item, pk)
78
89
  } else {
79
90
  item
80
91
  }
81
92
  }
82
93
 
83
- module Make = (P: {let pool: PgDriver.pool}) => {
94
+ module Make = (
95
+ P: {
96
+ let pool: PgDriver.pool
97
+ },
98
+ ) => {
84
99
  let query = async (
85
100
  ~readModelName,
86
101
  ~key=?,
@@ -90,7 +105,7 @@ module Make = (P: {let pool: PgDriver.pool}) => {
90
105
  ~ascending=?,
91
106
  ~limit=?,
92
107
  ): array<JSON.t> => {
93
- let table = QueryDbStorage_Postgres.tableName(readModelName)
108
+ let table = QueryDbStorage_Postgres_Ops.tableName(readModelName)
94
109
  let keyStr = switch key {
95
110
  | Some(k) => k
96
111
  | None => valueToText(id)
@@ -109,16 +124,22 @@ module Make = (P: {let pool: PgDriver.pool}) => {
109
124
  | Some(n) => ` LIMIT ${b->param(JSON.Encode.int(n))}`
110
125
  | None => ""
111
126
  }
112
- let sql = `SELECT partition_key, item FROM ${table} WHERE ${where->Array.join(" AND ")} ORDER BY sub_key ${dir}${limitClause}`
127
+ let sql = `SELECT partition_key, item FROM ${table} WHERE ${where->Array.join(
128
+ " AND ",
129
+ )} ORDER BY sub_key ${dir}${limitClause}`
113
130
  (await P.pool->PgDriver.query(sql, b.params))->Array.map(decodeRow(~withId=false, ...))
114
131
  }
115
132
 
116
133
  let scan = async (~readModelName, ~filterConfigs, ~limit): array<JSON.t> => {
117
- let table = QueryDbStorage_Postgres.tableName(readModelName)
134
+ let table = QueryDbStorage_Postgres_Ops.tableName(readModelName)
118
135
  let b = {params: []}
119
136
  let where = [notExpired]
120
- filterConfigs->Array.forEach(((field, cmp, v)) => where->Array.push(filterSql(b, field, cmp, v)))
121
- let sql = `SELECT partition_key, item FROM ${table} WHERE ${where->Array.join(" AND ")} ORDER BY partition_key, sub_key LIMIT ${b->param(JSON.Encode.int(limit))}`
137
+ filterConfigs->Array.forEach(((field, cmp, v)) =>
138
+ where->Array.push(filterSql(b, field, cmp, v))
139
+ )
140
+ let sql = `SELECT partition_key, item FROM ${table} WHERE ${where->Array.join(
141
+ " AND ",
142
+ )} ORDER BY partition_key, sub_key LIMIT ${b->param(JSON.Encode.int(limit))}`
122
143
  (await P.pool->PgDriver.query(sql, b.params))->Array.map(decodeRow(~withId=true, ...))
123
144
  }
124
145
 
@@ -138,7 +159,7 @@ module Make = (P: {let pool: PgDriver.pool}) => {
138
159
  // (`ensureIndexes`) instead of scanning. String comparison mirrors the
139
160
  // resolver's JS path (which only matched string-typed fields).
140
161
  let indexLookup = async (~readModelName, field: string, value: string): array<JSON.t> => {
141
- let table = QueryDbStorage_Postgres.tableName(readModelName)
162
+ let table = QueryDbStorage_Postgres_Ops.tableName(readModelName)
142
163
  let b = {params: []}
143
164
  let sql = `SELECT partition_key, item FROM ${table} WHERE ${col(field)} = ${b->param(
144
165
  JSON.Encode.string(value),
@@ -154,7 +175,7 @@ module Make = (P: {let pool: PgDriver.pool}) => {
154
175
  if ids->Array.length == 0 {
155
176
  []
156
177
  } else {
157
- let table = QueryDbStorage_Postgres.tableName(readModelName)
178
+ let table = QueryDbStorage_Postgres_Ops.tableName(readModelName)
158
179
  let b = {params: []}
159
180
  let idsJson = JSON.Encode.array(ids->Array.map(JSON.Encode.string))
160
181
  let sql = `SELECT partition_key, item FROM ${table} WHERE partition_key = ANY(${b->param(
@@ -168,6 +189,39 @@ module Make = (P: {let pool: PgDriver.pool}) => {
168
189
  let idExprC = `COALESCE(item->>'id', partition_key) COLLATE "C"`
169
190
  let jsonTextC = (field: string): string => `${col(field)} COLLATE "C"`
170
191
 
192
+ // "not retired", as jsonb rather than text. `->` keeps the value typed, so a
193
+ // JSON boolean true is distinguishable from the JSON string "true"; `->>`
194
+ // would render both as 'true' and hide a row the shared spec keeps, since that
195
+ // spec decodes strictly and reads a non-boolean as "no flag". `IS DISTINCT
196
+ // FROM` rather than `<>` so an absent key — NULL — keeps the row instead of
197
+ // making the whole predicate NULL and dropping it.
198
+ //
199
+ // The state form compares against JSON strings instead of `true`, one
200
+ // `IS DISTINCT FROM` per state, ANDed. Not `NOT IN`: that is ordinary
201
+ // equality, so an absent key makes the predicate NULL and drops a row this
202
+ // spec keeps. Same shape and same index behaviour as the single-state form —
203
+ // a handful of equality predicates over an enum-valued attribute cost what one
204
+ // over a boolean does — so the `@index` guidance carries over unchanged.
205
+ //
206
+ // A set naming no states withdraws nothing, so the predicate is vacuously
207
+ // true, matching what `isRetiredValue` answers for it.
208
+ let notRetiredC = (scope: Reventless.OwnerScope.retiredScope): string => {
209
+ let key = scope.field->String.replaceAll("'", "''")
210
+ let distinctFrom = lit => `(item->'${key}') IS DISTINCT FROM ${lit}`
211
+ switch scope.values {
212
+ | None => distinctFrom("'true'::jsonb")
213
+ | Some([]) => "TRUE"
214
+ | Some(states) =>
215
+ // Parenthesised though every caller ANDs these together already: a clause
216
+ // that is safe only because of where it is spliced is a clause that breaks
217
+ // the first time it is spliced somewhere else.
218
+ "(" ++
219
+ states
220
+ ->Array.map(state => distinctFrom(`'"${state->String.replaceAll("'", "''")}"'::jsonb`))
221
+ ->Array.join(" AND ") ++ ")"
222
+ }
223
+ }
224
+
171
225
  // Connection-list push-down. Builds `item->>'field'` predicates + keyset
172
226
  // WHERE + `ORDER BY … LIMIT pageSize+1` so a page reads only what it returns
173
227
  // rather than the whole read model. Reproduces `QueryDbListQuery` semantics
@@ -183,8 +237,10 @@ module Make = (P: {let pool: PgDriver.pool}) => {
183
237
  ~argsDict: dict<JSON.t>,
184
238
  ~capability: GraphQL_FragmentGenerator.serverCapability,
185
239
  ~labelField as _,
240
+ ~ownerScope: option<(string, string)>=?,
241
+ ~retiredScope: option<Reventless.OwnerScope.retiredScope>=?,
186
242
  ): option<JSON.t> => {
187
- let table = QueryDbStorage_Postgres.tableName(readModelName)
243
+ let table = QueryDbStorage_Postgres_Ops.tableName(readModelName)
188
244
  let filterDict =
189
245
  argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
190
246
  let strNonEmpty = k =>
@@ -210,6 +266,19 @@ module Make = (P: {let pool: PgDriver.pool}) => {
210
266
  } else {
211
267
  let b = {params: []}
212
268
  let whereParts = [notExpired]
269
+ // Into the SQL, before the LIMIT — narrowing the returned page instead
270
+ // would hand back fewer rows than asked for while still claiming a next
271
+ // page. Mirrors the SQLite push-down; the parity harness holds the two
272
+ // and the shared spec to the same answer.
273
+ switch ownerScope {
274
+ | Some((field, required)) =>
275
+ whereParts->Array.push(`${jsonTextC(field)} = ${b->param(JSON.Encode.string(required))}`)
276
+ | None => ()
277
+ }
278
+ switch retiredScope {
279
+ | Some(scope) => whereParts->Array.push(notRetiredC(scope))
280
+ | None => ()
281
+ }
213
282
  let valString = v =>
214
283
  switch v->JSON.Decode.string {
215
284
  | Some(s) => Some(s)
@@ -217,15 +286,17 @@ module Make = (P: {let pool: PgDriver.pool}) => {
217
286
  }
218
287
  capability.filterFields->Array.forEach(f => {
219
288
  switch filterDict->Dict.get(f.name ++ "Eq") {
220
- | Some(v) when v != JSON.Encode.null =>
289
+ | Some(v) if v != JSON.Encode.null =>
221
290
  whereParts->Array.push(
222
- `${jsonTextC(f.name)} = ${b->param(JSON.Encode.string(valString(v)->Option.getOr("")))}`,
291
+ `${jsonTextC(f.name)} = ${b->param(
292
+ JSON.Encode.string(valString(v)->Option.getOr("")),
293
+ )}`,
223
294
  )
224
295
  | _ => ()
225
296
  }
226
297
  if f.range {
227
298
  switch filterDict->Dict.get(f.name ++ "From") {
228
- | Some(v) when v != JSON.Encode.null =>
299
+ | Some(v) if v != JSON.Encode.null =>
229
300
  whereParts->Array.push(
230
301
  `${jsonTextC(f.name)} >= ${b->param(
231
302
  JSON.Encode.string(valString(v)->Option.getOr("")),
@@ -234,7 +305,7 @@ module Make = (P: {let pool: PgDriver.pool}) => {
234
305
  | _ => ()
235
306
  }
236
307
  switch filterDict->Dict.get(f.name ++ "To") {
237
- | Some(v) when v != JSON.Encode.null =>
308
+ | Some(v) if v != JSON.Encode.null =>
238
309
  whereParts->Array.push(
239
310
  `${jsonTextC(f.name)} <= ${b->param(
240
311
  JSON.Encode.string(valString(v)->Option.getOr("")),
@@ -256,7 +327,8 @@ module Make = (P: {let pool: PgDriver.pool}) => {
256
327
  | Some(f) => jsonTextC(f)
257
328
  | None => idExprC
258
329
  }
259
- switch argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string) {
330
+ let afterCursor = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
331
+ switch afterCursor {
260
332
  | Some(c) =>
261
333
  whereParts->Array.push(
262
334
  `${cursorExpr} ${isDesc ? "<" : ">"} ${b->param(
@@ -278,7 +350,8 @@ module Make = (P: {let pool: PgDriver.pool}) => {
278
350
  let sql = `SELECT partition_key, item FROM ${table} WHERE ${whereParts->Array.join(
279
351
  " AND ",
280
352
  )} ORDER BY ${orderClause} LIMIT ${b->param(JSON.Encode.int(pageSize + 1))}`
281
- let rows = (await P.pool->PgDriver.query(sql, b.params))->Array.map(decodeRow(~withId=true, ...))
353
+ let rows =
354
+ (await P.pool->PgDriver.query(sql, b.params))->Array.map(decodeRow(~withId=true, ...))
282
355
  let hasMore = rows->Array.length > pageSize
283
356
  let pageItems = rows->Array.slice(~start=0, ~end=pageSize)
284
357
  let cursorField = orderByField->Option.getOr("id")
@@ -290,7 +363,10 @@ module Make = (P: {let pool: PgDriver.pool}) => {
290
363
  ReventlessCore.QueryDbListQuery.buildConnection(
291
364
  ~pageItems,
292
365
  ~hasNextPage=hasMore,
293
- ~hasPreviousPage=false,
366
+ // Same rule the spec uses, because the parity suite compares them: a
367
+ // forward page reached by a cursor has one behind it. Hardcoding false
368
+ // made Prev disappear on exactly the pages that have a previous one.
369
+ ~hasPreviousPage=afterCursor->Option.isSome,
294
370
  ~cursorValueOf,
295
371
  ),
296
372
  )
@@ -309,14 +385,18 @@ module Make = (P: {let pool: PgDriver.pool}) => {
309
385
  ~subIdField as _: string,
310
386
  ~id: string,
311
387
  ~argsDict: dict<JSON.t>,
388
+ ~ownerScope: option<(string, string)>=?,
389
+ ~retiredScope: option<Reventless.OwnerScope.retiredScope>=?,
312
390
  ): JSON.t => {
313
- let table = QueryDbStorage_Postgres.tableName(readModelName)
391
+ let table = QueryDbStorage_Postgres_Ops.tableName(readModelName)
314
392
  let filterDict =
315
393
  argsDict->Dict.get("filter")->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
316
394
  let fstr = k => filterDict->Dict.get(k)->Option.flatMap(JSON.Decode.string)
317
395
  let orderDesc = fstr("order")->Option.mapOr(false, o => o == "DESC")
318
- let last = argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
319
- let first = argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
396
+ let last =
397
+ argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
398
+ let first =
399
+ argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
320
400
  let after = argsDict->Dict.get("after")->Option.flatMap(JSON.Decode.string)
321
401
  let before = argsDict->Dict.get("before")->Option.flatMap(JSON.Decode.string)
322
402
  let isBackward = last->Option.isSome
@@ -324,28 +404,49 @@ module Make = (P: {let pool: PgDriver.pool}) => {
324
404
  let sk = `sub_key COLLATE "C"`
325
405
  let b = {params: []}
326
406
  let where = [`partition_key = ${b->param(JSON.Encode.string(id))}`, notExpired]
407
+ // Before the keyset window and the LIMIT, for the same reason as `listPage`.
408
+ switch ownerScope {
409
+ | Some((field, required)) =>
410
+ where->Array.push(`${jsonTextC(field)} = ${b->param(JSON.Encode.string(required))}`)
411
+ | None => ()
412
+ }
413
+ switch retiredScope {
414
+ | Some(scope) => where->Array.push(notRetiredC(scope))
415
+ | None => ()
416
+ }
327
417
  fstr("prefix")->Option.forEach(p =>
328
418
  where->Array.push(`starts_with(sub_key, ${b->param(JSON.Encode.string(p))})`)
329
419
  )
330
- fstr("from")->Option.forEach(f => where->Array.push(`${sk} >= ${b->param(JSON.Encode.string(f))}`))
331
- fstr("to")->Option.forEach(t => where->Array.push(`${sk} <= ${b->param(JSON.Encode.string(t))}`))
420
+ fstr("from")->Option.forEach(f =>
421
+ where->Array.push(`${sk} >= ${b->param(JSON.Encode.string(f))}`)
422
+ )
423
+ fstr("to")->Option.forEach(t =>
424
+ where->Array.push(`${sk} <= ${b->param(JSON.Encode.string(t))}`)
425
+ )
332
426
  fstr("eq")->Option.forEach(e => where->Array.push(`${sk} = ${b->param(JSON.Encode.string(e))}`))
333
427
  // Keyset: forward excludes ≤ after; backward excludes ≥ before.
334
428
  switch (isBackward, after, before) {
335
429
  | (false, Some(c), _) =>
336
- where->Array.push(`${sk} > ${b->param(JSON.Encode.string(ReventlessCore.QueryDbListQuery.decodeCursor(c)))}`)
430
+ where->Array.push(
431
+ `${sk} > ${b->param(JSON.Encode.string(ReventlessCore.QueryDbListQuery.decodeCursor(c)))}`,
432
+ )
337
433
  | (true, _, Some(c)) =>
338
- where->Array.push(`${sk} < ${b->param(JSON.Encode.string(ReventlessCore.QueryDbListQuery.decodeCursor(c)))}`)
434
+ where->Array.push(
435
+ `${sk} < ${b->param(JSON.Encode.string(ReventlessCore.QueryDbListQuery.decodeCursor(c)))}`,
436
+ )
339
437
  | _ => ()
340
438
  }
341
439
  // Page-fetch direction is the LOGICAL order forward, and its OPPOSITE
342
440
  // backward (take the N nearest `before`, then reverse to logical) — mirrors
343
441
  // the resolver's `reverse = isBackward ? !orderDesc : orderDesc`.
344
442
  let fetchDesc = isBackward ? !orderDesc : orderDesc
345
- let pageSize = (isBackward ? last : first)->Option.getOr(ReventlessCore.QueryDbListQuery.defaultListPageSize)
443
+ let pageSize =
444
+ (isBackward ? last : first)->Option.getOr(ReventlessCore.QueryDbListQuery.defaultListPageSize)
346
445
  let sql = `SELECT partition_key, sub_key, item FROM ${table} WHERE ${where->Array.join(
347
446
  " AND ",
348
- )} ORDER BY ${sk} ${fetchDesc ? "DESC" : "ASC"} LIMIT ${b->param(JSON.Encode.int(pageSize + 1))}`
447
+ )} ORDER BY ${sk} ${fetchDesc ? "DESC" : "ASC"} LIMIT ${b->param(
448
+ JSON.Encode.int(pageSize + 1),
449
+ )}`
349
450
  let rows = await P.pool->PgDriver.query(sql, b.params)
350
451
  let hasMore = rows->Array.length > pageSize
351
452
  let taken = rows->Array.slice(~start=0, ~end=pageSize)
@@ -359,17 +460,18 @@ module Make = (P: {let pool: PgDriver.pool}) => {
359
460
  | _ => ""
360
461
  }
361
462
  let cursors = logical->Array.map(subKeyOf)
362
- let edges =
363
- pageItems->Array.mapWithIndex((item, i) =>
364
- Obj.magic({
365
- "node": item,
366
- "cursor": ReventlessCore.QueryDbListQuery.encodeCursor(cursors->Array.getUnsafe(i)),
367
- })
368
- )
463
+ let edges = pageItems->Array.mapWithIndex((item, i) =>
464
+ Obj.magic({
465
+ "node": item,
466
+ "cursor": ReventlessCore.QueryDbListQuery.encodeCursor(cursors->Array.getUnsafe(i)),
467
+ })
468
+ )
369
469
  let startCursor =
370
470
  cursors->Array.get(0)->Option.map(ReventlessCore.QueryDbListQuery.encodeCursor)
371
471
  let endCursor =
372
- cursors->Array.get(cursors->Array.length - 1)->Option.map(ReventlessCore.QueryDbListQuery.encodeCursor)
472
+ cursors
473
+ ->Array.get(cursors->Array.length - 1)
474
+ ->Option.map(ReventlessCore.QueryDbListQuery.encodeCursor)
373
475
  Obj.magic({
374
476
  "edges": edges,
375
477
  "pageInfo": {
@@ -380,7 +482,4 @@ module Make = (P: {let pool: PgDriver.pool}) => {
380
482
  },
381
483
  })
382
484
  }
383
-
384
- let make: QueryDb_Adapter.queryEngineMaker = _allQueryDbs =>
385
- Pulumi.Output.make({QueryEngine.scan, query})
386
485
  }
@@ -2,11 +2,10 @@
2
2
 
3
3
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
- import * as Pulumi from "@pulumi/pulumi";
6
5
  import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
7
6
  import * as PgDriver$ReventlessPostgres from "./PgDriver.res.mjs";
8
7
  import * as QueryDbListQuery$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/QueryDbListQuery.res.mjs";
9
- import * as QueryDbStorage_Postgres$ReventlessPostgres from "./QueryDbStorage_Postgres.res.mjs";
8
+ import * as QueryDbStorage_Postgres_Ops$ReventlessPostgres from "./QueryDbStorage_Postgres_Ops.res.mjs";
10
9
 
11
10
  function valueToText(v) {
12
11
  switch (v.TAG) {
@@ -105,25 +104,25 @@ function subIdSql(b, field, cmp, v) {
105
104
  }
106
105
 
107
106
  function decodeRow(withId, row) {
108
- let item = QueryDbStorage_Postgres$ReventlessPostgres.decodeItem(row);
107
+ let item = QueryDbStorage_Postgres_Ops$ReventlessPostgres.decodeItem(row);
109
108
  if (!withId) {
110
109
  return item;
111
110
  }
112
111
  let match = row["partition_key"];
113
112
  let pk = typeof match === "string" ? match : "";
114
- return QueryDbStorage_Postgres$ReventlessPostgres.withId(item, pk);
113
+ return QueryDbStorage_Postgres_Ops$ReventlessPostgres.withId(item, pk);
115
114
  }
116
115
 
117
116
  function Make(P) {
118
117
  let query = async (readModelName, key, id, subIdConfig, filterConfigs, ascending, limit) => {
119
- let table = QueryDbStorage_Postgres$ReventlessPostgres.tableName(readModelName);
118
+ let table = QueryDbStorage_Postgres_Ops$ReventlessPostgres.tableName(readModelName);
120
119
  let keyStr = key !== undefined ? key : valueToText(id);
121
120
  let b = {
122
121
  params: []
123
122
  };
124
123
  let where = [
125
124
  `partition_key = ` + param(b, keyStr),
126
- QueryDbStorage_Postgres$ReventlessPostgres.notExpiredClause
125
+ QueryDbStorage_Postgres_Ops$ReventlessPostgres.notExpiredClause
127
126
  ];
128
127
  if (subIdConfig !== undefined) {
129
128
  where.push(subIdSql(b, subIdConfig[0], subIdConfig[1], subIdConfig[2]));
@@ -137,11 +136,11 @@ function Make(P) {
137
136
  return (await PgDriver$ReventlessPostgres.query(P.pool, sql, b.params)).map(extra => decodeRow(false, extra));
138
137
  };
139
138
  let scan = async (readModelName, filterConfigs, limit) => {
140
- let table = QueryDbStorage_Postgres$ReventlessPostgres.tableName(readModelName);
139
+ let table = QueryDbStorage_Postgres_Ops$ReventlessPostgres.tableName(readModelName);
141
140
  let b = {
142
141
  params: []
143
142
  };
144
- let where = [QueryDbStorage_Postgres$ReventlessPostgres.notExpiredClause];
143
+ let where = [QueryDbStorage_Postgres_Ops$ReventlessPostgres.notExpiredClause];
145
144
  filterConfigs.forEach(param => {
146
145
  where.push(filterSql(b, param[0], param[1], param[2]));
147
146
  });
@@ -149,29 +148,45 @@ function Make(P) {
149
148
  return (await PgDriver$ReventlessPostgres.query(P.pool, sql, b.params)).map(extra => decodeRow(true, extra));
150
149
  };
151
150
  let indexLookup = async (readModelName, field, value) => {
152
- let table = QueryDbStorage_Postgres$ReventlessPostgres.tableName(readModelName);
151
+ let table = QueryDbStorage_Postgres_Ops$ReventlessPostgres.tableName(readModelName);
153
152
  let b = {
154
153
  params: []
155
154
  };
156
- let sql = `SELECT partition_key, item FROM ` + table + ` WHERE ` + col(field) + ` = ` + param(b, value) + ` AND ` + QueryDbStorage_Postgres$ReventlessPostgres.notExpiredClause;
155
+ let sql = `SELECT partition_key, item FROM ` + table + ` WHERE ` + col(field) + ` = ` + param(b, value) + ` AND ` + QueryDbStorage_Postgres_Ops$ReventlessPostgres.notExpiredClause;
157
156
  return (await PgDriver$ReventlessPostgres.query(P.pool, sql, b.params)).map(extra => decodeRow(true, extra));
158
157
  };
159
158
  let byIds = async (readModelName, ids) => {
160
159
  if (ids.length === 0) {
161
160
  return [];
162
161
  }
163
- let table = QueryDbStorage_Postgres$ReventlessPostgres.tableName(readModelName);
162
+ let table = QueryDbStorage_Postgres_Ops$ReventlessPostgres.tableName(readModelName);
164
163
  let b = {
165
164
  params: []
166
165
  };
167
166
  let idsJson = ids.map(prim => prim);
168
- let sql = `SELECT partition_key, item FROM ` + table + ` WHERE partition_key = ANY(` + param(b, idsJson) + `::text[]) AND ` + QueryDbStorage_Postgres$ReventlessPostgres.notExpiredClause;
167
+ let sql = `SELECT partition_key, item FROM ` + table + ` WHERE partition_key = ANY(` + param(b, idsJson) + `::text[]) AND ` + QueryDbStorage_Postgres_Ops$ReventlessPostgres.notExpiredClause;
169
168
  return (await PgDriver$ReventlessPostgres.query(P.pool, sql, b.params)).map(extra => decodeRow(true, extra));
170
169
  };
171
170
  let idExprC = `COALESCE(item->>'id', partition_key) COLLATE "C"`;
172
171
  let jsonTextC = field => col(field) + ` COLLATE "C"`;
173
- let listPage = async (readModelName, argsDict, capability, param$1) => {
174
- let table = QueryDbStorage_Postgres$ReventlessPostgres.tableName(readModelName);
172
+ let notRetiredC = scope => {
173
+ let key = scope.field.replaceAll("'", "''");
174
+ let states = scope.values;
175
+ if (states !== undefined) {
176
+ if (states.length !== 0) {
177
+ return "(" + states.map(state => {
178
+ let lit = `'"` + state.replaceAll("'", "''") + `"'::jsonb`;
179
+ return `(item->'` + key + `') IS DISTINCT FROM ` + lit;
180
+ }).join(" AND ") + ")";
181
+ } else {
182
+ return "TRUE";
183
+ }
184
+ } else {
185
+ return `(item->'` + key + `') IS DISTINCT FROM ` + "'true'::jsonb";
186
+ }
187
+ };
188
+ let listPage = async (readModelName, argsDict, capability, param$1, ownerScope, retiredScope) => {
189
+ let table = QueryDbStorage_Postgres_Ops$ReventlessPostgres.tableName(readModelName);
175
190
  let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
176
191
  let strNonEmpty = k => Stdlib_Option.mapOr(Stdlib_Option.flatMap(filterDict[k], Stdlib_JSON.Decode.string), false, s => s.length > 0);
177
192
  let hasIds = Stdlib_Option.mapOr(Stdlib_Option.flatMap(filterDict["ids"], Stdlib_JSON.Decode.array), false, a => a.length !== 0);
@@ -183,7 +198,13 @@ function Make(P) {
183
198
  let b = {
184
199
  params: []
185
200
  };
186
- let whereParts = [QueryDbStorage_Postgres$ReventlessPostgres.notExpiredClause];
201
+ let whereParts = [QueryDbStorage_Postgres_Ops$ReventlessPostgres.notExpiredClause];
202
+ if (ownerScope !== undefined) {
203
+ whereParts.push(jsonTextC(ownerScope[0]) + ` = ` + param(b, ownerScope[1]));
204
+ }
205
+ if (retiredScope !== undefined) {
206
+ whereParts.push(notRetiredC(retiredScope));
207
+ }
187
208
  let valString = v => {
188
209
  let s = Stdlib_JSON.Decode.string(v);
189
210
  if (s !== undefined) {
@@ -214,11 +235,11 @@ function Make(P) {
214
235
  let orderByField = Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["field"]), Stdlib_JSON.Decode.string);
215
236
  let isDesc = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["direction"]), Stdlib_JSON.Decode.string), "ASC") === "DESC";
216
237
  let cursorExpr = orderByField !== undefined ? jsonTextC(orderByField) : idExprC;
217
- let c = Stdlib_Option.flatMap(argsDict["after"], Stdlib_JSON.Decode.string);
218
- if (c !== undefined) {
238
+ let afterCursor = Stdlib_Option.flatMap(argsDict["after"], Stdlib_JSON.Decode.string);
239
+ if (afterCursor !== undefined) {
219
240
  whereParts.push(cursorExpr + ` ` + (
220
241
  isDesc ? "<" : ">"
221
- ) + ` ` + param(b, QueryDbListQuery$ReventlessCore.decodeCursor(c)));
242
+ ) + ` ` + param(b, QueryDbListQuery$ReventlessCore.decodeCursor(afterCursor)));
222
243
  }
223
244
  let orderClause = orderByField !== undefined ? jsonTextC(orderByField) + ` ` + (
224
245
  isDesc ? "DESC" : "ASC"
@@ -230,10 +251,10 @@ function Make(P) {
230
251
  let pageItems = rows.slice(0, pageSize);
231
252
  let cursorField = Stdlib_Option.getOr(orderByField, "id");
232
253
  let cursorValueOf = item => Stdlib_Option.getOr(QueryDbListQuery$ReventlessCore.getFieldString(item, cursorField), QueryDbListQuery$ReventlessCore.getId(item));
233
- return QueryDbListQuery$ReventlessCore.buildConnection(pageItems, hasMore, false, cursorValueOf);
254
+ return QueryDbListQuery$ReventlessCore.buildConnection(pageItems, hasMore, Stdlib_Option.isSome(afterCursor), cursorValueOf);
234
255
  };
235
- let itemsPage = async (readModelName, param$1, id, argsDict) => {
236
- let table = QueryDbStorage_Postgres$ReventlessPostgres.tableName(readModelName);
256
+ let itemsPage = async (readModelName, param$1, id, argsDict, ownerScope, retiredScope) => {
257
+ let table = QueryDbStorage_Postgres_Ops$ReventlessPostgres.tableName(readModelName);
237
258
  let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
238
259
  let fstr = k => Stdlib_Option.flatMap(filterDict[k], Stdlib_JSON.Decode.string);
239
260
  let orderDesc = Stdlib_Option.mapOr(fstr("order"), false, o => o === "DESC");
@@ -248,8 +269,14 @@ function Make(P) {
248
269
  };
249
270
  let where = [
250
271
  `partition_key = ` + param(b, id),
251
- QueryDbStorage_Postgres$ReventlessPostgres.notExpiredClause
272
+ QueryDbStorage_Postgres_Ops$ReventlessPostgres.notExpiredClause
252
273
  ];
274
+ if (ownerScope !== undefined) {
275
+ where.push(jsonTextC(ownerScope[0]) + ` = ` + param(b, ownerScope[1]));
276
+ }
277
+ if (retiredScope !== undefined) {
278
+ where.push(notRetiredC(retiredScope));
279
+ }
253
280
  Stdlib_Option.forEach(fstr("prefix"), p => {
254
281
  where.push(`starts_with(sub_key, ` + param(b, p) + `)`);
255
282
  });
@@ -304,10 +331,6 @@ function Make(P) {
304
331
  }
305
332
  };
306
333
  };
307
- let make = _allQueryDbs => Pulumi.output({
308
- scan: scan,
309
- query: query
310
- });
311
334
  return {
312
335
  query: query,
313
336
  scan: scan,
@@ -315,13 +338,13 @@ function Make(P) {
315
338
  byIds: byIds,
316
339
  idExprC: idExprC,
317
340
  jsonTextC: jsonTextC,
341
+ notRetiredC: notRetiredC,
318
342
  listPage: listPage,
319
- itemsPage: itemsPage,
320
- make: make
343
+ itemsPage: itemsPage
321
344
  };
322
345
  }
323
346
 
324
- let notExpired = QueryDbStorage_Postgres$ReventlessPostgres.notExpiredClause;
347
+ let notExpired = QueryDbStorage_Postgres_Ops$ReventlessPostgres.notExpiredClause;
325
348
 
326
349
  export {
327
350
  valueToText,
@@ -334,4 +357,4 @@ export {
334
357
  decodeRow,
335
358
  Make,
336
359
  }
337
- /* @pulumi/pulumi Not a pure module */
360
+ /* PgDriver-ReventlessPostgres Not a pure module */