@owlmeans/postgres-resource 0.1.18-rc.42 → 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 (154) 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 +1 -1
  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 +5 -1
  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.map +1 -1
  33. package/build/resource.js +15 -11
  34. package/build/resource.js.map +1 -1
  35. package/build/types.d.ts.map +1 -1
  36. package/build/utils/consts.local.d.ts +2 -0
  37. package/build/utils/consts.local.d.ts.map +1 -1
  38. package/build/utils/consts.local.js +2 -0
  39. package/build/utils/consts.local.js.map +1 -1
  40. package/build/utils/criteria/types.d.ts +31 -0
  41. package/build/utils/criteria/types.d.ts.map +1 -0
  42. package/build/utils/criteria/types.js +2 -0
  43. package/build/utils/criteria/types.js.map +1 -0
  44. package/build/utils/criteria.d.ts +3 -27
  45. package/build/utils/criteria.d.ts.map +1 -1
  46. package/build/utils/criteria.js +86 -104
  47. package/build/utils/criteria.js.map +1 -1
  48. package/build/utils/diff/types.d.ts +40 -0
  49. package/build/utils/diff/types.d.ts.map +1 -0
  50. package/build/utils/diff/types.js +2 -0
  51. package/build/utils/diff/types.js.map +1 -0
  52. package/build/utils/diff.d.ts +5 -35
  53. package/build/utils/diff.d.ts.map +1 -1
  54. package/build/utils/diff.js +320 -344
  55. package/build/utils/diff.js.map +1 -1
  56. package/build/utils/index.d.ts +9 -0
  57. package/build/utils/index.d.ts.map +1 -1
  58. package/build/utils/index.js.map +1 -1
  59. package/build/utils/introspect/types.d.ts +9 -0
  60. package/build/utils/introspect/types.d.ts.map +1 -0
  61. package/build/utils/introspect/types.js +2 -0
  62. package/build/utils/introspect/types.js.map +1 -0
  63. package/build/utils/introspect.d.ts +3 -3
  64. package/build/utils/introspect.d.ts.map +1 -1
  65. package/build/utils/introspect.js +41 -38
  66. package/build/utils/introspect.js.map +1 -1
  67. package/build/utils/life-cycle/types.d.ts +37 -0
  68. package/build/utils/life-cycle/types.d.ts.map +1 -0
  69. package/build/utils/life-cycle/types.js +2 -0
  70. package/build/utils/life-cycle/types.js.map +1 -0
  71. package/build/utils/life-cycle.d.ts +3 -33
  72. package/build/utils/life-cycle.d.ts.map +1 -1
  73. package/build/utils/life-cycle.js +88 -108
  74. package/build/utils/life-cycle.js.map +1 -1
  75. package/build/utils/marshal/types.d.ts +39 -0
  76. package/build/utils/marshal/types.d.ts.map +1 -0
  77. package/build/utils/marshal/types.js +2 -0
  78. package/build/utils/marshal/types.js.map +1 -0
  79. package/build/utils/marshal.d.ts +4 -32
  80. package/build/utils/marshal.d.ts.map +1 -1
  81. package/build/utils/marshal.js +121 -144
  82. package/build/utils/marshal.js.map +1 -1
  83. package/build/utils/migrations.d.ts.map +1 -1
  84. package/build/utils/migrations.js +41 -36
  85. package/build/utils/migrations.js.map +1 -1
  86. package/build/utils/name/types.d.ts +43 -0
  87. package/build/utils/name/types.d.ts.map +1 -0
  88. package/build/utils/name/types.js +2 -0
  89. package/build/utils/name/types.js.map +1 -0
  90. package/build/utils/name.d.ts +6 -35
  91. package/build/utils/name.d.ts.map +1 -1
  92. package/build/utils/name.js +38 -57
  93. package/build/utils/name.js.map +1 -1
  94. package/build/utils/schema/types.d.ts +22 -0
  95. package/build/utils/schema/types.d.ts.map +1 -0
  96. package/build/utils/schema/types.js +2 -0
  97. package/build/utils/schema/types.js.map +1 -0
  98. package/build/utils/schema.d.ts +4 -24
  99. package/build/utils/schema.d.ts.map +1 -1
  100. package/build/utils/schema.js +339 -352
  101. package/build/utils/schema.js.map +1 -1
  102. package/build/utils/sql/types.d.ts +25 -0
  103. package/build/utils/sql/types.d.ts.map +1 -0
  104. package/build/utils/sql/types.js +2 -0
  105. package/build/utils/sql/types.js.map +1 -0
  106. package/build/utils/sql.d.ts +4 -18
  107. package/build/utils/sql.d.ts.map +1 -1
  108. package/build/utils/sql.js +0 -0
  109. package/build/utils/sql.js.map +1 -1
  110. package/build/utils/sync/types.d.ts +25 -0
  111. package/build/utils/sync/types.d.ts.map +1 -0
  112. package/build/utils/sync/types.js +2 -0
  113. package/build/utils/sync/types.js.map +1 -0
  114. package/build/utils/sync.d.ts +2 -19
  115. package/build/utils/sync.d.ts.map +1 -1
  116. package/build/utils/sync.js +64 -71
  117. package/build/utils/sync.js.map +1 -1
  118. package/package.json +7 -7
  119. package/src/consts.ts +7 -0
  120. package/src/declarations/types.ts +12 -0
  121. package/src/declarations.ts +29 -14
  122. package/src/errors.ts +1 -121
  123. package/src/index.ts +3 -0
  124. package/src/pg-error/types.ts +23 -0
  125. package/src/pg-error.ts +114 -0
  126. package/src/resource.ts +17 -13
  127. package/src/types.ts +1 -0
  128. package/src/utils/consts.local.ts +3 -0
  129. package/src/utils/criteria/types.ts +32 -0
  130. package/src/utils/criteria.ts +95 -110
  131. package/src/utils/diff/types.ts +41 -0
  132. package/src/utils/diff.ts +330 -348
  133. package/src/utils/index.ts +9 -0
  134. package/src/utils/introspect/types.ts +9 -0
  135. package/src/utils/introspect.ts +51 -45
  136. package/src/utils/life-cycle/types.ts +41 -0
  137. package/src/utils/life-cycle.ts +102 -121
  138. package/src/utils/marshal/types.ts +40 -0
  139. package/src/utils/marshal.ts +129 -147
  140. package/src/utils/migrations.ts +39 -33
  141. package/src/utils/name/types.ts +44 -0
  142. package/src/utils/name.ts +47 -60
  143. package/src/utils/schema/types.ts +26 -0
  144. package/src/utils/schema.ts +359 -366
  145. package/src/utils/sql/types.ts +25 -0
  146. package/src/utils/sql.ts +0 -0
  147. package/src/utils/sync/types.ts +25 -0
  148. package/src/utils/sync.ts +62 -68
  149. package/tests/criteria.spec.ts +4 -1
  150. package/tests/diff.spec.ts +4 -1
  151. package/tests/errors.spec.ts +4 -3
  152. package/tests/name.spec.ts +5 -3
  153. package/tests/schema.spec.ts +3 -1
  154. package/tests/sql.spec.ts +20 -22
package/src/errors.ts CHANGED
@@ -1,10 +1,5 @@
1
1
  import { ResilientError } from '@owlmeans/error'
2
- import {
3
- MisshapedRecord, RecordExists, ResourceError
4
- } from '@owlmeans/resource'
5
-
6
- import { PgErrorCode } from './consts.js'
7
- import type { PgDriverError } from './types.local.js'
2
+ import { ResourceError } from '@owlmeans/resource'
8
3
 
9
4
  export class PostgresError extends ResourceError {
10
5
  public static override typeName = `${ResourceError.typeName}PostgresError`
@@ -107,121 +102,6 @@ export class PostgresBootstrapError extends PostgresError {
107
102
  }
108
103
  }
109
104
 
110
- const isDriverError = (error: unknown): error is PgDriverError =>
111
- error != null && typeof error === 'object' && 'code' in error
112
-
113
- /**
114
- * Reach the `pg` error inside whatever wrapped it.
115
- *
116
- * Drizzle raises `DrizzleQueryError` and hangs the driver error off `cause`, so a unique
117
- * violation arriving through the query builder carries no `code` at the top level. Reading
118
- * only the outer error is what makes the whole vocabulary below collapse to one opaque
119
- * class on every CRUD path — and it is exactly the paths that go through Drizzle.
120
- *
121
- * The walk is bounded because `cause` chains can be circular.
122
- */
123
- const unwrap = (error: unknown): unknown => {
124
- let current = error
125
- for (let depth = 0; depth < 8; depth += 1) {
126
- if (isDriverError(current)) {
127
- return current
128
- }
129
- const cause = (current as { cause?: unknown } | null)?.cause
130
- if (cause == null || cause === current) {
131
- return error
132
- }
133
- current = cause
134
- }
135
-
136
- return error
137
- }
138
-
139
- /**
140
- * The framework's error marshalling preserves only type, message and stack, so the
141
- * Postgres diagnostics have to travel inside the message. Downstream tooling — notably
142
- * the viable-agent fixer, which retries on `42P01` / `42703` — classifies on exactly
143
- * this text, so the code always leads.
144
- *
145
- * `where` is deliberately never included: it can echo bound parameter values.
146
- *
147
- * Exported so a wrapper can quote the diagnostics without also quoting the translated error's
148
- * own type prefix, which would nest one error vocabulary inside another's message.
149
- */
150
- export const describePgError = (error: unknown): string => {
151
- const driver = unwrap(error)
152
- if (!isDriverError(driver)) {
153
- return error instanceof Error ? error.message : `${error}`
154
- }
155
-
156
- return describe(driver)
157
- }
158
-
159
- const describe = (error: PgDriverError): string => {
160
- const parts = [error.code ?? 'unknown']
161
- if (error.message != null) parts.push(error.message)
162
- if (error.constraint != null) parts.push(`constraint=${error.constraint}`)
163
- if (error.table != null) parts.push(`table=${error.table}`)
164
- if (error.column != null) parts.push(`column=${error.column}`)
165
- if (error.detail != null) parts.push(`detail=${error.detail}`)
166
- if (error.hint != null) parts.push(`hint=${error.hint}`)
167
- if (error.severity != null) parts.push(`severity=${error.severity}`)
168
-
169
- return parts.join(' ')
170
- }
171
-
172
- /**
173
- * Translate a raw `pg` driver error into the framework's error vocabulary, so a driver
174
- * type never escapes the package. Errors that already are `ResilientError`s pass through
175
- * untouched — the reconciler and the migration runner raise their own.
176
- * A migration may supply its resolved SQL; placeholders stay intact and parameter values
177
- * are never added. The statement travels in the message so error marshalling preserves it.
178
- */
179
- export const pgErrorToResourceError = (error: unknown, query?: string): Error => {
180
- if (error instanceof ResilientError) {
181
- return error
182
- }
183
- const driver = unwrap(error)
184
- if (!isDriverError(driver)) {
185
- return error instanceof Error ? error : new PostgresError(`${error}`)
186
- }
187
-
188
- const message = describe(driver) + (query == null ? '' : `\nSQL: ${query}`)
189
- const produce = (): Error => {
190
- switch (driver.code) {
191
- case PgErrorCode.UniqueViolation:
192
- return new RecordExists(message)
193
- case PgErrorCode.NotNullViolation:
194
- return new MisshapedRecord(message)
195
- case PgErrorCode.ForeignKeyViolation:
196
- return new PostgresForeignKeyError(message)
197
- case PgErrorCode.CheckViolation:
198
- return new PostgresCheckError(message)
199
- case PgErrorCode.DatatypeMismatch:
200
- case PgErrorCode.CannotCoerce:
201
- return new PostgresCastRequired(message)
202
- case PgErrorCode.SerializationFailure:
203
- case PgErrorCode.DeadlockDetected:
204
- return new PostgresDeadlockError(message)
205
- default:
206
- break
207
- }
208
- if (driver.code != null && driver.code.startsWith('08')) {
209
- return new PostgresConnectionError(message)
210
- }
211
- if (driver.code != null && driver.code.startsWith('23')) {
212
- return new PostgresConstraintError(message)
213
- }
214
-
215
- return new PostgresError(message)
216
- }
217
-
218
- const produced = produce()
219
- /** The *original* error, wrapper and all — unwrapping is for classification, not for loss. */
220
- produced.cause = error
221
-
222
- return produced
223
- }
224
-
225
105
  ResilientError.registerErrorClass(PostgresError)
226
106
  ResilientError.registerErrorClass(PostgresSyncError)
227
107
  ResilientError.registerErrorClass(PostgresCastRequired)
package/src/index.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export type * from './types.js'
2
2
  export * from './consts.js'
3
3
  export * from './declarations.js'
4
+ export type * from './declarations/types.js'
4
5
  export * from './errors.js'
6
+ export * from './pg-error.js'
7
+ export type * from './pg-error/types.js'
5
8
  export * from './helper.js'
6
9
  export * from './resource.js'
7
10
  export * from './utils/index.js'
@@ -0,0 +1,23 @@
1
+ /** Reading and translating the errors the `pg` driver raises. */
2
+ export interface PgErrorHelper {
3
+ /**
4
+ * The framework's error marshalling preserves only type, message and stack, so the
5
+ * Postgres diagnostics have to travel inside the message. Downstream tooling — notably
6
+ * the viable-agent fixer, which retries on `42P01` / `42703` — classifies on exactly
7
+ * this text, so the code always leads.
8
+ *
9
+ * `where` is deliberately never included: it can echo bound parameter values.
10
+ *
11
+ * Exported so a wrapper can quote the diagnostics without also quoting the translated error's
12
+ * own type prefix, which would nest one error vocabulary inside another's message.
13
+ */
14
+ describePgError: (error: unknown) => string
15
+ /**
16
+ * Translate a raw `pg` driver error into the framework's error vocabulary, so a driver
17
+ * type never escapes the package. Errors that already are `ResilientError`s pass through
18
+ * untouched — the reconciler and the migration runner raise their own.
19
+ * A migration may supply its resolved SQL; placeholders stay intact and parameter values
20
+ * are never added. The statement travels in the message so error marshalling preserves it.
21
+ */
22
+ pgErrorToResourceError: (error: unknown, query?: string) => Error
23
+ }
@@ -0,0 +1,114 @@
1
+ import { ResilientError } from '@owlmeans/error'
2
+ import { MisshapedRecord, RecordExists } from '@owlmeans/resource'
3
+
4
+ import { PgErrorCode } from './consts.js'
5
+ import {
6
+ PostgresCastRequired, PostgresCheckError, PostgresConnectionError, PostgresConstraintError,
7
+ PostgresDeadlockError, PostgresError, PostgresForeignKeyError
8
+ } from './errors.js'
9
+ import type { PgErrorHelper } from './pg-error/types.js'
10
+ import type { PgDriverError } from './types.local.js'
11
+
12
+ export const createPgErrorHelper = (): PgErrorHelper => {
13
+ const isDriverError = (error: unknown): error is PgDriverError =>
14
+ error != null && typeof error === 'object' && 'code' in error
15
+
16
+ /**
17
+ * Reach the `pg` error inside whatever wrapped it.
18
+ *
19
+ * Drizzle raises `DrizzleQueryError` and hangs the driver error off `cause`, so a unique
20
+ * violation arriving through the query builder carries no `code` at the top level. Reading
21
+ * only the outer error is what makes the whole vocabulary below collapse to one opaque
22
+ * class on every CRUD path — and it is exactly the paths that go through Drizzle.
23
+ *
24
+ * The walk is bounded because `cause` chains can be circular.
25
+ */
26
+ const unwrap = (error: unknown): unknown => {
27
+ let current = error
28
+ for (let depth = 0; depth < 8; depth += 1) {
29
+ if (isDriverError(current)) {
30
+ return current
31
+ }
32
+ const cause = (current as { cause?: unknown } | null)?.cause
33
+ if (cause == null || cause === current) {
34
+ return error
35
+ }
36
+ current = cause
37
+ }
38
+
39
+ return error
40
+ }
41
+
42
+ const describePgError = (error: unknown): string => {
43
+ const driver = unwrap(error)
44
+ if (!isDriverError(driver)) {
45
+ return error instanceof Error ? error.message : `${error}`
46
+ }
47
+
48
+ return describe(driver)
49
+ }
50
+
51
+ const describe = (error: PgDriverError): string => {
52
+ const parts = [error.code ?? 'unknown']
53
+ if (error.message != null) parts.push(error.message)
54
+ if (error.constraint != null) parts.push(`constraint=${error.constraint}`)
55
+ if (error.table != null) parts.push(`table=${error.table}`)
56
+ if (error.column != null) parts.push(`column=${error.column}`)
57
+ if (error.detail != null) parts.push(`detail=${error.detail}`)
58
+ if (error.hint != null) parts.push(`hint=${error.hint}`)
59
+ if (error.severity != null) parts.push(`severity=${error.severity}`)
60
+
61
+ return parts.join(' ')
62
+ }
63
+
64
+ const pgErrorToResourceError = (error: unknown, query?: string): Error => {
65
+ if (error instanceof ResilientError) {
66
+ return error
67
+ }
68
+ const driver = unwrap(error)
69
+ if (!isDriverError(driver)) {
70
+ return error instanceof Error ? error : new PostgresError(`${error}`)
71
+ }
72
+
73
+ const message = describe(driver) + (query == null ? '' : `\nSQL: ${query}`)
74
+ const produce = (): Error => {
75
+ switch (driver.code) {
76
+ case PgErrorCode.UniqueViolation:
77
+ return new RecordExists(message)
78
+ case PgErrorCode.NotNullViolation:
79
+ return new MisshapedRecord(message)
80
+ case PgErrorCode.ForeignKeyViolation:
81
+ return new PostgresForeignKeyError(message)
82
+ case PgErrorCode.CheckViolation:
83
+ return new PostgresCheckError(message)
84
+ case PgErrorCode.DatatypeMismatch:
85
+ case PgErrorCode.CannotCoerce:
86
+ return new PostgresCastRequired(message)
87
+ case PgErrorCode.SerializationFailure:
88
+ case PgErrorCode.DeadlockDetected:
89
+ return new PostgresDeadlockError(message)
90
+ default:
91
+ break
92
+ }
93
+ if (driver.code != null && driver.code.startsWith('08')) {
94
+ return new PostgresConnectionError(message)
95
+ }
96
+ if (driver.code != null && driver.code.startsWith('23')) {
97
+ return new PostgresConstraintError(message)
98
+ }
99
+
100
+ return new PostgresError(message)
101
+ }
102
+
103
+ const produced = produce()
104
+ /** The *original* error, wrapper and all — unwrapping is for classification, not for loss. */
105
+ produced.cause = error
106
+
107
+ return produced
108
+ }
109
+
110
+ return { describePgError, pgErrorToResourceError }
111
+ }
112
+
113
+ export const pgErrorHelper = createPgErrorHelper()
114
+
package/src/resource.ts CHANGED
@@ -5,18 +5,18 @@ import { eq, sql, type SQL } from 'drizzle-orm'
5
5
  import type { PoolClient, QueryResultRow } from 'pg'
6
6
 
7
7
  import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE, ID_FIELD } from './consts.js'
8
- import { getDeclaration } from './declarations.js'
9
- import { pgErrorToResourceError } from './errors.js'
8
+ import { pgDeclarationHelper } from './declarations.js'
10
9
  import { getSchemaSecureFeilds } from './helper.js'
10
+ import { pgErrorHelper } from './pg-error.js'
11
11
  import type {
12
12
  ColumnSpec, PgIndexSpec, PgRuntimeTable, PostgresDb, PostgresDbService, PostgresResource,
13
13
  PostgresTx, TableSpec
14
14
  } from './types.js'
15
- import { criteriaToSql, sortToSql } from './utils/criteria.js'
16
- import { initializeTable } from './utils/life-cycle.js'
17
- import { recordToFullValues, recordToValues, resultToRecord, rowToRecord } from './utils/marshal.js'
15
+ import { pgCriteriaHelper } from './utils/criteria.js'
16
+ import { makePgLifeCycleHelper } from './utils/life-cycle.js'
17
+ import { pgMarshalHelper } from './utils/marshal.js'
18
18
  import { makeTx } from './utils/migrations.js'
19
- import { refOf, resolvePlaceholders } from './utils/sql.js'
19
+ import { pgPlaceholdersOf } from './utils/sql.js'
20
20
  import type { Config, Context } from './types.local.js'
21
21
 
22
22
 
@@ -34,7 +34,10 @@ export const makePostgresResource = <
34
34
  tableName?: string
35
35
  ): T => {
36
36
  const location = `postgres-resource:${alias}`
37
- const declaration = getDeclaration(alias)
37
+ const declaration = pgDeclarationHelper.getDeclaration(alias)
38
+ const { pgErrorToResourceError } = pgErrorHelper
39
+ const { criteriaToSql, sortToSql } = pgCriteriaHelper
40
+ const { recordToFullValues, recordToValues, resultToRecord, rowToRecord } = pgMarshalHelper
38
41
 
39
42
  /** Set by `init()`; every data method goes through `ensure()`, which guarantees them. */
40
43
  let spec: TableSpec | undefined
@@ -107,7 +110,7 @@ export const makePostgresResource = <
107
110
  const context = assertContext<Config, Context>(resource.ctx as Context, location)
108
111
  try {
109
112
  const result = await db.pool.query<Row>(
110
- resolvePlaceholders(text, context, table), params as never[]
113
+ pgPlaceholdersOf(context).resolvePlaceholders(text, table), params as never[]
111
114
  )
112
115
 
113
116
  return { rows: result.rows, count: result.rowCount ?? 0 }
@@ -397,19 +400,20 @@ export const makePostgresResource = <
397
400
  throw new SyntaxError(`Postgres resource not initialized: ${alias}`)
398
401
  }
399
402
 
400
- return refOf(context, spec, resourceAlias)
403
+ return pgPlaceholdersOf(context).refOf(spec, resourceAlias)
401
404
  },
402
405
 
403
406
  transaction: async fn => {
404
407
  const { db, spec: table } = await ensure()
405
408
  const context = assertContext<Config, Context>(resource.ctx as Context, location)
409
+ const placeholders = pgPlaceholdersOf(context)
406
410
  const client: PoolClient = await db.pool.connect()
407
411
  try {
408
412
  await client.query('BEGIN')
409
413
  const result = await fn(makeTx(
410
414
  client,
411
- text => resolvePlaceholders(text, context, table),
412
- resourceAlias => refOf(context, table, resourceAlias)
415
+ text => placeholders.resolvePlaceholders(text, table),
416
+ resourceAlias => placeholders.refOf(table, resourceAlias)
413
417
  ))
414
418
  await client.query('COMMIT')
415
419
 
@@ -504,8 +508,8 @@ export const makePostgresResource = <
504
508
  const db = await postgres.db(dbAlias)
505
509
  const config = postgres.config(dbAlias)
506
510
 
507
- const initialized = await initializeTable(
508
- db, config, resource as unknown as PostgresResource<ResourceRecord>, context,
511
+ const initialized = await makePgLifeCycleHelper(db).initializeTable(
512
+ config, resource as unknown as PostgresResource<ResourceRecord>, context,
509
513
  task => postgres.defer(dbAlias, task)
510
514
  )
511
515
  spec = initialized.spec
package/src/types.ts CHANGED
@@ -255,6 +255,7 @@ export interface PgReferenceSpec {
255
255
  onUpdate?: PgReferentialAction | `${PgReferentialAction}`
256
256
  }
257
257
 
258
+ // Kept as a type: it merges an index signature over the table's own members, which an interface refuses.
258
259
  /**
259
260
  * The Drizzle table `specToTable` compiles from a {@link TableSpec}.
260
261
  *
@@ -66,3 +66,6 @@ export const CAST_DATA_CODES: string[] = [
66
66
  PgErrorCode.StringDataRightTruncation,
67
67
  PgErrorCode.NumericValueOutOfRange
68
68
  ]
69
+
70
+ /** The largest `integer` column value; a schema `maximum` past it needs `bigint`. */
71
+ export const INT32_MAX = 2147483647
@@ -0,0 +1,32 @@
1
+ import type { Criteria, Sort } from '@owlmeans/resource'
2
+ import type { SQL } from 'drizzle-orm'
3
+
4
+ import type { PgRuntimeTable, TableSpec } from '../../types.js'
5
+
6
+ /** The portable criteria and sort vocabulary, as Drizzle SQL over one compiled table. */
7
+ export interface PgCriteriaHelper {
8
+ /**
9
+ * Translate a {@link Criteria} into a WHERE clause. `undefined` means "no constraint at
10
+ * all" — the caller decides whether that is a full scan or a refusal.
11
+ *
12
+ * An unknown key raises rather than being skipped: a typo silently widening a query to
13
+ * the whole table is the failure mode worth being loud about.
14
+ *
15
+ * @throws {UnsupportedArgumentError}
16
+ */
17
+ criteriaToSql: <T>(criteria: Criteria<T> | undefined, spec: TableSpec, table: PgRuntimeTable) => SQL | undefined
18
+ /**
19
+ * Translate a {@link Sort} list into ORDER BY. A bare field name is ascending;
20
+ * `{ field, order: 'desc' }` reverses it — the same meaning every backend gives it.
21
+ *
22
+ * The primary key is always appended as a tiebreak. Postgres has no implicit row order, so
23
+ * paginating on a non-unique sort key silently duplicates and skips rows between pages —
24
+ * a difference from mongo that would otherwise surface as a data bug rather than an error.
25
+ *
26
+ * A dotted path is a column this table does not have: criteria can reach into jsonb, ORDER BY
27
+ * cannot, and refusing is better than ordering by something the caller did not name.
28
+ *
29
+ * @throws {UnsupportedArgumentError}
30
+ */
31
+ sortToSql: <T>(sort: Sort<T>[] | undefined, spec: TableSpec, table: PgRuntimeTable) => SQL[]
32
+ }
@@ -4,6 +4,7 @@ import { and, asc, desc, or, param, sql, type SQL } from 'drizzle-orm'
4
4
  import type { ColumnSpec, PgRuntimeTable, TableSpec } from '../types.js'
5
5
  import { COMPARISON } from './consts.local.js'
6
6
  import type { PathTarget } from './types.local.js'
7
+ import type { PgCriteriaHelper } from './criteria/types.js'
7
8
 
8
9
  const columnRef = (table: PgRuntimeTable, column: ColumnSpec): SQL => sql`${table[column.property]}`
9
10
 
@@ -280,131 +281,115 @@ const isOperatorSpec = (value: unknown): value is FieldOperators<any> =>
280
281
  value != null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)
281
282
  && Object.keys(value).some(key => key.startsWith('$'))
282
283
 
283
- /**
284
- * Translate a {@link Criteria} into a WHERE clause. `undefined` means "no constraint at
285
- * all" — the caller decides whether that is a full scan or a refusal.
286
- *
287
- * An unknown key raises rather than being skipped: a typo silently widening a query to
288
- * the whole table is the failure mode worth being loud about.
289
- *
290
- * @throws {UnsupportedArgumentError}
291
- */
292
- export const criteriaToSql = <T>(
293
- criteria: Criteria<T> | undefined, spec: TableSpec, table: PgRuntimeTable
294
- ): SQL | undefined => {
295
- 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)
296
289
 
297
- return conditions.length > 0 ? and(...conditions) : undefined
298
- }
290
+ return conditions.length > 0 ? and(...conditions) : undefined
291
+ }
299
292
 
300
- const build = <T>(
301
- criteria: Criteria<T> | undefined, spec: TableSpec, table: PgRuntimeTable
302
- ): SQL[] => {
303
- const conditions: SQL[] = []
304
- for (const [key, raw] of Object.entries(criteria ?? {})) {
305
- if (raw === undefined) {
306
- continue
307
- }
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
+ }
308
301
 
309
- if (key === '$and' || key === '$or') {
310
- const parts = (Array.isArray(raw) ? raw : [raw])
311
- .map(entry => and(...build(entry as Criteria<T>, spec, table)))
312
- .filter((entry): entry is SQL => entry != null)
313
- if (parts.length > 0) {
314
- const combined = key === '$and' ? and(...parts) : or(...parts)
315
- if (combined != null) {
316
- 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
+ }
317
311
  }
312
+ continue
318
313
  }
319
- continue
320
- }
321
- if (key === '$not') {
322
- const inner = and(...build(raw as Criteria<T>, spec, table))
323
- if (inner != null) {
324
- 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
325
320
  }
326
- continue
327
- }
328
321
 
329
- /** `profile.city` reaches into a jsonb column rather than naming a column. */
330
- const [head, ...path] = key.split('.')
331
- const column = spec.byProperty[head]
332
- if (column == null) {
333
- throw new UnsupportedArgumentError(`criteria:${key}`)
334
- }
335
- if (path.length > 0) {
336
- if (!column.jsonb) {
337
- 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
338
334
  }
339
- conditions.push(...pathCondition(pathTarget(table, column, path), raw, key))
340
- continue
341
- }
342
335
 
343
- if (raw === null) {
344
- conditions.push(sql`${columnRef(table, column)} IS NULL`)
345
- continue
346
- }
347
- if (isOperatorSpec(raw)) {
348
- conditions.push(...operators(table, column, raw))
349
- continue
350
- }
351
- if (Array.isArray(raw)) {
352
- /**
353
- * A bare array means `IN` for a relational store, which is overwhelmingly the intent.
354
- * Exact array equality against a `text[]` column stays available as `{ $eq: [...] }`.
355
- */
356
- conditions.push(inList(table, column, raw, false))
357
- continue
358
- }
359
- if (column.jsonb && typeof raw === 'object') {
360
- conditions.push(sql`${columnRef(table, column)} @> ${JSON.stringify(raw)}`)
361
- 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)}`)
362
357
  }
363
- conditions.push(sql`${columnRef(table, column)} = ${value(raw, column)}`)
364
- }
365
-
366
- return conditions
367
- }
368
358
 
369
- /**
370
- * Translate a {@link Sort} list into ORDER BY. A bare field name is ascending;
371
- * `{ field, order: 'desc' }` reverses it — the same meaning every backend gives it.
372
- *
373
- * The primary key is always appended as a tiebreak. Postgres has no implicit row order, so
374
- * paginating on a non-unique sort key silently duplicates and skips rows between pages —
375
- * a difference from mongo that would otherwise surface as a data bug rather than an error.
376
- *
377
- * A dotted path is a column this table does not have: criteria can reach into jsonb, ORDER BY
378
- * cannot, and refusing is better than ordering by something the caller did not name.
379
- *
380
- * @throws {UnsupportedArgumentError}
381
- */
382
- export const sortToSql = <T>(
383
- sort: Sort<T>[] | undefined, spec: TableSpec, table: PgRuntimeTable
384
- ): SQL[] => {
385
- const order: SQL[] = []
386
- const seen: string[] = []
387
-
388
- for (const entry of sort ?? []) {
389
- const property = typeof entry === 'string' ? entry : entry.field
390
- const descending = typeof entry !== 'string' && entry.order === 'desc'
391
- const column = spec.byProperty[property]
392
- if (column == null) {
393
- throw new UnsupportedArgumentError(`sort:${property}`)
394
- }
395
- seen.push(column.column)
396
- order.push(descending ? desc(table[column.property]) : asc(table[column.property]))
359
+ return conditions
397
360
  }
398
361
 
399
- for (const key of spec.primaryKey) {
400
- if (seen.includes(key)) {
401
- 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]))
402
377
  }
403
- const column = spec.byColumn[key]
404
- if (column != null) {
405
- 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
+ }
406
387
  }
388
+
389
+ return order
407
390
  }
408
391
 
409
- return order
392
+ return { criteriaToSql, sortToSql }
410
393
  }
394
+
395
+ export const pgCriteriaHelper = createPgCriteriaHelper()