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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +13 -13
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/postgres-resource/SKILL.md +2 -2
  4. package/build/consts.d.ts +9 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +6 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/declarations/types.d.ts +12 -0
  9. package/build/declarations/types.d.ts.map +1 -0
  10. package/build/declarations/types.js +2 -0
  11. package/build/declarations/types.js.map +1 -0
  12. package/build/declarations.d.ts +6 -9
  13. package/build/declarations.d.ts.map +1 -1
  14. package/build/declarations.js +25 -15
  15. package/build/declarations.js.map +1 -1
  16. package/build/errors.d.ts +0 -20
  17. package/build/errors.d.ts.map +1 -1
  18. package/build/errors.js +1 -111
  19. package/build/errors.js.map +1 -1
  20. package/build/index.d.ts +3 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +1 -0
  23. package/build/index.js.map +1 -1
  24. package/build/pg-error/types.d.ts +24 -0
  25. package/build/pg-error/types.d.ts.map +1 -0
  26. package/build/pg-error/types.js +2 -0
  27. package/build/pg-error/types.js.map +1 -0
  28. package/build/pg-error.d.ts +4 -0
  29. package/build/pg-error.d.ts.map +1 -0
  30. package/build/pg-error.js +100 -0
  31. package/build/pg-error.js.map +1 -0
  32. package/build/resource.d.ts +1 -1
  33. package/build/resource.d.ts.map +1 -1
  34. package/build/resource.js +15 -11
  35. package/build/resource.js.map +1 -1
  36. package/build/types.d.ts +6 -1
  37. package/build/types.d.ts.map +1 -1
  38. package/build/types.local.d.ts +14 -0
  39. package/build/types.local.d.ts.map +1 -0
  40. package/build/types.local.js +2 -0
  41. package/build/types.local.js.map +1 -0
  42. package/build/utils/consts.local.d.ts +35 -0
  43. package/build/utils/consts.local.d.ts.map +1 -0
  44. package/build/utils/consts.local.js +63 -0
  45. package/build/utils/consts.local.js.map +1 -0
  46. package/build/utils/criteria/types.d.ts +31 -0
  47. package/build/utils/criteria/types.d.ts.map +1 -0
  48. package/build/utils/criteria/types.js +2 -0
  49. package/build/utils/criteria/types.js.map +1 -0
  50. package/build/utils/criteria.d.ts +3 -27
  51. package/build/utils/criteria.d.ts.map +1 -1
  52. package/build/utils/criteria.js +88 -107
  53. package/build/utils/criteria.js.map +1 -1
  54. package/build/utils/diff/types.d.ts +40 -0
  55. package/build/utils/diff/types.d.ts.map +1 -0
  56. package/build/utils/diff/types.js +2 -0
  57. package/build/utils/diff/types.js.map +1 -0
  58. package/build/utils/diff.d.ts +5 -35
  59. package/build/utils/diff.d.ts.map +1 -1
  60. package/build/utils/diff.js +321 -343
  61. package/build/utils/diff.js.map +1 -1
  62. package/build/utils/index.d.ts +10 -0
  63. package/build/utils/index.d.ts.map +1 -1
  64. package/build/utils/index.js.map +1 -1
  65. package/build/utils/introspect/types.d.ts +9 -0
  66. package/build/utils/introspect/types.d.ts.map +1 -0
  67. package/build/utils/introspect/types.js +2 -0
  68. package/build/utils/introspect/types.js.map +1 -0
  69. package/build/utils/introspect.d.ts +3 -3
  70. package/build/utils/introspect.d.ts.map +1 -1
  71. package/build/utils/introspect.js +42 -67
  72. package/build/utils/introspect.js.map +1 -1
  73. package/build/utils/life-cycle/types.d.ts +37 -0
  74. package/build/utils/life-cycle/types.d.ts.map +1 -0
  75. package/build/utils/life-cycle/types.js +2 -0
  76. package/build/utils/life-cycle/types.js.map +1 -0
  77. package/build/utils/life-cycle.d.ts +3 -38
  78. package/build/utils/life-cycle.d.ts.map +1 -1
  79. package/build/utils/life-cycle.js +90 -107
  80. package/build/utils/life-cycle.js.map +1 -1
  81. package/build/utils/marshal/types.d.ts +39 -0
  82. package/build/utils/marshal/types.d.ts.map +1 -0
  83. package/build/utils/marshal/types.js +2 -0
  84. package/build/utils/marshal/types.js.map +1 -0
  85. package/build/utils/marshal.d.ts +4 -32
  86. package/build/utils/marshal.d.ts.map +1 -1
  87. package/build/utils/marshal.js +124 -141
  88. package/build/utils/marshal.js.map +1 -1
  89. package/build/utils/migrations.d.ts.map +1 -1
  90. package/build/utils/migrations.js +41 -36
  91. package/build/utils/migrations.js.map +1 -1
  92. package/build/utils/name/types.d.ts +43 -0
  93. package/build/utils/name/types.d.ts.map +1 -0
  94. package/build/utils/name/types.js +2 -0
  95. package/build/utils/name/types.js.map +1 -0
  96. package/build/utils/name.d.ts +6 -35
  97. package/build/utils/name.d.ts.map +1 -1
  98. package/build/utils/name.js +40 -59
  99. package/build/utils/name.js.map +1 -1
  100. package/build/utils/schema/types.d.ts +22 -0
  101. package/build/utils/schema/types.d.ts.map +1 -0
  102. package/build/utils/schema/types.js +2 -0
  103. package/build/utils/schema/types.js.map +1 -0
  104. package/build/utils/schema.d.ts +4 -24
  105. package/build/utils/schema.d.ts.map +1 -1
  106. package/build/utils/schema.js +341 -352
  107. package/build/utils/schema.js.map +1 -1
  108. package/build/utils/sql/types.d.ts +25 -0
  109. package/build/utils/sql/types.d.ts.map +1 -0
  110. package/build/utils/sql/types.js +2 -0
  111. package/build/utils/sql/types.js.map +1 -0
  112. package/build/utils/sql.d.ts +5 -27
  113. package/build/utils/sql.d.ts.map +1 -1
  114. package/build/utils/sql.js +0 -0
  115. package/build/utils/sql.js.map +1 -1
  116. package/build/utils/sync/types.d.ts +25 -0
  117. package/build/utils/sync/types.d.ts.map +1 -0
  118. package/build/utils/sync/types.js +2 -0
  119. package/build/utils/sync/types.js.map +1 -0
  120. package/build/utils/sync.d.ts +2 -19
  121. package/build/utils/sync.d.ts.map +1 -1
  122. package/build/utils/sync.js +67 -84
  123. package/build/utils/sync.js.map +1 -1
  124. package/build/utils/types.d.ts +18 -0
  125. package/build/utils/types.d.ts.map +1 -0
  126. package/build/utils/types.js +2 -0
  127. package/build/utils/types.js.map +1 -0
  128. package/build/utils/types.local.d.ts +33 -0
  129. package/build/utils/types.local.d.ts.map +1 -0
  130. package/build/utils/types.local.js +2 -0
  131. package/build/utils/types.local.js.map +1 -0
  132. package/package.json +14 -13
  133. package/src/consts.ts +7 -0
  134. package/src/declarations/types.ts +12 -0
  135. package/src/declarations.ts +30 -23
  136. package/src/errors.ts +1 -131
  137. package/src/index.ts +3 -0
  138. package/src/pg-error/types.ts +23 -0
  139. package/src/pg-error.ts +114 -0
  140. package/src/resource.ts +20 -24
  141. package/src/types.local.ts +16 -0
  142. package/src/types.ts +8 -4
  143. package/src/utils/consts.local.ts +71 -0
  144. package/src/utils/criteria/types.ts +32 -0
  145. package/src/utils/criteria.ts +99 -129
  146. package/src/utils/diff/types.ts +41 -0
  147. package/src/utils/diff.ts +331 -348
  148. package/src/utils/index.ts +10 -0
  149. package/src/utils/introspect/types.ts +9 -0
  150. package/src/utils/introspect.ts +54 -79
  151. package/src/utils/life-cycle/types.ts +41 -0
  152. package/src/utils/life-cycle.ts +108 -134
  153. package/src/utils/marshal/types.ts +40 -0
  154. package/src/utils/marshal.ts +134 -147
  155. package/src/utils/migrations.ts +39 -33
  156. package/src/utils/name/types.ts +44 -0
  157. package/src/utils/name.ts +49 -63
  158. package/src/utils/schema/types.ts +26 -0
  159. package/src/utils/schema.ts +366 -397
  160. package/src/utils/sql/types.ts +25 -0
  161. package/src/utils/sql.ts +0 -0
  162. package/src/utils/sync/types.ts +25 -0
  163. package/src/utils/sync.ts +65 -83
  164. package/src/utils/types.local.ts +35 -0
  165. package/src/utils/types.ts +19 -0
  166. package/tests/criteria.spec.ts +4 -1
  167. package/tests/diff.spec.ts +28 -1
  168. package/tests/errors.spec.ts +4 -3
  169. package/tests/name.spec.ts +5 -3
  170. package/tests/schema.spec.ts +3 -1
  171. package/tests/sql.spec.ts +20 -22
@@ -1,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 { PgIndexSpec, PostgresTx } from './types.js'
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 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
- }
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
- return declaration
32
- }
28
+ return declaration
29
+ }
33
30
 
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
31
+ const resetDeclarations = (alias?: string): void => {
32
+ if (alias == null) {
33
+ declarations.clear()
34
+ return
35
+ }
36
+ declarations.delete(alias)
39
37
  }
40
- declarations.delete(alias)
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
+ }
@@ -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 { getDeclaration } from './declarations.js'
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 { criteriaToSql, sortToSql } from './utils/criteria.js'
23
- import { initializeTable } from './utils/life-cycle.js'
24
- import { recordToFullValues, recordToValues, resultToRecord, rowToRecord } from './utils/marshal.js'
15
+ import { pgCriteriaHelper } from './utils/criteria.js'
16
+ import { makePgLifeCycleHelper } from './utils/life-cycle.js'
17
+ import { pgMarshalHelper } from './utils/marshal.js'
25
18
  import { makeTx } from './utils/migrations.js'
26
- import { refOf, resolvePlaceholders } from './utils/sql.js'
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, context, table), params as never[]
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(context, spec, resourceAlias)
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, context, table),
420
- resourceAlias => refOf(context, table, resourceAlias)
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
- db, config, resource as unknown as PostgresResource<ResourceRecord>, context,
511
+ const initialized = await makePgLifeCycleHelper(db).initializeTable(
512
+ config, resource as unknown as PostgresResource<ResourceRecord>, context,
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
+ }