@owlmeans/postgres-resource 0.1.15
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/LICENSE +21 -0
- package/README.md +219 -0
- package/agent-meta/instructions/postgres-resource.instructions.md +60 -0
- package/agent-meta/manifest.json +23 -0
- package/agent-meta/skills/postgres-resource/SKILL.md +186 -0
- package/build/consts.d.ts +81 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +86 -0
- package/build/consts.js.map +1 -0
- package/build/declarations.d.ts +12 -0
- package/build/declarations.d.ts.map +1 -0
- package/build/declarations.js +28 -0
- package/build/declarations.js.map +1 -0
- package/build/errors.d.ts +70 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +201 -0
- package/build/errors.js.map +1 -0
- package/build/helper.d.ts +4 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +5 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resource.d.ts +4 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +395 -0
- package/build/resource.js.map +1 -0
- package/build/types.d.ts +273 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/criteria.d.ts +24 -0
- package/build/utils/criteria.d.ts.map +1 -0
- package/build/utils/criteria.js +209 -0
- package/build/utils/criteria.js.map +1 -0
- package/build/utils/diff.d.ts +24 -0
- package/build/utils/diff.d.ts.map +1 -0
- package/build/utils/diff.js +330 -0
- package/build/utils/diff.js.map +1 -0
- package/build/utils/index.d.ts +12 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +12 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/introspect.d.ts +7 -0
- package/build/utils/introspect.d.ts.map +1 -0
- package/build/utils/introspect.js +70 -0
- package/build/utils/introspect.js.map +1 -0
- package/build/utils/life-cycle.d.ts +39 -0
- package/build/utils/life-cycle.d.ts.map +1 -0
- package/build/utils/life-cycle.js +119 -0
- package/build/utils/life-cycle.js.map +1 -0
- package/build/utils/marshal.d.ts +36 -0
- package/build/utils/marshal.d.ts.map +1 -0
- package/build/utils/marshal.js +152 -0
- package/build/utils/marshal.js.map +1 -0
- package/build/utils/migrations.d.ts +16 -0
- package/build/utils/migrations.d.ts.map +1 -0
- package/build/utils/migrations.js +108 -0
- package/build/utils/migrations.js.map +1 -0
- package/build/utils/name.d.ts +39 -0
- package/build/utils/name.d.ts.map +1 -0
- package/build/utils/name.js +63 -0
- package/build/utils/name.js.map +1 -0
- package/build/utils/schema.d.ts +28 -0
- package/build/utils/schema.d.ts.map +1 -0
- package/build/utils/schema.js +356 -0
- package/build/utils/schema.js.map +1 -0
- package/build/utils/sql.d.ts +22 -0
- package/build/utils/sql.d.ts.map +1 -0
- package/build/utils/sql.js +0 -0
- package/build/utils/sql.js.map +1 -0
- package/build/utils/sync.d.ts +21 -0
- package/build/utils/sync.d.ts.map +1 -0
- package/build/utils/sync.js +93 -0
- package/build/utils/sync.js.map +1 -0
- package/build/utils/table.d.ts +8 -0
- package/build/utils/table.d.ts.map +1 -0
- package/build/utils/table.js +68 -0
- package/build/utils/table.js.map +1 -0
- package/package.json +50 -0
- package/src/consts.ts +95 -0
- package/src/declarations.ts +41 -0
- package/src/errors.ts +242 -0
- package/src/helper.ts +7 -0
- package/src/index.ts +7 -0
- package/src/resource.ts +524 -0
- package/src/types.ts +310 -0
- package/src/utils/criteria.ts +246 -0
- package/src/utils/diff.ts +363 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/introspect.ts +87 -0
- package/src/utils/life-cycle.ts +155 -0
- package/src/utils/marshal.ts +175 -0
- package/src/utils/migrations.ts +135 -0
- package/src/utils/name.ts +79 -0
- package/src/utils/schema.ts +426 -0
- package/src/utils/sql.ts +0 -0
- package/src/utils/sync.ts +106 -0
- package/src/utils/table.ts +76 -0
- package/tests/errors.spec.ts +121 -0
- package/tests/name.spec.ts +75 -0
- package/tests/schema.spec.ts +258 -0
- package/tests/sql.spec.ts +104 -0
- package/tsconfig.json +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/postgres-resource",
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -b",
|
|
8
|
+
"dev": "sleep 204 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
9
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
10
|
+
"test": "bun test ./tests"
|
|
11
|
+
},
|
|
12
|
+
"main": "build/index.js",
|
|
13
|
+
"module": "build/index.js",
|
|
14
|
+
"types": "build/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./build/index.js",
|
|
18
|
+
"require": "./build/index.js",
|
|
19
|
+
"default": "./build/index.js",
|
|
20
|
+
"module": "./build/index.js",
|
|
21
|
+
"types": "./build/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"ajv": "*",
|
|
26
|
+
"pg": "*"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@noble/hashes": "^1.5.0",
|
|
30
|
+
"@owlmeans/basic-ids": "^0.1.15",
|
|
31
|
+
"@owlmeans/context": "^0.1.15",
|
|
32
|
+
"@owlmeans/resource": "^0.1.15",
|
|
33
|
+
"@owlmeans/server-context": "^0.1.15",
|
|
34
|
+
"@scure/base": "^1.1.9",
|
|
35
|
+
"drizzle-orm": "~0.45.2"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
39
|
+
"@owlmeans/test-integration": "^0.1.15",
|
|
40
|
+
"@types/bun": "^1.3.14",
|
|
41
|
+
"@types/node": "^26.1.0",
|
|
42
|
+
"@types/pg": "^8.20.4",
|
|
43
|
+
"nodemon": "^3.1.14",
|
|
44
|
+
"pg": "^8.22.0",
|
|
45
|
+
"typescript": "^6.0.3"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
export const DEFAULT_DB_ALIAS = 'postgres'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PAGE_SIZE = 10
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Table that records which code-registered migrations have already been applied.
|
|
8
|
+
* Lives in the same Postgres schema as the resources it tracks, so dropping the
|
|
9
|
+
* schema drops the ledger with it.
|
|
10
|
+
*/
|
|
11
|
+
export const DEF_MIGRATIONS_TABLE = '_owlmeans_migrations'
|
|
12
|
+
|
|
13
|
+
/** JSON Schema keyword carrying the Postgres specific overrides. */
|
|
14
|
+
export const PG_KEYWORD = 'pg'
|
|
15
|
+
|
|
16
|
+
/** Postgres `NAMEDATALEN - 1`. Identifiers past this are silently truncated by the server. */
|
|
17
|
+
export const PG_MAX_IDENTIFIER = 63
|
|
18
|
+
|
|
19
|
+
/** Property name that carries the record identity across every OwlMeans resource. */
|
|
20
|
+
export const ID_FIELD = 'id'
|
|
21
|
+
|
|
22
|
+
/** Fallback when a scalar property can't be mapped to anything more specific. */
|
|
23
|
+
export const DEF_SQL_TYPE = 'text'
|
|
24
|
+
|
|
25
|
+
export const DEF_JSON_TYPE = 'jsonb'
|
|
26
|
+
|
|
27
|
+
/** Server side identity default for the implicit `id` primary key. */
|
|
28
|
+
export const DEF_ID_DEFAULT = 'gen_random_uuid()::text'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How far structure reconciliation is allowed to go.
|
|
32
|
+
*
|
|
33
|
+
* `additive` is the adoption path for a table this package didn't create: it adds what's
|
|
34
|
+
* missing but never retypes and never drops, so a first boot against an existing schema
|
|
35
|
+
* converges without touching data. Flip to `full` once the plan comes out empty.
|
|
36
|
+
*/
|
|
37
|
+
export enum PgAutoSync {
|
|
38
|
+
Full = 'full',
|
|
39
|
+
Additive = 'additive',
|
|
40
|
+
Off = 'off'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export enum PgIndexMethod {
|
|
44
|
+
BTree = 'btree',
|
|
45
|
+
Hash = 'hash',
|
|
46
|
+
Gin = 'gin',
|
|
47
|
+
Gist = 'gist',
|
|
48
|
+
Brin = 'brin',
|
|
49
|
+
SpGist = 'spgist'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export enum PgReferentialAction {
|
|
53
|
+
NoAction = 'no action',
|
|
54
|
+
Restrict = 'restrict',
|
|
55
|
+
Cascade = 'cascade',
|
|
56
|
+
SetNull = 'set null',
|
|
57
|
+
SetDefault = 'set default'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Postgres error codes the resource layer translates into framework errors. Sourced
|
|
62
|
+
* from the `pg` driver's `DatabaseError.code`.
|
|
63
|
+
*/
|
|
64
|
+
export enum PgErrorCode {
|
|
65
|
+
UniqueViolation = '23505',
|
|
66
|
+
ForeignKeyViolation = '23503',
|
|
67
|
+
NotNullViolation = '23502',
|
|
68
|
+
CheckViolation = '23514',
|
|
69
|
+
UndefinedTable = '42P01',
|
|
70
|
+
UndefinedColumn = '42703',
|
|
71
|
+
DuplicateTable = '42P07',
|
|
72
|
+
DuplicateColumn = '42701',
|
|
73
|
+
DuplicateObject = '42710',
|
|
74
|
+
CannotCoerce = '42846',
|
|
75
|
+
DatatypeMismatch = '42804',
|
|
76
|
+
InvalidTextRepresentation = '22P02',
|
|
77
|
+
StringDataRightTruncation = '22001',
|
|
78
|
+
NumericValueOutOfRange = '22003',
|
|
79
|
+
SerializationFailure = '40001',
|
|
80
|
+
DeadlockDetected = '40P01',
|
|
81
|
+
NoActiveTransaction = '25001'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Postgres reports scalar values of these types as strings to avoid precision loss.
|
|
86
|
+
* The marshaller converts them back using the resource's schema.
|
|
87
|
+
*/
|
|
88
|
+
export const STRING_RETURNING_TYPES = ['numeric', 'bigint', 'int8', 'decimal', 'money'] as const
|
|
89
|
+
|
|
90
|
+
/** Built-in type OIDs the marshaller has to name to reach the driver's own parsers. */
|
|
91
|
+
export enum PgTypeOid {
|
|
92
|
+
Date = 1082,
|
|
93
|
+
Timestamp = 1114,
|
|
94
|
+
TimestampTz = 1184
|
|
95
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createMigrationRegistry } from '@owlmeans/resource'
|
|
2
|
+
import type { MigrationRegistry } from '@owlmeans/resource'
|
|
3
|
+
import type { AnySchema } from 'ajv'
|
|
4
|
+
|
|
5
|
+
import type { PgIndexSpec, PostgresTx } from './types.js'
|
|
6
|
+
|
|
7
|
+
export interface PostgresDeclaration {
|
|
8
|
+
schema?: AnySchema
|
|
9
|
+
indexes: PgIndexSpec[]
|
|
10
|
+
migrations: MigrationRegistry<PostgresTx>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Per-alias declaration store, held at module scope rather than on the resource object.
|
|
15
|
+
*
|
|
16
|
+
* `reinitializeContext` rebuilds every resource, which drops anything a caller attached
|
|
17
|
+
* by chaining. Mongo lives with that by requiring the app to pass `makeCustomResource`
|
|
18
|
+
* and re-run the whole maker; migrations can't depend on that discipline, because losing
|
|
19
|
+
* one silently means a data transformation never runs. Keying the declarations by alias
|
|
20
|
+
* makes them survive any number of context switches.
|
|
21
|
+
*/
|
|
22
|
+
const declarations: Map<string, PostgresDeclaration> = new Map()
|
|
23
|
+
|
|
24
|
+
export const getDeclaration = (alias: string): PostgresDeclaration => {
|
|
25
|
+
let declaration = declarations.get(alias)
|
|
26
|
+
if (declaration == null) {
|
|
27
|
+
declaration = { indexes: [], migrations: createMigrationRegistry<PostgresTx>() }
|
|
28
|
+
declarations.set(alias, declaration)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return declaration
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Testing seam — drops every declaration so a spec can redeclare a resource from scratch. */
|
|
35
|
+
export const resetDeclarations = (alias?: string): void => {
|
|
36
|
+
if (alias == null) {
|
|
37
|
+
declarations.clear()
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
declarations.delete(alias)
|
|
41
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error'
|
|
2
|
+
import {
|
|
3
|
+
MisshapedRecord, RecordExists, ResourceError
|
|
4
|
+
} from '@owlmeans/resource'
|
|
5
|
+
|
|
6
|
+
import { PgErrorCode } from './consts.js'
|
|
7
|
+
|
|
8
|
+
export class PostgresError extends ResourceError {
|
|
9
|
+
public static override typeName = `${ResourceError.typeName}PostgresError`
|
|
10
|
+
|
|
11
|
+
constructor(message: string = 'error') {
|
|
12
|
+
super(`postgres:${message}`)
|
|
13
|
+
this.type = PostgresError.typeName
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Structure reconciliation failed. The message carries the offending statement. */
|
|
18
|
+
export class PostgresSyncError extends PostgresError {
|
|
19
|
+
public static override typeName = `${PostgresError.typeName}SyncError`
|
|
20
|
+
|
|
21
|
+
constructor(message: string) {
|
|
22
|
+
super(`sync:${message}`)
|
|
23
|
+
this.type = PostgresSyncError.typeName
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Postgres refused to cast a column to its new type. Fix it with `pg: { using: '<expr>' }`
|
|
29
|
+
* on the property, or with a `pre` migration that performs the ALTER by hand — after
|
|
30
|
+
* which reconciliation sees no drift and emits nothing.
|
|
31
|
+
*/
|
|
32
|
+
export class PostgresCastRequired extends PostgresSyncError {
|
|
33
|
+
public static override typeName = `${PostgresSyncError.typeName}CastRequired`
|
|
34
|
+
|
|
35
|
+
constructor(message: string) {
|
|
36
|
+
super(`cast-required:${message}`)
|
|
37
|
+
this.type = PostgresCastRequired.typeName
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class PostgresConstraintError extends PostgresError {
|
|
42
|
+
public static override typeName = `${PostgresError.typeName}ConstraintError`
|
|
43
|
+
|
|
44
|
+
constructor(message: string) {
|
|
45
|
+
super(`constraint:${message}`)
|
|
46
|
+
this.type = PostgresConstraintError.typeName
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class PostgresForeignKeyError extends PostgresConstraintError {
|
|
51
|
+
public static override typeName = `${PostgresConstraintError.typeName}ForeignKey`
|
|
52
|
+
|
|
53
|
+
constructor(message: string) {
|
|
54
|
+
super(`foreign-key:${message}`)
|
|
55
|
+
this.type = PostgresForeignKeyError.typeName
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class PostgresCheckError extends PostgresConstraintError {
|
|
60
|
+
public static override typeName = `${PostgresConstraintError.typeName}Check`
|
|
61
|
+
|
|
62
|
+
constructor(message: string) {
|
|
63
|
+
super(`check:${message}`)
|
|
64
|
+
this.type = PostgresCheckError.typeName
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Serialization failure or deadlock — the caller may retry the whole transaction. */
|
|
69
|
+
export class PostgresDeadlockError extends PostgresError {
|
|
70
|
+
public static override typeName = `${PostgresError.typeName}DeadlockError`
|
|
71
|
+
|
|
72
|
+
public readonly retryable: boolean = true
|
|
73
|
+
|
|
74
|
+
constructor(message: string) {
|
|
75
|
+
super(`deadlock:${message}`)
|
|
76
|
+
this.type = PostgresDeadlockError.typeName
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A `{{alias}}` placeholder in custom SQL couldn't be resolved. */
|
|
81
|
+
export class PostgresPlaceholderError extends PostgresError {
|
|
82
|
+
public static override typeName = `${PostgresError.typeName}PlaceholderError`
|
|
83
|
+
|
|
84
|
+
constructor(message: string) {
|
|
85
|
+
super(`placeholder:${message}`)
|
|
86
|
+
this.type = PostgresPlaceholderError.typeName
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class PostgresConnectionError extends PostgresError {
|
|
91
|
+
public static override typeName = `${PostgresError.typeName}ConnectionError`
|
|
92
|
+
|
|
93
|
+
constructor(message: string) {
|
|
94
|
+
super(`connection:${message}`)
|
|
95
|
+
this.type = PostgresConnectionError.typeName
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The least-privilege admin path failed. */
|
|
100
|
+
export class PostgresBootstrapError extends PostgresError {
|
|
101
|
+
public static override typeName = `${PostgresError.typeName}BootstrapError`
|
|
102
|
+
|
|
103
|
+
constructor(message: string) {
|
|
104
|
+
super(`bootstrap:${message}`)
|
|
105
|
+
this.type = PostgresBootstrapError.typeName
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
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
|
+
*/
|
|
187
|
+
export const pgErrorToResourceError = (error: unknown): Error => {
|
|
188
|
+
if (error instanceof ResilientError) {
|
|
189
|
+
return error
|
|
190
|
+
}
|
|
191
|
+
const driver = unwrap(error)
|
|
192
|
+
if (!isDriverError(driver)) {
|
|
193
|
+
return error instanceof Error ? error : new PostgresError(`${error}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const message = describe(driver)
|
|
197
|
+
const produce = (): Error => {
|
|
198
|
+
switch (driver.code) {
|
|
199
|
+
case PgErrorCode.UniqueViolation:
|
|
200
|
+
return new RecordExists(message)
|
|
201
|
+
case PgErrorCode.NotNullViolation:
|
|
202
|
+
return new MisshapedRecord(message)
|
|
203
|
+
case PgErrorCode.ForeignKeyViolation:
|
|
204
|
+
return new PostgresForeignKeyError(message)
|
|
205
|
+
case PgErrorCode.CheckViolation:
|
|
206
|
+
return new PostgresCheckError(message)
|
|
207
|
+
case PgErrorCode.DatatypeMismatch:
|
|
208
|
+
case PgErrorCode.CannotCoerce:
|
|
209
|
+
return new PostgresCastRequired(message)
|
|
210
|
+
case PgErrorCode.SerializationFailure:
|
|
211
|
+
case PgErrorCode.DeadlockDetected:
|
|
212
|
+
return new PostgresDeadlockError(message)
|
|
213
|
+
default:
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
if (driver.code != null && driver.code.startsWith('08')) {
|
|
217
|
+
return new PostgresConnectionError(message)
|
|
218
|
+
}
|
|
219
|
+
if (driver.code != null && driver.code.startsWith('23')) {
|
|
220
|
+
return new PostgresConstraintError(message)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return new PostgresError(message)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const produced = produce()
|
|
227
|
+
/** The *original* error, wrapper and all — unwrapping is for classification, not for loss. */
|
|
228
|
+
produced.cause = error
|
|
229
|
+
|
|
230
|
+
return produced
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
ResilientError.registerErrorClass(PostgresError)
|
|
234
|
+
ResilientError.registerErrorClass(PostgresSyncError)
|
|
235
|
+
ResilientError.registerErrorClass(PostgresCastRequired)
|
|
236
|
+
ResilientError.registerErrorClass(PostgresConstraintError)
|
|
237
|
+
ResilientError.registerErrorClass(PostgresForeignKeyError)
|
|
238
|
+
ResilientError.registerErrorClass(PostgresCheckError)
|
|
239
|
+
ResilientError.registerErrorClass(PostgresDeadlockError)
|
|
240
|
+
ResilientError.registerErrorClass(PostgresPlaceholderError)
|
|
241
|
+
ResilientError.registerErrorClass(PostgresConnectionError)
|
|
242
|
+
ResilientError.registerErrorClass(PostgresBootstrapError)
|
package/src/helper.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AnySchema, JSONSchemaType } from 'ajv'
|
|
2
|
+
|
|
3
|
+
/** Properties marked `secure: true` — the fields `lock()`/`unlock()` operate on by default. */
|
|
4
|
+
export const getSchemaSecureFeilds = (schema: AnySchema): string[] =>
|
|
5
|
+
Object.entries((schema as JSONSchemaType<any>)?.properties ?? {})
|
|
6
|
+
.filter(([, property]) => (property as { secure?: boolean }).secure === true)
|
|
7
|
+
.map(([key]) => key)
|