@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.
- package/README.md +13 -13
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres-resource/SKILL.md +1 -1
- package/build/consts.d.ts +9 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +6 -0
- package/build/consts.js.map +1 -1
- package/build/declarations/types.d.ts +12 -0
- package/build/declarations/types.d.ts.map +1 -0
- package/build/declarations/types.js +2 -0
- package/build/declarations/types.js.map +1 -0
- package/build/declarations.d.ts +5 -1
- package/build/declarations.d.ts.map +1 -1
- package/build/declarations.js +25 -15
- package/build/declarations.js.map +1 -1
- package/build/errors.d.ts +0 -20
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +1 -111
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/pg-error/types.d.ts +24 -0
- package/build/pg-error/types.d.ts.map +1 -0
- package/build/pg-error/types.js +2 -0
- package/build/pg-error/types.js.map +1 -0
- package/build/pg-error.d.ts +4 -0
- package/build/pg-error.d.ts.map +1 -0
- package/build/pg-error.js +100 -0
- package/build/pg-error.js.map +1 -0
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +15 -11
- package/build/resource.js.map +1 -1
- package/build/types.d.ts.map +1 -1
- package/build/utils/consts.local.d.ts +2 -0
- package/build/utils/consts.local.d.ts.map +1 -1
- package/build/utils/consts.local.js +2 -0
- package/build/utils/consts.local.js.map +1 -1
- package/build/utils/criteria/types.d.ts +31 -0
- package/build/utils/criteria/types.d.ts.map +1 -0
- package/build/utils/criteria/types.js +2 -0
- package/build/utils/criteria/types.js.map +1 -0
- package/build/utils/criteria.d.ts +3 -27
- package/build/utils/criteria.d.ts.map +1 -1
- package/build/utils/criteria.js +86 -104
- package/build/utils/criteria.js.map +1 -1
- package/build/utils/diff/types.d.ts +40 -0
- package/build/utils/diff/types.d.ts.map +1 -0
- package/build/utils/diff/types.js +2 -0
- package/build/utils/diff/types.js.map +1 -0
- package/build/utils/diff.d.ts +5 -35
- package/build/utils/diff.d.ts.map +1 -1
- package/build/utils/diff.js +320 -344
- package/build/utils/diff.js.map +1 -1
- package/build/utils/index.d.ts +9 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/introspect/types.d.ts +9 -0
- package/build/utils/introspect/types.d.ts.map +1 -0
- package/build/utils/introspect/types.js +2 -0
- package/build/utils/introspect/types.js.map +1 -0
- package/build/utils/introspect.d.ts +3 -3
- package/build/utils/introspect.d.ts.map +1 -1
- package/build/utils/introspect.js +41 -38
- package/build/utils/introspect.js.map +1 -1
- package/build/utils/life-cycle/types.d.ts +37 -0
- package/build/utils/life-cycle/types.d.ts.map +1 -0
- package/build/utils/life-cycle/types.js +2 -0
- package/build/utils/life-cycle/types.js.map +1 -0
- package/build/utils/life-cycle.d.ts +3 -33
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +88 -108
- package/build/utils/life-cycle.js.map +1 -1
- package/build/utils/marshal/types.d.ts +39 -0
- package/build/utils/marshal/types.d.ts.map +1 -0
- package/build/utils/marshal/types.js +2 -0
- package/build/utils/marshal/types.js.map +1 -0
- package/build/utils/marshal.d.ts +4 -32
- package/build/utils/marshal.d.ts.map +1 -1
- package/build/utils/marshal.js +121 -144
- package/build/utils/marshal.js.map +1 -1
- package/build/utils/migrations.d.ts.map +1 -1
- package/build/utils/migrations.js +41 -36
- package/build/utils/migrations.js.map +1 -1
- package/build/utils/name/types.d.ts +43 -0
- package/build/utils/name/types.d.ts.map +1 -0
- package/build/utils/name/types.js +2 -0
- package/build/utils/name/types.js.map +1 -0
- package/build/utils/name.d.ts +6 -35
- package/build/utils/name.d.ts.map +1 -1
- package/build/utils/name.js +38 -57
- package/build/utils/name.js.map +1 -1
- package/build/utils/schema/types.d.ts +22 -0
- package/build/utils/schema/types.d.ts.map +1 -0
- package/build/utils/schema/types.js +2 -0
- package/build/utils/schema/types.js.map +1 -0
- package/build/utils/schema.d.ts +4 -24
- package/build/utils/schema.d.ts.map +1 -1
- package/build/utils/schema.js +339 -352
- package/build/utils/schema.js.map +1 -1
- package/build/utils/sql/types.d.ts +25 -0
- package/build/utils/sql/types.d.ts.map +1 -0
- package/build/utils/sql/types.js +2 -0
- package/build/utils/sql/types.js.map +1 -0
- package/build/utils/sql.d.ts +4 -18
- package/build/utils/sql.d.ts.map +1 -1
- package/build/utils/sql.js +0 -0
- package/build/utils/sql.js.map +1 -1
- package/build/utils/sync/types.d.ts +25 -0
- package/build/utils/sync/types.d.ts.map +1 -0
- package/build/utils/sync/types.js +2 -0
- package/build/utils/sync/types.js.map +1 -0
- package/build/utils/sync.d.ts +2 -19
- package/build/utils/sync.d.ts.map +1 -1
- package/build/utils/sync.js +64 -71
- package/build/utils/sync.js.map +1 -1
- package/package.json +7 -7
- package/src/consts.ts +7 -0
- package/src/declarations/types.ts +12 -0
- package/src/declarations.ts +29 -14
- package/src/errors.ts +1 -121
- package/src/index.ts +3 -0
- package/src/pg-error/types.ts +23 -0
- package/src/pg-error.ts +114 -0
- package/src/resource.ts +17 -13
- package/src/types.ts +1 -0
- package/src/utils/consts.local.ts +3 -0
- package/src/utils/criteria/types.ts +32 -0
- package/src/utils/criteria.ts +95 -110
- package/src/utils/diff/types.ts +41 -0
- package/src/utils/diff.ts +330 -348
- package/src/utils/index.ts +9 -0
- package/src/utils/introspect/types.ts +9 -0
- package/src/utils/introspect.ts +51 -45
- package/src/utils/life-cycle/types.ts +41 -0
- package/src/utils/life-cycle.ts +102 -121
- package/src/utils/marshal/types.ts +40 -0
- package/src/utils/marshal.ts +129 -147
- package/src/utils/migrations.ts +39 -33
- package/src/utils/name/types.ts +44 -0
- package/src/utils/name.ts +47 -60
- package/src/utils/schema/types.ts +26 -0
- package/src/utils/schema.ts +359 -366
- package/src/utils/sql/types.ts +25 -0
- package/src/utils/sql.ts +0 -0
- package/src/utils/sync/types.ts +25 -0
- package/src/utils/sync.ts +62 -68
- package/tests/criteria.spec.ts +4 -1
- package/tests/diff.spec.ts +4 -1
- package/tests/errors.spec.ts +4 -3
- package/tests/name.spec.ts +5 -3
- package/tests/schema.spec.ts +3 -1
- 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
|
+
}
|
package/src/pg-error.ts
ADDED
|
@@ -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 {
|
|
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 {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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 {
|
|
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,
|
|
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(
|
|
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,
|
|
412
|
-
resourceAlias => refOf(
|
|
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
|
-
|
|
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
|
*
|
|
@@ -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
|
+
}
|
package/src/utils/criteria.ts
CHANGED
|
@@ -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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
298
|
-
}
|
|
290
|
+
return conditions.length > 0 ? and(...conditions) : undefined
|
|
291
|
+
}
|
|
299
292
|
|
|
300
|
-
const build = <T>(
|
|
301
|
-
|
|
302
|
-
): SQL[] => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
392
|
+
return { criteriaToSql, sortToSql }
|
|
410
393
|
}
|
|
394
|
+
|
|
395
|
+
export const pgCriteriaHelper = createPgCriteriaHelper()
|