@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.
- package/README.md +13 -13
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres-resource/SKILL.md +2 -2
- 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 +6 -9
- 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 +1 -1
- 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 +6 -1
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +14 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/consts.local.d.ts +35 -0
- package/build/utils/consts.local.d.ts.map +1 -0
- package/build/utils/consts.local.js +63 -0
- package/build/utils/consts.local.js.map +1 -0
- 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 +88 -107
- 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 +321 -343
- package/build/utils/diff.js.map +1 -1
- package/build/utils/index.d.ts +10 -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 +42 -67
- 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 -38
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +90 -107
- 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 +124 -141
- 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 +40 -59
- 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 +341 -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 +5 -27
- 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 +67 -84
- package/build/utils/sync.js.map +1 -1
- package/build/utils/types.d.ts +18 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/build/utils/types.local.d.ts +33 -0
- package/build/utils/types.local.d.ts.map +1 -0
- package/build/utils/types.local.js +2 -0
- package/build/utils/types.local.js.map +1 -0
- package/package.json +14 -13
- package/src/consts.ts +7 -0
- package/src/declarations/types.ts +12 -0
- package/src/declarations.ts +30 -23
- package/src/errors.ts +1 -131
- 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 +20 -24
- package/src/types.local.ts +16 -0
- package/src/types.ts +8 -4
- package/src/utils/consts.local.ts +71 -0
- package/src/utils/criteria/types.ts +32 -0
- package/src/utils/criteria.ts +99 -129
- package/src/utils/diff/types.ts +41 -0
- package/src/utils/diff.ts +331 -348
- package/src/utils/index.ts +10 -0
- package/src/utils/introspect/types.ts +9 -0
- package/src/utils/introspect.ts +54 -79
- package/src/utils/life-cycle/types.ts +41 -0
- package/src/utils/life-cycle.ts +108 -134
- package/src/utils/marshal/types.ts +40 -0
- package/src/utils/marshal.ts +134 -147
- package/src/utils/migrations.ts +39 -33
- package/src/utils/name/types.ts +44 -0
- package/src/utils/name.ts +49 -63
- package/src/utils/schema/types.ts +26 -0
- package/src/utils/schema.ts +366 -397
- 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 +65 -83
- package/src/utils/types.local.ts +35 -0
- package/src/utils/types.ts +19 -0
- package/tests/criteria.spec.ts +4 -1
- package/tests/diff.spec.ts +28 -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/declarations.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { createMigrationRegistry } from '@owlmeans/resource'
|
|
2
|
-
import type { MigrationRegistry } from '@owlmeans/resource'
|
|
3
|
-
import type { AnySchema } from 'ajv'
|
|
4
2
|
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
export interface PostgresDeclaration {
|
|
8
|
-
schema?: AnySchema
|
|
9
|
-
indexes: PgIndexSpec[]
|
|
10
|
-
migrations: MigrationRegistry<PostgresTx>
|
|
11
|
-
}
|
|
3
|
+
import type { PgDeclarationHelper } from './declarations/types.js'
|
|
4
|
+
import type { PostgresTx, PostgresDeclaration } from './types.js'
|
|
12
5
|
|
|
13
6
|
/**
|
|
14
7
|
* Per-alias declaration store, held at module scope rather than on the resource object.
|
|
@@ -18,24 +11,38 @@ export interface PostgresDeclaration {
|
|
|
18
11
|
* a no-op: every run reads and extends the same schema, indexes and migrations, so nothing a
|
|
19
12
|
* caller chained onto an earlier resource object is lost. Losing a migration is silent — the
|
|
20
13
|
* data transformation simply never runs — which is why the store cannot live on the object.
|
|
14
|
+
*
|
|
15
|
+
* Process-wide for the same reason: it stays outside the factory, so every helper built from it
|
|
16
|
+
* reads the one store.
|
|
21
17
|
*/
|
|
22
18
|
const declarations: Map<string, PostgresDeclaration> = new Map()
|
|
23
19
|
|
|
24
|
-
export const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
declaration
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
export const createPgDeclarationHelper = (): PgDeclarationHelper => {
|
|
21
|
+
const getDeclaration = (alias: string): PostgresDeclaration => {
|
|
22
|
+
let declaration = declarations.get(alias)
|
|
23
|
+
if (declaration == null) {
|
|
24
|
+
declaration = { indexes: [], migrations: createMigrationRegistry<PostgresTx>() }
|
|
25
|
+
declarations.set(alias, declaration)
|
|
26
|
+
}
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
}
|
|
28
|
+
return declaration
|
|
29
|
+
}
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const resetDeclarations = (alias?: string): void => {
|
|
32
|
+
if (alias == null) {
|
|
33
|
+
declarations.clear()
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
declarations.delete(alias)
|
|
39
37
|
}
|
|
40
|
-
|
|
38
|
+
|
|
39
|
+
return { getDeclaration, resetDeclarations }
|
|
41
40
|
}
|
|
41
|
+
|
|
42
|
+
export const pgDeclarationHelper = createPgDeclarationHelper()
|
|
43
|
+
|
|
44
|
+
/** @deprecated compat:factory-refactor — use `pgDeclarationHelper.getDeclaration(…)` */
|
|
45
|
+
export const getDeclaration = (alias: string): PostgresDeclaration => pgDeclarationHelper.getDeclaration(alias)
|
|
46
|
+
|
|
47
|
+
/** @deprecated compat:factory-refactor — use `pgDeclarationHelper.resetDeclarations(…)` */
|
|
48
|
+
export const resetDeclarations = (alias?: string): void => pgDeclarationHelper.resetDeclarations(alias)
|
package/src/errors.ts
CHANGED
|
@@ -1,9 +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'
|
|
2
|
+
import { ResourceError } from '@owlmeans/resource'
|
|
7
3
|
|
|
8
4
|
export class PostgresError extends ResourceError {
|
|
9
5
|
public static override typeName = `${ResourceError.typeName}PostgresError`
|
|
@@ -106,132 +102,6 @@ export class PostgresBootstrapError extends PostgresError {
|
|
|
106
102
|
}
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
interface PgDriverError {
|
|
110
|
-
code?: string
|
|
111
|
-
detail?: string
|
|
112
|
-
hint?: string
|
|
113
|
-
severity?: string
|
|
114
|
-
constraint?: string
|
|
115
|
-
table?: string
|
|
116
|
-
column?: string
|
|
117
|
-
message?: string
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const isDriverError = (error: unknown): error is PgDriverError =>
|
|
121
|
-
error != null && typeof error === 'object' && 'code' in error
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Reach the `pg` error inside whatever wrapped it.
|
|
125
|
-
*
|
|
126
|
-
* Drizzle raises `DrizzleQueryError` and hangs the driver error off `cause`, so a unique
|
|
127
|
-
* violation arriving through the query builder carries no `code` at the top level. Reading
|
|
128
|
-
* only the outer error is what makes the whole vocabulary below collapse to one opaque
|
|
129
|
-
* class on every CRUD path — and it is exactly the paths that go through Drizzle.
|
|
130
|
-
*
|
|
131
|
-
* The walk is bounded because `cause` chains can be circular.
|
|
132
|
-
*/
|
|
133
|
-
const unwrap = (error: unknown): unknown => {
|
|
134
|
-
let current = error
|
|
135
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
136
|
-
if (isDriverError(current)) {
|
|
137
|
-
return current
|
|
138
|
-
}
|
|
139
|
-
const cause = (current as { cause?: unknown } | null)?.cause
|
|
140
|
-
if (cause == null || cause === current) {
|
|
141
|
-
return error
|
|
142
|
-
}
|
|
143
|
-
current = cause
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return error
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* The framework's error marshalling preserves only type, message and stack, so the
|
|
151
|
-
* Postgres diagnostics have to travel inside the message. Downstream tooling — notably
|
|
152
|
-
* the viable-agent fixer, which retries on `42P01` / `42703` — classifies on exactly
|
|
153
|
-
* this text, so the code always leads.
|
|
154
|
-
*
|
|
155
|
-
* `where` is deliberately never included: it can echo bound parameter values.
|
|
156
|
-
*
|
|
157
|
-
* Exported so a wrapper can quote the diagnostics without also quoting the translated error's
|
|
158
|
-
* own type prefix, which would nest one error vocabulary inside another's message.
|
|
159
|
-
*/
|
|
160
|
-
export const describePgError = (error: unknown): string => {
|
|
161
|
-
const driver = unwrap(error)
|
|
162
|
-
if (!isDriverError(driver)) {
|
|
163
|
-
return error instanceof Error ? error.message : `${error}`
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return describe(driver)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const describe = (error: PgDriverError): string => {
|
|
170
|
-
const parts = [error.code ?? 'unknown']
|
|
171
|
-
if (error.message != null) parts.push(error.message)
|
|
172
|
-
if (error.constraint != null) parts.push(`constraint=${error.constraint}`)
|
|
173
|
-
if (error.table != null) parts.push(`table=${error.table}`)
|
|
174
|
-
if (error.column != null) parts.push(`column=${error.column}`)
|
|
175
|
-
if (error.detail != null) parts.push(`detail=${error.detail}`)
|
|
176
|
-
if (error.hint != null) parts.push(`hint=${error.hint}`)
|
|
177
|
-
if (error.severity != null) parts.push(`severity=${error.severity}`)
|
|
178
|
-
|
|
179
|
-
return parts.join(' ')
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Translate a raw `pg` driver error into the framework's error vocabulary, so a driver
|
|
184
|
-
* type never escapes the package. Errors that already are `ResilientError`s pass through
|
|
185
|
-
* untouched — the reconciler and the migration runner raise their own.
|
|
186
|
-
* A migration may supply its resolved SQL; placeholders stay intact and parameter values
|
|
187
|
-
* are never added. The statement travels in the message so error marshalling preserves it.
|
|
188
|
-
*/
|
|
189
|
-
export const pgErrorToResourceError = (error: unknown, query?: string): Error => {
|
|
190
|
-
if (error instanceof ResilientError) {
|
|
191
|
-
return error
|
|
192
|
-
}
|
|
193
|
-
const driver = unwrap(error)
|
|
194
|
-
if (!isDriverError(driver)) {
|
|
195
|
-
return error instanceof Error ? error : new PostgresError(`${error}`)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const message = describe(driver) + (query == null ? '' : `\nSQL: ${query}`)
|
|
199
|
-
const produce = (): Error => {
|
|
200
|
-
switch (driver.code) {
|
|
201
|
-
case PgErrorCode.UniqueViolation:
|
|
202
|
-
return new RecordExists(message)
|
|
203
|
-
case PgErrorCode.NotNullViolation:
|
|
204
|
-
return new MisshapedRecord(message)
|
|
205
|
-
case PgErrorCode.ForeignKeyViolation:
|
|
206
|
-
return new PostgresForeignKeyError(message)
|
|
207
|
-
case PgErrorCode.CheckViolation:
|
|
208
|
-
return new PostgresCheckError(message)
|
|
209
|
-
case PgErrorCode.DatatypeMismatch:
|
|
210
|
-
case PgErrorCode.CannotCoerce:
|
|
211
|
-
return new PostgresCastRequired(message)
|
|
212
|
-
case PgErrorCode.SerializationFailure:
|
|
213
|
-
case PgErrorCode.DeadlockDetected:
|
|
214
|
-
return new PostgresDeadlockError(message)
|
|
215
|
-
default:
|
|
216
|
-
break
|
|
217
|
-
}
|
|
218
|
-
if (driver.code != null && driver.code.startsWith('08')) {
|
|
219
|
-
return new PostgresConnectionError(message)
|
|
220
|
-
}
|
|
221
|
-
if (driver.code != null && driver.code.startsWith('23')) {
|
|
222
|
-
return new PostgresConstraintError(message)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return new PostgresError(message)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const produced = produce()
|
|
229
|
-
/** The *original* error, wrapper and all — unwrapping is for classification, not for loss. */
|
|
230
|
-
produced.cause = error
|
|
231
|
-
|
|
232
|
-
return produced
|
|
233
|
-
}
|
|
234
|
-
|
|
235
105
|
ResilientError.registerErrorClass(PostgresError)
|
|
236
106
|
ResilientError.registerErrorClass(PostgresSyncError)
|
|
237
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
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
import { appendContextual, assertContext } from '@owlmeans/context'
|
|
2
|
-
import {
|
|
3
|
-
MisshapedRecord, RecordExists, RecordUpdateFailed, UnknownRecordError, UnsupportedArgumentError
|
|
4
|
-
} from '@owlmeans/resource'
|
|
5
|
-
import type {
|
|
6
|
-
Criteria, FirstOptions, ListOptions, ListResult, MigrationStage, ResourceRecord, WriteOptions
|
|
7
|
-
} from '@owlmeans/resource'
|
|
8
|
-
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
2
|
+
import { MisshapedRecord, RecordExists, RecordUpdateFailed, UnknownRecordError, UnsupportedArgumentError, type Criteria, type FirstOptions, type ListOptions, type ListResult, type MigrationStage, type ResourceRecord, type WriteOptions } from '@owlmeans/resource'
|
|
9
3
|
import type { AnySchema, JSONSchemaType } from 'ajv'
|
|
10
|
-
import { eq, sql } from 'drizzle-orm'
|
|
11
|
-
import type { SQL } from 'drizzle-orm'
|
|
4
|
+
import { eq, sql, type SQL } from 'drizzle-orm'
|
|
12
5
|
import type { PoolClient, QueryResultRow } from 'pg'
|
|
13
6
|
|
|
14
7
|
import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE, ID_FIELD } from './consts.js'
|
|
15
|
-
import {
|
|
16
|
-
import { pgErrorToResourceError } from './errors.js'
|
|
8
|
+
import { pgDeclarationHelper } from './declarations.js'
|
|
17
9
|
import { getSchemaSecureFeilds } from './helper.js'
|
|
10
|
+
import { pgErrorHelper } from './pg-error.js'
|
|
18
11
|
import type {
|
|
19
12
|
ColumnSpec, PgIndexSpec, PgRuntimeTable, PostgresDb, PostgresDbService, PostgresResource,
|
|
20
13
|
PostgresTx, TableSpec
|
|
21
14
|
} from './types.js'
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
15
|
+
import { pgCriteriaHelper } from './utils/criteria.js'
|
|
16
|
+
import { makePgLifeCycleHelper } from './utils/life-cycle.js'
|
|
17
|
+
import { pgMarshalHelper } from './utils/marshal.js'
|
|
25
18
|
import { makeTx } from './utils/migrations.js'
|
|
26
|
-
import {
|
|
19
|
+
import { pgPlaceholdersOf } from './utils/sql.js'
|
|
20
|
+
import type { Config, Context } from './types.local.js'
|
|
27
21
|
|
|
28
|
-
type Config = ServerConfig
|
|
29
|
-
type Context<C extends Config = Config> = ServerContext<C>
|
|
30
22
|
|
|
31
23
|
/** Postgres has no row expiry — silently ignoring a TTL would lose data. */
|
|
32
24
|
const refuseTtl = (opts?: WriteOptions): void => {
|
|
@@ -42,7 +34,10 @@ export const makePostgresResource = <
|
|
|
42
34
|
tableName?: string
|
|
43
35
|
): T => {
|
|
44
36
|
const location = `postgres-resource:${alias}`
|
|
45
|
-
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
|
|
46
41
|
|
|
47
42
|
/** Set by `init()`; every data method goes through `ensure()`, which guarantees them. */
|
|
48
43
|
let spec: TableSpec | undefined
|
|
@@ -115,7 +110,7 @@ export const makePostgresResource = <
|
|
|
115
110
|
const context = assertContext<Config, Context>(resource.ctx as Context, location)
|
|
116
111
|
try {
|
|
117
112
|
const result = await db.pool.query<Row>(
|
|
118
|
-
resolvePlaceholders(text,
|
|
113
|
+
pgPlaceholdersOf(context).resolvePlaceholders(text, table), params as never[]
|
|
119
114
|
)
|
|
120
115
|
|
|
121
116
|
return { rows: result.rows, count: result.rowCount ?? 0 }
|
|
@@ -405,19 +400,20 @@ export const makePostgresResource = <
|
|
|
405
400
|
throw new SyntaxError(`Postgres resource not initialized: ${alias}`)
|
|
406
401
|
}
|
|
407
402
|
|
|
408
|
-
return refOf(
|
|
403
|
+
return pgPlaceholdersOf(context).refOf(spec, resourceAlias)
|
|
409
404
|
},
|
|
410
405
|
|
|
411
406
|
transaction: async fn => {
|
|
412
407
|
const { db, spec: table } = await ensure()
|
|
413
408
|
const context = assertContext<Config, Context>(resource.ctx as Context, location)
|
|
409
|
+
const placeholders = pgPlaceholdersOf(context)
|
|
414
410
|
const client: PoolClient = await db.pool.connect()
|
|
415
411
|
try {
|
|
416
412
|
await client.query('BEGIN')
|
|
417
413
|
const result = await fn(makeTx(
|
|
418
414
|
client,
|
|
419
|
-
text => resolvePlaceholders(text,
|
|
420
|
-
resourceAlias => refOf(
|
|
415
|
+
text => placeholders.resolvePlaceholders(text, table),
|
|
416
|
+
resourceAlias => placeholders.refOf(table, resourceAlias)
|
|
421
417
|
))
|
|
422
418
|
await client.query('COMMIT')
|
|
423
419
|
|
|
@@ -512,8 +508,8 @@ export const makePostgresResource = <
|
|
|
512
508
|
const db = await postgres.db(dbAlias)
|
|
513
509
|
const config = postgres.config(dbAlias)
|
|
514
510
|
|
|
515
|
-
const initialized = await initializeTable(
|
|
516
|
-
|
|
511
|
+
const initialized = await makePgLifeCycleHelper(db).initializeTable(
|
|
512
|
+
config, resource as unknown as PostgresResource<ResourceRecord>, context,
|
|
517
513
|
task => postgres.defer(dbAlias, task)
|
|
518
514
|
)
|
|
519
515
|
spec = initialized.spec
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
2
|
+
|
|
3
|
+
export interface PgDriverError {
|
|
4
|
+
code?: string
|
|
5
|
+
detail?: string
|
|
6
|
+
hint?: string
|
|
7
|
+
severity?: string
|
|
8
|
+
constraint?: string
|
|
9
|
+
table?: string
|
|
10
|
+
column?: string
|
|
11
|
+
message?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Config = ServerConfig
|
|
15
|
+
|
|
16
|
+
export type Context<C extends Config = Config> = ServerContext<C>
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Criteria, DbLocker, LockableResource, MigratableResource, Resource,
|
|
3
|
-
ResourceDbService, ResourceRecord, WriteOptions
|
|
4
|
-
} from '@owlmeans/resource'
|
|
1
|
+
import type { Criteria, DbLocker, LockableResource, MigratableResource, Resource, ResourceDbService, ResourceRecord, WriteOptions, MigrationRegistry } from '@owlmeans/resource'
|
|
5
2
|
import type { AnySchema } from 'ajv'
|
|
6
3
|
import type { Pool, PoolClient, QueryResultRow } from 'pg'
|
|
7
4
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
|
@@ -258,6 +255,7 @@ export interface PgReferenceSpec {
|
|
|
258
255
|
onUpdate?: PgReferentialAction | `${PgReferentialAction}`
|
|
259
256
|
}
|
|
260
257
|
|
|
258
|
+
// Kept as a type: it merges an index signature over the table's own members, which an interface refuses.
|
|
261
259
|
/**
|
|
262
260
|
* The Drizzle table `specToTable` compiles from a {@link TableSpec}.
|
|
263
261
|
*
|
|
@@ -321,3 +319,9 @@ export interface DdlPlan {
|
|
|
321
319
|
fresh: boolean
|
|
322
320
|
statements: DdlStatement[]
|
|
323
321
|
}
|
|
322
|
+
|
|
323
|
+
export interface PostgresDeclaration {
|
|
324
|
+
schema?: AnySchema
|
|
325
|
+
indexes: PgIndexSpec[]
|
|
326
|
+
migrations: MigrationRegistry<PostgresTx>
|
|
327
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { PgErrorCode } from '../consts.js'
|
|
2
|
+
|
|
3
|
+
export const COMPARISON: Record<string, string> = {
|
|
4
|
+
$eq: '=', $ne: '<>', $gt: '>', $gte: '>=', $lt: '<', $lte: '<='
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** A type cast as `pg_get_expr` / `pg_get_indexdef` print one: `::text`, `::character varying(64)[]`. */
|
|
8
|
+
export const CAST = /::(?:character varying|double precision|bit varying|timestamp with(?:out)? time zone|time with(?:out)? time zone|[a-z_][a-z0-9_]*)(?:\(\d+(?:\s*,\s*\d+)?\))?(?:\[\])?/g
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `pg_attribute` + `format_type` rather than `information_schema.columns`, because it
|
|
12
|
+
* returns one canonical type string with the typmod already baked in
|
|
13
|
+
* (`character varying(320)`). `information_schema` splits the same information across
|
|
14
|
+
* four columns and reports every array as `ARRAY`, which no comparison can use.
|
|
15
|
+
*/
|
|
16
|
+
export const COLUMNS = `
|
|
17
|
+
SELECT a.attname AS name,
|
|
18
|
+
format_type(a.atttypid, a.atttypmod) AS type,
|
|
19
|
+
a.attnotnull AS not_null,
|
|
20
|
+
pg_get_expr(d.adbin, d.adrelid) AS default_expr,
|
|
21
|
+
a.attidentity AS identity,
|
|
22
|
+
a.attgenerated AS generated,
|
|
23
|
+
a.attnum AS ordinal
|
|
24
|
+
FROM pg_attribute a
|
|
25
|
+
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
|
|
26
|
+
WHERE a.attrelid = to_regclass($1) AND a.attnum > 0 AND NOT a.attisdropped
|
|
27
|
+
ORDER BY a.attnum
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
export const INDEXES = `
|
|
31
|
+
SELECT indexname AS name, indexdef AS definition
|
|
32
|
+
FROM pg_indexes
|
|
33
|
+
WHERE schemaname = $1 AND tablename = $2
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
export const CONSTRAINTS = `
|
|
37
|
+
SELECT c.conname AS name, c.contype AS type, pg_get_constraintdef(c.oid) AS definition
|
|
38
|
+
FROM pg_constraint c
|
|
39
|
+
WHERE c.conrelid = to_regclass($1)
|
|
40
|
+
AND c.contype <> 'n'
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
export const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `{{ }}` / `{{self}}` — the owning resource's qualified table
|
|
47
|
+
* `{{alias}}` — another registered postgres resource's qualified table
|
|
48
|
+
* `{{alias.property}}` — a qualified column of that resource
|
|
49
|
+
* `{{#alias}}` — the bare quoted table name, for `ON CONFLICT ON CONSTRAINT`
|
|
50
|
+
* `{{$}}` — the owning resource's quoted schema
|
|
51
|
+
*
|
|
52
|
+
* Resource aliases in this codebase use `-` and `:` but never `.`, so `.` is unambiguous
|
|
53
|
+
* as the column separator.
|
|
54
|
+
*/
|
|
55
|
+
export const PLACEHOLDER = /\{\{\s*([#$]?)([A-Za-z0-9_:-]*)(?:\.([A-Za-z0-9_]+))?\s*\}\}/g
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Codes that mean "the cast is legal, the data isn't": a `text` column holding `'a'` retyped to
|
|
59
|
+
* `integer`, a value too wide for the new length, a number past the new range. Postgres refuses
|
|
60
|
+
* each of these rather than truncating, and the remedy is the same one a refused cast needs — so
|
|
61
|
+
* they are only read as a cast problem here, inside DDL. On the CRUD path the identical code means
|
|
62
|
+
* a caller passed a bad value, which is a different bug with a different fix.
|
|
63
|
+
*/
|
|
64
|
+
export const CAST_DATA_CODES: string[] = [
|
|
65
|
+
PgErrorCode.InvalidTextRepresentation,
|
|
66
|
+
PgErrorCode.StringDataRightTruncation,
|
|
67
|
+
PgErrorCode.NumericValueOutOfRange
|
|
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
|
+
}
|