@owlmeans/postgres-resource 0.1.18-rc.41 → 0.1.18-rc.43

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 (171) hide show
  1. package/README.md +13 -13
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/postgres-resource/SKILL.md +2 -2
  4. package/build/consts.d.ts +9 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +6 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/declarations/types.d.ts +12 -0
  9. package/build/declarations/types.d.ts.map +1 -0
  10. package/build/declarations/types.js +2 -0
  11. package/build/declarations/types.js.map +1 -0
  12. package/build/declarations.d.ts +6 -9
  13. package/build/declarations.d.ts.map +1 -1
  14. package/build/declarations.js +25 -15
  15. package/build/declarations.js.map +1 -1
  16. package/build/errors.d.ts +0 -20
  17. package/build/errors.d.ts.map +1 -1
  18. package/build/errors.js +1 -111
  19. package/build/errors.js.map +1 -1
  20. package/build/index.d.ts +3 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +1 -0
  23. package/build/index.js.map +1 -1
  24. package/build/pg-error/types.d.ts +24 -0
  25. package/build/pg-error/types.d.ts.map +1 -0
  26. package/build/pg-error/types.js +2 -0
  27. package/build/pg-error/types.js.map +1 -0
  28. package/build/pg-error.d.ts +4 -0
  29. package/build/pg-error.d.ts.map +1 -0
  30. package/build/pg-error.js +100 -0
  31. package/build/pg-error.js.map +1 -0
  32. package/build/resource.d.ts +1 -1
  33. package/build/resource.d.ts.map +1 -1
  34. package/build/resource.js +15 -11
  35. package/build/resource.js.map +1 -1
  36. package/build/types.d.ts +6 -1
  37. package/build/types.d.ts.map +1 -1
  38. package/build/types.local.d.ts +14 -0
  39. package/build/types.local.d.ts.map +1 -0
  40. package/build/types.local.js +2 -0
  41. package/build/types.local.js.map +1 -0
  42. package/build/utils/consts.local.d.ts +35 -0
  43. package/build/utils/consts.local.d.ts.map +1 -0
  44. package/build/utils/consts.local.js +63 -0
  45. package/build/utils/consts.local.js.map +1 -0
  46. package/build/utils/criteria/types.d.ts +31 -0
  47. package/build/utils/criteria/types.d.ts.map +1 -0
  48. package/build/utils/criteria/types.js +2 -0
  49. package/build/utils/criteria/types.js.map +1 -0
  50. package/build/utils/criteria.d.ts +3 -27
  51. package/build/utils/criteria.d.ts.map +1 -1
  52. package/build/utils/criteria.js +88 -107
  53. package/build/utils/criteria.js.map +1 -1
  54. package/build/utils/diff/types.d.ts +40 -0
  55. package/build/utils/diff/types.d.ts.map +1 -0
  56. package/build/utils/diff/types.js +2 -0
  57. package/build/utils/diff/types.js.map +1 -0
  58. package/build/utils/diff.d.ts +5 -35
  59. package/build/utils/diff.d.ts.map +1 -1
  60. package/build/utils/diff.js +321 -343
  61. package/build/utils/diff.js.map +1 -1
  62. package/build/utils/index.d.ts +10 -0
  63. package/build/utils/index.d.ts.map +1 -1
  64. package/build/utils/index.js.map +1 -1
  65. package/build/utils/introspect/types.d.ts +9 -0
  66. package/build/utils/introspect/types.d.ts.map +1 -0
  67. package/build/utils/introspect/types.js +2 -0
  68. package/build/utils/introspect/types.js.map +1 -0
  69. package/build/utils/introspect.d.ts +3 -3
  70. package/build/utils/introspect.d.ts.map +1 -1
  71. package/build/utils/introspect.js +42 -67
  72. package/build/utils/introspect.js.map +1 -1
  73. package/build/utils/life-cycle/types.d.ts +37 -0
  74. package/build/utils/life-cycle/types.d.ts.map +1 -0
  75. package/build/utils/life-cycle/types.js +2 -0
  76. package/build/utils/life-cycle/types.js.map +1 -0
  77. package/build/utils/life-cycle.d.ts +3 -38
  78. package/build/utils/life-cycle.d.ts.map +1 -1
  79. package/build/utils/life-cycle.js +90 -107
  80. package/build/utils/life-cycle.js.map +1 -1
  81. package/build/utils/marshal/types.d.ts +39 -0
  82. package/build/utils/marshal/types.d.ts.map +1 -0
  83. package/build/utils/marshal/types.js +2 -0
  84. package/build/utils/marshal/types.js.map +1 -0
  85. package/build/utils/marshal.d.ts +4 -32
  86. package/build/utils/marshal.d.ts.map +1 -1
  87. package/build/utils/marshal.js +124 -141
  88. package/build/utils/marshal.js.map +1 -1
  89. package/build/utils/migrations.d.ts.map +1 -1
  90. package/build/utils/migrations.js +41 -36
  91. package/build/utils/migrations.js.map +1 -1
  92. package/build/utils/name/types.d.ts +43 -0
  93. package/build/utils/name/types.d.ts.map +1 -0
  94. package/build/utils/name/types.js +2 -0
  95. package/build/utils/name/types.js.map +1 -0
  96. package/build/utils/name.d.ts +6 -35
  97. package/build/utils/name.d.ts.map +1 -1
  98. package/build/utils/name.js +40 -59
  99. package/build/utils/name.js.map +1 -1
  100. package/build/utils/schema/types.d.ts +22 -0
  101. package/build/utils/schema/types.d.ts.map +1 -0
  102. package/build/utils/schema/types.js +2 -0
  103. package/build/utils/schema/types.js.map +1 -0
  104. package/build/utils/schema.d.ts +4 -24
  105. package/build/utils/schema.d.ts.map +1 -1
  106. package/build/utils/schema.js +341 -352
  107. package/build/utils/schema.js.map +1 -1
  108. package/build/utils/sql/types.d.ts +25 -0
  109. package/build/utils/sql/types.d.ts.map +1 -0
  110. package/build/utils/sql/types.js +2 -0
  111. package/build/utils/sql/types.js.map +1 -0
  112. package/build/utils/sql.d.ts +5 -27
  113. package/build/utils/sql.d.ts.map +1 -1
  114. package/build/utils/sql.js +0 -0
  115. package/build/utils/sql.js.map +1 -1
  116. package/build/utils/sync/types.d.ts +25 -0
  117. package/build/utils/sync/types.d.ts.map +1 -0
  118. package/build/utils/sync/types.js +2 -0
  119. package/build/utils/sync/types.js.map +1 -0
  120. package/build/utils/sync.d.ts +2 -19
  121. package/build/utils/sync.d.ts.map +1 -1
  122. package/build/utils/sync.js +67 -84
  123. package/build/utils/sync.js.map +1 -1
  124. package/build/utils/types.d.ts +18 -0
  125. package/build/utils/types.d.ts.map +1 -0
  126. package/build/utils/types.js +2 -0
  127. package/build/utils/types.js.map +1 -0
  128. package/build/utils/types.local.d.ts +33 -0
  129. package/build/utils/types.local.d.ts.map +1 -0
  130. package/build/utils/types.local.js +2 -0
  131. package/build/utils/types.local.js.map +1 -0
  132. package/package.json +14 -13
  133. package/src/consts.ts +7 -0
  134. package/src/declarations/types.ts +12 -0
  135. package/src/declarations.ts +30 -23
  136. package/src/errors.ts +1 -131
  137. package/src/index.ts +3 -0
  138. package/src/pg-error/types.ts +23 -0
  139. package/src/pg-error.ts +114 -0
  140. package/src/resource.ts +20 -24
  141. package/src/types.local.ts +16 -0
  142. package/src/types.ts +8 -4
  143. package/src/utils/consts.local.ts +71 -0
  144. package/src/utils/criteria/types.ts +32 -0
  145. package/src/utils/criteria.ts +99 -129
  146. package/src/utils/diff/types.ts +41 -0
  147. package/src/utils/diff.ts +331 -348
  148. package/src/utils/index.ts +10 -0
  149. package/src/utils/introspect/types.ts +9 -0
  150. package/src/utils/introspect.ts +54 -79
  151. package/src/utils/life-cycle/types.ts +41 -0
  152. package/src/utils/life-cycle.ts +108 -134
  153. package/src/utils/marshal/types.ts +40 -0
  154. package/src/utils/marshal.ts +134 -147
  155. package/src/utils/migrations.ts +39 -33
  156. package/src/utils/name/types.ts +44 -0
  157. package/src/utils/name.ts +49 -63
  158. package/src/utils/schema/types.ts +26 -0
  159. package/src/utils/schema.ts +366 -397
  160. package/src/utils/sql/types.ts +25 -0
  161. package/src/utils/sql.ts +0 -0
  162. package/src/utils/sync/types.ts +25 -0
  163. package/src/utils/sync.ts +65 -83
  164. package/src/utils/types.local.ts +35 -0
  165. package/src/utils/types.ts +19 -0
  166. package/tests/criteria.spec.ts +4 -1
  167. package/tests/diff.spec.ts +28 -1
  168. package/tests/errors.spec.ts +4 -3
  169. package/tests/name.spec.ts +5 -3
  170. package/tests/schema.spec.ts +3 -1
  171. package/tests/sql.spec.ts +20 -22
@@ -1,13 +1,10 @@
1
- import { UnsupportedArgumentError } from '@owlmeans/resource'
2
- import type { Criteria, FieldOperators, Sort } from '@owlmeans/resource'
3
- import { and, asc, desc, or, param, sql } from 'drizzle-orm'
4
- import type { SQL } from 'drizzle-orm'
1
+ import { UnsupportedArgumentError, type Criteria, type FieldOperators, type Sort } from '@owlmeans/resource'
2
+ import { and, asc, desc, or, param, sql, type SQL } from 'drizzle-orm'
5
3
 
6
4
  import type { ColumnSpec, PgRuntimeTable, TableSpec } from '../types.js'
7
-
8
- const COMPARISON: Record<string, string> = {
9
- $eq: '=', $ne: '<>', $gt: '>', $gte: '>=', $lt: '<', $lte: '<='
10
- }
5
+ import { COMPARISON } from './consts.local.js'
6
+ import type { PathTarget } from './types.local.js'
7
+ import type { PgCriteriaHelper } from './criteria/types.js'
11
8
 
12
9
  const columnRef = (table: PgRuntimeTable, column: ColumnSpec): SQL => sql`${table[column.property]}`
13
10
 
@@ -166,17 +163,6 @@ const escapeLike = (value: string): string => value.replace(/[\\%_]/g, match =>
166
163
 
167
164
  // ─── Dotted paths into a jsonb column ────────────────────────────────────────────────────────────
168
165
 
169
- /**
170
- * A value at a path, as the jsonb it is (`#>`) and as text (`#>>`). The path is ONE bound `text[]`
171
- * parameter, so a segment holding a comma or a brace stays one segment.
172
- */
173
- interface PathTarget {
174
- value: SQL
175
- text: SQL
176
- /** Absent, or a JSON `null` — what every other store reads as "no value". */
177
- missing: SQL
178
- }
179
-
180
166
  const pathTarget = (table: PgRuntimeTable, column: ColumnSpec, path: string[]): PathTarget => {
181
167
  const value = sql`(${columnRef(table, column)} #> ${param(path)}::text[])`
182
168
 
@@ -295,131 +281,115 @@ const isOperatorSpec = (value: unknown): value is FieldOperators<any> =>
295
281
  value != null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)
296
282
  && Object.keys(value).some(key => key.startsWith('$'))
297
283
 
298
- /**
299
- * Translate a {@link Criteria} into a WHERE clause. `undefined` means "no constraint at
300
- * all" — the caller decides whether that is a full scan or a refusal.
301
- *
302
- * An unknown key raises rather than being skipped: a typo silently widening a query to
303
- * the whole table is the failure mode worth being loud about.
304
- *
305
- * @throws {UnsupportedArgumentError}
306
- */
307
- export const criteriaToSql = <T>(
308
- criteria: Criteria<T> | undefined, spec: TableSpec, table: PgRuntimeTable
309
- ): SQL | undefined => {
310
- const conditions = build(criteria, spec, table)
284
+ export const createPgCriteriaHelper = (): PgCriteriaHelper => {
285
+ const criteriaToSql = <T>(
286
+ criteria: Criteria<T> | undefined, spec: TableSpec, table: PgRuntimeTable
287
+ ): SQL | undefined => {
288
+ const conditions = build(criteria, spec, table)
311
289
 
312
- return conditions.length > 0 ? and(...conditions) : undefined
313
- }
290
+ return conditions.length > 0 ? and(...conditions) : undefined
291
+ }
314
292
 
315
- const build = <T>(
316
- criteria: Criteria<T> | undefined, spec: TableSpec, table: PgRuntimeTable
317
- ): SQL[] => {
318
- const conditions: SQL[] = []
319
- for (const [key, raw] of Object.entries(criteria ?? {})) {
320
- if (raw === undefined) {
321
- continue
322
- }
293
+ const build = <T>(
294
+ criteria: Criteria<T> | undefined, spec: TableSpec, table: PgRuntimeTable
295
+ ): SQL[] => {
296
+ const conditions: SQL[] = []
297
+ for (const [key, raw] of Object.entries(criteria ?? {})) {
298
+ if (raw === undefined) {
299
+ continue
300
+ }
323
301
 
324
- if (key === '$and' || key === '$or') {
325
- const parts = (Array.isArray(raw) ? raw : [raw])
326
- .map(entry => and(...build(entry as Criteria<T>, spec, table)))
327
- .filter((entry): entry is SQL => entry != null)
328
- if (parts.length > 0) {
329
- const combined = key === '$and' ? and(...parts) : or(...parts)
330
- if (combined != null) {
331
- conditions.push(combined)
302
+ if (key === '$and' || key === '$or') {
303
+ const parts = (Array.isArray(raw) ? raw : [raw])
304
+ .map(entry => and(...build(entry as Criteria<T>, spec, table)))
305
+ .filter((entry): entry is SQL => entry != null)
306
+ if (parts.length > 0) {
307
+ const combined = key === '$and' ? and(...parts) : or(...parts)
308
+ if (combined != null) {
309
+ conditions.push(combined)
310
+ }
332
311
  }
312
+ continue
333
313
  }
334
- continue
335
- }
336
- if (key === '$not') {
337
- const inner = and(...build(raw as Criteria<T>, spec, table))
338
- if (inner != null) {
339
- conditions.push(sql`NOT (${inner})`)
314
+ if (key === '$not') {
315
+ const inner = and(...build(raw as Criteria<T>, spec, table))
316
+ if (inner != null) {
317
+ conditions.push(sql`NOT (${inner})`)
318
+ }
319
+ continue
340
320
  }
341
- continue
342
- }
343
321
 
344
- /** `profile.city` reaches into a jsonb column rather than naming a column. */
345
- const [head, ...path] = key.split('.')
346
- const column = spec.byProperty[head]
347
- if (column == null) {
348
- throw new UnsupportedArgumentError(`criteria:${key}`)
349
- }
350
- if (path.length > 0) {
351
- if (!column.jsonb) {
352
- throw new UnsupportedArgumentError(`criteria-path:${key}`)
322
+ /** `profile.city` reaches into a jsonb column rather than naming a column. */
323
+ const [head, ...path] = key.split('.')
324
+ const column = spec.byProperty[head]
325
+ if (column == null) {
326
+ throw new UnsupportedArgumentError(`criteria:${key}`)
327
+ }
328
+ if (path.length > 0) {
329
+ if (!column.jsonb) {
330
+ throw new UnsupportedArgumentError(`criteria-path:${key}`)
331
+ }
332
+ conditions.push(...pathCondition(pathTarget(table, column, path), raw, key))
333
+ continue
353
334
  }
354
- conditions.push(...pathCondition(pathTarget(table, column, path), raw, key))
355
- continue
356
- }
357
335
 
358
- if (raw === null) {
359
- conditions.push(sql`${columnRef(table, column)} IS NULL`)
360
- continue
361
- }
362
- if (isOperatorSpec(raw)) {
363
- conditions.push(...operators(table, column, raw))
364
- continue
365
- }
366
- if (Array.isArray(raw)) {
367
- /**
368
- * A bare array means `IN` for a relational store, which is overwhelmingly the intent.
369
- * Exact array equality against a `text[]` column stays available as `{ $eq: [...] }`.
370
- */
371
- conditions.push(inList(table, column, raw, false))
372
- continue
373
- }
374
- if (column.jsonb && typeof raw === 'object') {
375
- conditions.push(sql`${columnRef(table, column)} @> ${JSON.stringify(raw)}`)
376
- continue
336
+ if (raw === null) {
337
+ conditions.push(sql`${columnRef(table, column)} IS NULL`)
338
+ continue
339
+ }
340
+ if (isOperatorSpec(raw)) {
341
+ conditions.push(...operators(table, column, raw))
342
+ continue
343
+ }
344
+ if (Array.isArray(raw)) {
345
+ /**
346
+ * A bare array means `IN` for a relational store, which is overwhelmingly the intent.
347
+ * Exact array equality against a `text[]` column stays available as `{ $eq: [...] }`.
348
+ */
349
+ conditions.push(inList(table, column, raw, false))
350
+ continue
351
+ }
352
+ if (column.jsonb && typeof raw === 'object') {
353
+ conditions.push(sql`${columnRef(table, column)} @> ${JSON.stringify(raw)}`)
354
+ continue
355
+ }
356
+ conditions.push(sql`${columnRef(table, column)} = ${value(raw, column)}`)
377
357
  }
378
- conditions.push(sql`${columnRef(table, column)} = ${value(raw, column)}`)
379
- }
380
358
 
381
- return conditions
382
- }
383
-
384
- /**
385
- * Translate a {@link Sort} list into ORDER BY. A bare field name is ascending;
386
- * `{ field, order: 'desc' }` reverses it — the same meaning every backend gives it.
387
- *
388
- * The primary key is always appended as a tiebreak. Postgres has no implicit row order, so
389
- * paginating on a non-unique sort key silently duplicates and skips rows between pages —
390
- * a difference from mongo that would otherwise surface as a data bug rather than an error.
391
- *
392
- * A dotted path is a column this table does not have: criteria can reach into jsonb, ORDER BY
393
- * cannot, and refusing is better than ordering by something the caller did not name.
394
- *
395
- * @throws {UnsupportedArgumentError}
396
- */
397
- export const sortToSql = <T>(
398
- sort: Sort<T>[] | undefined, spec: TableSpec, table: PgRuntimeTable
399
- ): SQL[] => {
400
- const order: SQL[] = []
401
- const seen: string[] = []
402
-
403
- for (const entry of sort ?? []) {
404
- const property = typeof entry === 'string' ? entry : entry.field
405
- const descending = typeof entry !== 'string' && entry.order === 'desc'
406
- const column = spec.byProperty[property]
407
- if (column == null) {
408
- throw new UnsupportedArgumentError(`sort:${property}`)
409
- }
410
- seen.push(column.column)
411
- order.push(descending ? desc(table[column.property]) : asc(table[column.property]))
359
+ return conditions
412
360
  }
413
361
 
414
- for (const key of spec.primaryKey) {
415
- if (seen.includes(key)) {
416
- continue
362
+ const sortToSql = <T>(
363
+ sort: Sort<T>[] | undefined, spec: TableSpec, table: PgRuntimeTable
364
+ ): SQL[] => {
365
+ const order: SQL[] = []
366
+ const seen: string[] = []
367
+
368
+ for (const entry of sort ?? []) {
369
+ const property = typeof entry === 'string' ? entry : entry.field
370
+ const descending = typeof entry !== 'string' && entry.order === 'desc'
371
+ const column = spec.byProperty[property]
372
+ if (column == null) {
373
+ throw new UnsupportedArgumentError(`sort:${property}`)
374
+ }
375
+ seen.push(column.column)
376
+ order.push(descending ? desc(table[column.property]) : asc(table[column.property]))
417
377
  }
418
- const column = spec.byColumn[key]
419
- if (column != null) {
420
- order.push(asc(table[column.property]))
378
+
379
+ for (const key of spec.primaryKey) {
380
+ if (seen.includes(key)) {
381
+ continue
382
+ }
383
+ const column = spec.byColumn[key]
384
+ if (column != null) {
385
+ order.push(asc(table[column.property]))
386
+ }
421
387
  }
388
+
389
+ return order
422
390
  }
423
391
 
424
- return order
392
+ return { criteriaToSql, sortToSql }
425
393
  }
394
+
395
+ export const pgCriteriaHelper = createPgCriteriaHelper()
@@ -0,0 +1,41 @@
1
+ import type { BasicContext } from '@owlmeans/context'
2
+
3
+ import type { DdlPlan, DdlStatement, LiveTable, TableSpec } from '../../types.js'
4
+
5
+ /** Structure reconciliation: the DDL that converges a live table onto its compiled spec. */
6
+ export interface PgDiffHelper {
7
+ /**
8
+ * The form an index or constraint definition is compared in, so the statement this package emits
9
+ * and the text Postgres reports for the object it created compare equal.
10
+ *
11
+ * Postgres deparses what it stores: it leaves an identifier unquoted when it can, casts every
12
+ * literal (`''::text`), casts a `varchar` column inside an expression, wraps a partial predicate and
13
+ * each of its operator expressions in parentheses, and rewrites `x IN (…)` as `x = ANY (ARRAY[…])`.
14
+ * Comparing the raw texts made every boot drop and recreate every declared index and every enum
15
+ * `CHECK`. So both sides lose case, quotes, casts, whitespace and parentheses, and `= ANY (ARRAY[…])`
16
+ * reads as `IN (…)`. What that cannot tell apart — two definitions differing only in how their
17
+ * operators group — is a change to make under a new name.
18
+ */
19
+ canonicalDefinition: (definition: string) => string
20
+ /**
21
+ * Foreign keys are planned separately from the rest of the table, and applied only once
22
+ * every resource has initialized: a key points at a table another resource owns, and that
23
+ * resource's own `init()` may not have run yet.
24
+ *
25
+ * @throws {PostgresPlaceholderError} when a target can't be resolved.
26
+ */
27
+ planForeignKeys: (spec: TableSpec, context?: BasicContext<any>) => DdlStatement[]
28
+ /**
29
+ * Compare the compiled specification against the live table and produce the ordered DDL
30
+ * that converges one onto the other.
31
+ *
32
+ * Statement order is load bearing:
33
+ * - `DROP COLUMN` is last, because a `USING` cast or a backfill may legitimately read a
34
+ * column that is about to disappear.
35
+ * - A primary key constraint is never dropped: it cascades into every referencing key and
36
+ * can't be undone inside the same transaction once dependents exist.
37
+ *
38
+ * Foreign keys are not part of the plan at all — see {@link PgDiffHelper.planForeignKeys}.
39
+ */
40
+ planSync: (spec: TableSpec, live: LiveTable, additive?: boolean) => DdlPlan
41
+ }