@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/utils/schema.ts
CHANGED
|
@@ -1,454 +1,423 @@
|
|
|
1
1
|
import { UnsupportedArgumentError } from '@owlmeans/resource'
|
|
2
2
|
import type { AnySchema } from 'ajv'
|
|
3
|
+
import { logger } from '@owlmeans/log'
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
DEF_ID_DEFAULT, DEF_JSON_TYPE, DEF_SQL_TYPE, ID_FIELD, PG_KEYWORD, PgIndexMethod
|
|
6
7
|
} from '../consts.js'
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from '
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
8
|
+
import type { ColumnJsonType, ColumnSpec, PgCheckSpec, PgIndexSpec, PgPropertyOverride, PgReferenceSpec, PgUniqueSpec, TableSpec } from '../types.js'
|
|
9
|
+
import { pgNameHelper } from './name.js'
|
|
10
|
+
import { INT32_MAX } from './consts.local.js'
|
|
11
|
+
import type { PgSchemaHelper } from './schema/types.js'
|
|
12
|
+
import type { RawProperty, RawSchema } from './types.local.js'
|
|
13
|
+
|
|
14
|
+
const log = logger('postgres-resource')
|
|
15
|
+
|
|
16
|
+
export const createPgSchemaHelper = (): PgSchemaHelper => {
|
|
17
|
+
const { pgIdentifier, qualify, quoteIdent } = pgNameHelper
|
|
18
|
+
|
|
19
|
+
const toFormatType = (type: string): string => {
|
|
20
|
+
const trimmed = type.trim().toLowerCase()
|
|
21
|
+
const match = /^([a-z0-9_ ]+?)\s*(\(([^)]*)\))?\s*(\[\])?$/.exec(trimmed)
|
|
22
|
+
if (match == null) {
|
|
23
|
+
return trimmed
|
|
24
|
+
}
|
|
25
|
+
const [, base, , args, array] = match
|
|
26
|
+
const aliases: Record<string, string> = {
|
|
27
|
+
int: 'integer', int4: 'integer', int2: 'smallint', int8: 'bigint', serial: 'integer',
|
|
28
|
+
bool: 'boolean', float4: 'real', float8: 'double precision', decimal: 'numeric',
|
|
29
|
+
varchar: 'character varying', char: 'character', bpchar: 'character',
|
|
30
|
+
timestamptz: 'timestamp with time zone', timetz: 'time with time zone',
|
|
31
|
+
timestamp: 'timestamp without time zone', time: 'time without time zone'
|
|
32
|
+
}
|
|
33
|
+
const resolved = aliases[base.trim()] ?? base.trim()
|
|
34
|
+
/** Postgres prints multi-argument typmods without spaces: `numeric(12,2)`. */
|
|
35
|
+
const suffix = args != null ? `(${args.split(',').map(part => part.trim()).join(',')})` : ''
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Normalize a Postgres type name to the spelling `format_type()` reports, so the drift
|
|
38
|
-
* comparison against a live table is an exact string match instead of a fuzzy one.
|
|
39
|
-
*/
|
|
40
|
-
export const toFormatType = (type: string): string => {
|
|
41
|
-
const trimmed = type.trim().toLowerCase()
|
|
42
|
-
const match = /^([a-z0-9_ ]+?)\s*(\(([^)]*)\))?\s*(\[\])?$/.exec(trimmed)
|
|
43
|
-
if (match == null) {
|
|
44
|
-
return trimmed
|
|
45
|
-
}
|
|
46
|
-
const [, base, , args, array] = match
|
|
47
|
-
const aliases: Record<string, string> = {
|
|
48
|
-
int: 'integer', int4: 'integer', int2: 'smallint', int8: 'bigint', serial: 'integer',
|
|
49
|
-
bool: 'boolean', float4: 'real', float8: 'double precision', decimal: 'numeric',
|
|
50
|
-
varchar: 'character varying', char: 'character', bpchar: 'character',
|
|
51
|
-
timestamptz: 'timestamp with time zone', timetz: 'time with time zone',
|
|
52
|
-
timestamp: 'timestamp without time zone', time: 'time without time zone'
|
|
37
|
+
return `${resolved}${suffix}${array ?? ''}`
|
|
53
38
|
}
|
|
54
|
-
const resolved = aliases[base.trim()] ?? base.trim()
|
|
55
|
-
/** Postgres prints multi-argument typmods without spaces: `numeric(12,2)`. */
|
|
56
|
-
const suffix = args != null ? `(${args.split(',').map(part => part.trim()).join(',')})` : ''
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
}
|
|
40
|
+
const readOverride = (property: RawProperty): PgPropertyOverride => property[PG_KEYWORD] ?? {}
|
|
60
41
|
|
|
61
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Nullability is decided in exactly one place and threaded down from there. Mongo's mapper
|
|
44
|
+
* recomputes it per branch and lost it twice — on `date-time` and on optional objects.
|
|
45
|
+
*/
|
|
46
|
+
const isNullable = (property: RawProperty, isRequired: boolean): boolean => {
|
|
47
|
+
const override = readOverride(property)
|
|
48
|
+
if (override.nullable != null) {
|
|
49
|
+
return override.nullable
|
|
50
|
+
}
|
|
51
|
+
if (property.nullable === true) {
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(property.type) && property.type.includes('null')) {
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
* Nullability is decided in exactly one place and threaded down from there. Mongo's mapper
|
|
65
|
-
* recomputes it per branch and lost it twice — on `date-time` and on optional objects.
|
|
66
|
-
*/
|
|
67
|
-
const isNullable = (property: RawProperty, isRequired: boolean): boolean => {
|
|
68
|
-
const override = readOverride(property)
|
|
69
|
-
if (override.nullable != null) {
|
|
70
|
-
return override.nullable
|
|
71
|
-
}
|
|
72
|
-
if (property.nullable === true) {
|
|
73
|
-
return true
|
|
74
|
-
}
|
|
75
|
-
if (Array.isArray(property.type) && property.type.includes('null')) {
|
|
76
|
-
return true
|
|
58
|
+
return !isRequired
|
|
77
59
|
}
|
|
78
60
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const baseType = (property: RawProperty): string | undefined =>
|
|
83
|
-
Array.isArray(property.type) ? property.type.find(type => type !== 'null') : property.type
|
|
84
|
-
|
|
85
|
-
const scalarSqlType = (property: RawProperty): { sqlType: string, jsonType: ColumnJsonType } => {
|
|
86
|
-
const override = readOverride(property)
|
|
87
|
-
const type = baseType(property)
|
|
88
|
-
|
|
89
|
-
switch (type) {
|
|
90
|
-
case 'string':
|
|
91
|
-
switch (property.format) {
|
|
92
|
-
case 'date-time': return { sqlType: 'timestamp with time zone', jsonType: 'date' }
|
|
93
|
-
case 'date': return { sqlType: 'date', jsonType: 'date' }
|
|
94
|
-
case 'time': return { sqlType: 'time without time zone', jsonType: 'string' }
|
|
95
|
-
case 'uuid': return { sqlType: 'uuid', jsonType: 'string' }
|
|
96
|
-
case 'binary': return { sqlType: 'bytea', jsonType: 'binary' }
|
|
97
|
-
default: break
|
|
98
|
-
}
|
|
99
|
-
return property.maxLength != null
|
|
100
|
-
? { sqlType: `character varying(${property.maxLength})`, jsonType: 'string' }
|
|
101
|
-
: { sqlType: DEF_SQL_TYPE, jsonType: 'string' }
|
|
102
|
-
case 'integer':
|
|
103
|
-
return property.maximum != null && property.maximum > INT32_MAX
|
|
104
|
-
? { sqlType: 'bigint', jsonType: 'bigint' }
|
|
105
|
-
: { sqlType: 'integer', jsonType: 'integer' }
|
|
106
|
-
case 'number':
|
|
107
|
-
return override.precision != null
|
|
108
|
-
? { sqlType: `numeric(${override.precision},${override.scale ?? 0})`, jsonType: 'number' }
|
|
109
|
-
: { sqlType: 'double precision', jsonType: 'number' }
|
|
110
|
-
case 'boolean':
|
|
111
|
-
return { sqlType: 'boolean', jsonType: 'boolean' }
|
|
112
|
-
case 'object':
|
|
113
|
-
/**
|
|
114
|
-
* The framework's date convention — `{ type: 'object', format: 'date-time' }`. Mongo's
|
|
115
|
-
* mapper broke twice on exactly this branch by dropping nullability, so nullability is
|
|
116
|
-
* decided by the caller, never here.
|
|
117
|
-
*/
|
|
118
|
-
return property.format === 'date-time'
|
|
119
|
-
? { sqlType: 'timestamp with time zone', jsonType: 'date' }
|
|
120
|
-
: { sqlType: DEF_JSON_TYPE, jsonType: 'object' }
|
|
121
|
-
default:
|
|
122
|
-
return { sqlType: DEF_JSON_TYPE, jsonType: 'unknown' }
|
|
123
|
-
}
|
|
124
|
-
}
|
|
61
|
+
const baseType = (property: RawProperty): string | undefined =>
|
|
62
|
+
Array.isArray(property.type) ? property.type.find(type => type !== 'null') : property.type
|
|
125
63
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
64
|
+
const scalarSqlType = (property: RawProperty): { sqlType: string, jsonType: ColumnJsonType } => {
|
|
65
|
+
const override = readOverride(property)
|
|
66
|
+
const type = baseType(property)
|
|
67
|
+
|
|
68
|
+
switch (type) {
|
|
69
|
+
case 'string':
|
|
70
|
+
switch (property.format) {
|
|
71
|
+
case 'date-time': return { sqlType: 'timestamp with time zone', jsonType: 'date' }
|
|
72
|
+
case 'date': return { sqlType: 'date', jsonType: 'date' }
|
|
73
|
+
case 'time': return { sqlType: 'time without time zone', jsonType: 'string' }
|
|
74
|
+
case 'uuid': return { sqlType: 'uuid', jsonType: 'string' }
|
|
75
|
+
case 'binary': return { sqlType: 'bytea', jsonType: 'binary' }
|
|
76
|
+
default: break
|
|
77
|
+
}
|
|
78
|
+
return property.maxLength != null
|
|
79
|
+
? { sqlType: `character varying(${property.maxLength})`, jsonType: 'string' }
|
|
80
|
+
: { sqlType: DEF_SQL_TYPE, jsonType: 'string' }
|
|
81
|
+
case 'integer':
|
|
82
|
+
return property.maximum != null && property.maximum > INT32_MAX
|
|
83
|
+
? { sqlType: 'bigint', jsonType: 'bigint' }
|
|
84
|
+
: { sqlType: 'integer', jsonType: 'integer' }
|
|
85
|
+
case 'number':
|
|
86
|
+
return override.precision != null
|
|
87
|
+
? { sqlType: `numeric(${override.precision},${override.scale ?? 0})`, jsonType: 'number' }
|
|
88
|
+
: { sqlType: 'double precision', jsonType: 'number' }
|
|
89
|
+
case 'boolean':
|
|
90
|
+
return { sqlType: 'boolean', jsonType: 'boolean' }
|
|
91
|
+
case 'object':
|
|
92
|
+
/**
|
|
93
|
+
* The framework's date convention — `{ type: 'object', format: 'date-time' }`. Mongo's
|
|
94
|
+
* mapper broke twice on exactly this branch by dropping nullability, so nullability is
|
|
95
|
+
* decided by the caller, never here.
|
|
96
|
+
*/
|
|
97
|
+
return property.format === 'date-time'
|
|
98
|
+
? { sqlType: 'timestamp with time zone', jsonType: 'date' }
|
|
99
|
+
: { sqlType: DEF_JSON_TYPE, jsonType: 'object' }
|
|
100
|
+
default:
|
|
101
|
+
return { sqlType: DEF_JSON_TYPE, jsonType: 'unknown' }
|
|
102
|
+
}
|
|
134
103
|
}
|
|
135
104
|
|
|
136
|
-
|
|
137
|
-
|
|
105
|
+
const arraySqlType = (property: RawProperty): { sqlType: string, jsonType: ColumnJsonType, array: boolean } => {
|
|
106
|
+
const items = property.items
|
|
107
|
+
const itemType = items != null ? baseType(items) : undefined
|
|
108
|
+
/** Only scalar element types become Postgres arrays; anything else stays jsonb. */
|
|
109
|
+
if (items != null && itemType != null && ['string', 'integer', 'number', 'boolean'].includes(itemType)
|
|
110
|
+
&& items.format == null && items.enum == null) {
|
|
111
|
+
const scalar = scalarSqlType(items)
|
|
112
|
+
return { sqlType: `${scalar.sqlType}[]`, jsonType: 'array', array: true }
|
|
113
|
+
}
|
|
138
114
|
|
|
139
|
-
|
|
140
|
-
name: string, property: RawProperty, required: string[]
|
|
141
|
-
): ColumnSpec => {
|
|
142
|
-
const override = readOverride(property)
|
|
143
|
-
const secure = property.secure === true
|
|
144
|
-
const type = baseType(property)
|
|
145
|
-
const nullable = isNullable(property, required.includes(name))
|
|
146
|
-
|
|
147
|
-
let sqlType: string
|
|
148
|
-
let jsonType: ColumnJsonType
|
|
149
|
-
let array = false
|
|
150
|
-
|
|
151
|
-
if (override.type != null) {
|
|
152
|
-
sqlType = override.type
|
|
153
|
-
jsonType = secure ? 'string' : scalarSqlType(property).jsonType
|
|
154
|
-
array = override.array === true || override.type.endsWith('[]')
|
|
155
|
-
} else if (secure) {
|
|
156
|
-
/**
|
|
157
|
-
* A secure field is stored as ciphertext, so its declared JSON type says nothing about
|
|
158
|
-
* its column. It's always unbounded text: `maxLength` would truncate the ciphertext,
|
|
159
|
-
* an enum CHECK would never match it, and jsonb would fail to parse it.
|
|
160
|
-
*/
|
|
161
|
-
sqlType = DEF_SQL_TYPE
|
|
162
|
-
jsonType = 'string'
|
|
163
|
-
} else if (type === 'array') {
|
|
164
|
-
const resolved = arraySqlType(property)
|
|
165
|
-
sqlType = resolved.sqlType
|
|
166
|
-
jsonType = resolved.jsonType
|
|
167
|
-
array = resolved.array
|
|
168
|
-
} else {
|
|
169
|
-
const resolved = scalarSqlType(property)
|
|
170
|
-
sqlType = resolved.sqlType
|
|
171
|
-
jsonType = resolved.jsonType
|
|
115
|
+
return { sqlType: DEF_JSON_TYPE, jsonType: 'array', array: false }
|
|
172
116
|
}
|
|
173
117
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
sqlType
|
|
183
|
-
|
|
184
|
-
array = false
|
|
185
|
-
|
|
118
|
+
const compileColumn = (
|
|
119
|
+
name: string, property: RawProperty, required: string[]
|
|
120
|
+
): ColumnSpec => {
|
|
121
|
+
const override = readOverride(property)
|
|
122
|
+
const secure = property.secure === true
|
|
123
|
+
const type = baseType(property)
|
|
124
|
+
const nullable = isNullable(property, required.includes(name))
|
|
125
|
+
|
|
126
|
+
let sqlType: string
|
|
127
|
+
let jsonType: ColumnJsonType
|
|
128
|
+
let array = false
|
|
129
|
+
|
|
130
|
+
if (override.type != null) {
|
|
131
|
+
sqlType = override.type
|
|
132
|
+
jsonType = secure ? 'string' : scalarSqlType(property).jsonType
|
|
133
|
+
array = override.array === true || override.type.endsWith('[]')
|
|
134
|
+
} else if (secure) {
|
|
135
|
+
/**
|
|
136
|
+
* A secure field is stored as ciphertext, so its declared JSON type says nothing about
|
|
137
|
+
* its column. It's always unbounded text: `maxLength` would truncate the ciphertext,
|
|
138
|
+
* an enum CHECK would never match it, and jsonb would fail to parse it.
|
|
139
|
+
*/
|
|
140
|
+
sqlType = DEF_SQL_TYPE
|
|
141
|
+
jsonType = 'string'
|
|
142
|
+
} else if (type === 'array') {
|
|
143
|
+
const resolved = arraySqlType(property)
|
|
144
|
+
sqlType = resolved.sqlType
|
|
145
|
+
jsonType = resolved.jsonType
|
|
146
|
+
array = resolved.array
|
|
147
|
+
} else {
|
|
148
|
+
const resolved = scalarSqlType(property)
|
|
149
|
+
sqlType = resolved.sqlType
|
|
150
|
+
jsonType = resolved.jsonType
|
|
151
|
+
}
|
|
186
152
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
jsonb: isJsonb,
|
|
200
|
-
array,
|
|
201
|
-
managed: override.managed !== false,
|
|
202
|
-
...(override.using != null ? { using: override.using } : {}),
|
|
203
|
-
...(override.comment != null ? { comment: override.comment } : {})
|
|
204
|
-
}
|
|
153
|
+
if (override.length != null) {
|
|
154
|
+
sqlType = `character varying(${override.length})`
|
|
155
|
+
}
|
|
156
|
+
if (override.array === true && !array && !sqlType.endsWith('[]')) {
|
|
157
|
+
sqlType = `${sqlType}[]`
|
|
158
|
+
array = true
|
|
159
|
+
}
|
|
160
|
+
if (override.jsonb === true) {
|
|
161
|
+
sqlType = DEF_JSON_TYPE
|
|
162
|
+
jsonType = jsonType === 'array' ? 'array' : 'object'
|
|
163
|
+
array = false
|
|
164
|
+
}
|
|
205
165
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
column
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
166
|
+
const formatted = toFormatType(sqlType)
|
|
167
|
+
const isJsonb = formatted === 'jsonb' || formatted === 'json'
|
|
168
|
+
|
|
169
|
+
const column: ColumnSpec = {
|
|
170
|
+
property: name,
|
|
171
|
+
/** No implicit snake_casing — a silent rename is a silent DROP plus ADD. */
|
|
172
|
+
column: pgIdentifier(override.column ?? name),
|
|
173
|
+
sqlType: formatted,
|
|
174
|
+
jsonType,
|
|
175
|
+
notNull: override.nullable === false ? true : !nullable,
|
|
176
|
+
primaryKey: override.primaryKey === true,
|
|
177
|
+
secure,
|
|
178
|
+
jsonb: isJsonb,
|
|
179
|
+
array,
|
|
180
|
+
managed: override.managed !== false,
|
|
181
|
+
...(override.using != null ? { using: override.using } : {}),
|
|
182
|
+
...(override.comment != null ? { comment: override.comment } : {})
|
|
183
|
+
}
|
|
214
184
|
|
|
215
|
-
|
|
216
|
-
|
|
185
|
+
if (override.defaultRaw != null) {
|
|
186
|
+
column.defaultRaw = override.defaultRaw
|
|
187
|
+
} else if (override.default !== undefined) {
|
|
188
|
+
column.defaultLiteral = override.default
|
|
189
|
+
} else if (property.default !== undefined && !isJsonb && !array) {
|
|
190
|
+
/** Non-scalar defaults stay in `getDefaults()` only — they'd need casting in DDL. */
|
|
191
|
+
column.defaultLiteral = property.default as string | number | boolean | null
|
|
192
|
+
}
|
|
217
193
|
|
|
218
|
-
|
|
219
|
-
const properties: Record<string, RawProperty> = { ...schema.properties }
|
|
220
|
-
const required: string[] = [...(schema.required ?? [])]
|
|
221
|
-
/** `allOf` composition is used across the framework's schemas — flatten it like mongo does. */
|
|
222
|
-
for (const part of schema.allOf ?? []) {
|
|
223
|
-
const nested = collectProperties(part)
|
|
224
|
-
Object.assign(properties, nested.properties)
|
|
225
|
-
required.push(...nested.required)
|
|
194
|
+
return column
|
|
226
195
|
}
|
|
227
196
|
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
* list, and nothing stops a declaration from repeating one of the others. Two entries with
|
|
237
|
-
* one name emit the same `CREATE INDEX` twice inside a single DDL transaction, and Postgres
|
|
238
|
-
* answers the second with `42P07 already exists` — which rolls back the whole plan, so the
|
|
239
|
-
* table the first statement created never lands and the resource can never initialize.
|
|
240
|
-
* Duplicates are a declaration style, not a schema error: dedupe them, don't fail the boot.
|
|
241
|
-
*/
|
|
242
|
-
const byName = <T extends { name?: string }>(specs: T[], alias: string, kind: string): T[] => {
|
|
243
|
-
const seen = new Map<string, T>()
|
|
244
|
-
for (const spec of specs) {
|
|
245
|
-
const name = spec.name!
|
|
246
|
-
if (seen.has(name)) {
|
|
247
|
-
console.warn(
|
|
248
|
-
`@owlmeans/postgres-resource: ${alias} declares ${kind} "${name}" more than once —`
|
|
249
|
-
+ ' keeping the first declaration.'
|
|
250
|
-
)
|
|
251
|
-
continue
|
|
197
|
+
const collectProperties = (schema: RawSchema): { properties: Record<string, RawProperty>, required: string[] } => {
|
|
198
|
+
const properties: Record<string, RawProperty> = { ...schema.properties }
|
|
199
|
+
const required: string[] = [...(schema.required ?? [])]
|
|
200
|
+
/** `allOf` composition is used across the framework's schemas — flatten it like mongo does. */
|
|
201
|
+
for (const part of schema.allOf ?? []) {
|
|
202
|
+
const nested = collectProperties(part)
|
|
203
|
+
Object.assign(properties, nested.properties)
|
|
204
|
+
required.push(...nested.required)
|
|
252
205
|
}
|
|
253
|
-
seen.set(name, spec)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return [...seen.values()]
|
|
257
|
-
}
|
|
258
206
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
* emission and drift detection.
|
|
262
|
-
*
|
|
263
|
-
* `extraIndexes` carries what `resource.index()` declared. It's compiled here rather than
|
|
264
|
-
* merged afterwards so chained declarations get the same property-to-column mapping and
|
|
265
|
-
* the same generated names as schema-borne ones.
|
|
266
|
-
*
|
|
267
|
-
* @throws {UnsupportedArgumentError} when an override asks for something unrepresentable.
|
|
268
|
-
*/
|
|
269
|
-
export const schemaToTableSpec = (
|
|
270
|
-
alias: string, schema: AnySchema | undefined, pgSchema: string, table: string,
|
|
271
|
-
autoSync: boolean, extraIndexes: PgIndexSpec[] = []
|
|
272
|
-
): TableSpec => {
|
|
273
|
-
const raw = (schema ?? {}) as RawSchema
|
|
274
|
-
const root = raw[PG_KEYWORD] ?? {}
|
|
275
|
-
const { properties, required } = collectProperties(raw)
|
|
276
|
-
|
|
277
|
-
const columns: ColumnSpec[] = []
|
|
278
|
-
const uniques: PgUniqueSpec[] = [...(root.unique ?? [])]
|
|
279
|
-
const checks: PgCheckSpec[] = [...(root.checks ?? [])]
|
|
280
|
-
const indexes: PgIndexSpec[] = [...(root.indexes ?? []), ...extraIndexes]
|
|
281
|
-
const references: PgReferenceSpec[] = []
|
|
207
|
+
return { properties, required }
|
|
208
|
+
}
|
|
282
209
|
|
|
283
|
-
|
|
284
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Collapse specs that resolved to the same object name, first declaration wins.
|
|
212
|
+
*
|
|
213
|
+
* The same index can legitimately be declared twice — a schema-root `pg.indexes` entry, a
|
|
214
|
+
* per-property `pg: { index: ... }` override and a `resource.index()` call all land in one
|
|
215
|
+
* list, and nothing stops a declaration from repeating one of the others. Two entries with
|
|
216
|
+
* one name emit the same `CREATE INDEX` twice inside a single DDL transaction, and Postgres
|
|
217
|
+
* answers the second with `42P07 already exists` — which rolls back the whole plan, so the
|
|
218
|
+
* table the first statement created never lands and the resource can never initialize.
|
|
219
|
+
* Duplicates are a declaration style, not a schema error: dedupe them, don't fail the boot.
|
|
220
|
+
*/
|
|
221
|
+
const byName = <T extends { name?: string }>(specs: T[], alias: string, kind: string): T[] => {
|
|
222
|
+
const seen = new Map<string, T>()
|
|
223
|
+
for (const spec of specs) {
|
|
224
|
+
const name = spec.name!
|
|
225
|
+
if (seen.has(name)) {
|
|
226
|
+
log.warn('Duplicate declaration, keeping the first one', { resource: alias, kind, name })
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
seen.set(name, spec)
|
|
230
|
+
}
|
|
285
231
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
columns.push(column)
|
|
232
|
+
return [...seen.values()]
|
|
233
|
+
}
|
|
289
234
|
|
|
290
|
-
|
|
235
|
+
const schemaToTableSpec = (
|
|
236
|
+
alias: string, schema: AnySchema | undefined, pgSchema: string, table: string,
|
|
237
|
+
autoSync: boolean, extraIndexes: PgIndexSpec[] = []
|
|
238
|
+
): TableSpec => {
|
|
239
|
+
const raw = (schema ?? {}) as RawSchema
|
|
240
|
+
const root = raw[PG_KEYWORD] ?? {}
|
|
241
|
+
const { properties, required } = collectProperties(raw)
|
|
242
|
+
|
|
243
|
+
const columns: ColumnSpec[] = []
|
|
244
|
+
const uniques: PgUniqueSpec[] = [...(root.unique ?? [])]
|
|
245
|
+
const checks: PgCheckSpec[] = [...(root.checks ?? [])]
|
|
246
|
+
const indexes: PgIndexSpec[] = [...(root.indexes ?? []), ...extraIndexes]
|
|
247
|
+
const references: PgReferenceSpec[] = []
|
|
248
|
+
|
|
249
|
+
const resolvedTable = pgIdentifier(root.table ?? table)
|
|
250
|
+
const resolvedSchema = pgIdentifier(root.schema ?? pgSchema)
|
|
251
|
+
|
|
252
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
253
|
+
const column = compileColumn(name, property, required)
|
|
254
|
+
columns.push(column)
|
|
255
|
+
|
|
256
|
+
const override = readOverride(property)
|
|
257
|
+
|
|
258
|
+
if (override.unique != null && override.unique !== false) {
|
|
259
|
+
if (column.secure) {
|
|
260
|
+
/**
|
|
261
|
+
* Encryption uses a managed nonce, so two identical plaintexts encrypt to different
|
|
262
|
+
* ciphertexts — a unique constraint on the column would never collide, silently.
|
|
263
|
+
*/
|
|
264
|
+
throw new UnsupportedArgumentError(`secure-unique:${name}`)
|
|
265
|
+
}
|
|
266
|
+
uniques.push({
|
|
267
|
+
columns: [name],
|
|
268
|
+
...(typeof override.unique === 'string' ? { name: override.unique } : {})
|
|
269
|
+
})
|
|
270
|
+
}
|
|
291
271
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
272
|
+
if (override.index != null && override.index !== false) {
|
|
273
|
+
if (column.secure) {
|
|
274
|
+
log.warn('Index on a secure column will never match: encryption is non-deterministic', {
|
|
275
|
+
resource: alias, column: name,
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
const specs = override.index === true
|
|
279
|
+
? [{ columns: [name] }]
|
|
280
|
+
: Array.isArray(override.index) ? override.index : [override.index]
|
|
281
|
+
indexes.push(...specs.map(spec => ({ ...spec, columns: spec.columns ?? [name] })))
|
|
299
282
|
}
|
|
300
|
-
uniques.push({
|
|
301
|
-
columns: [name],
|
|
302
|
-
...(typeof override.unique === 'string' ? { name: override.unique } : {})
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
283
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
284
|
+
if (override.references != null) {
|
|
285
|
+
references.push({
|
|
286
|
+
...override.references,
|
|
287
|
+
property: name,
|
|
288
|
+
column: override.references.column ?? ID_FIELD,
|
|
289
|
+
name: pgIdentifier(override.references.name ?? `${resolvedTable}_${column.column}_fkey`)
|
|
290
|
+
})
|
|
312
291
|
}
|
|
313
|
-
const specs = override.index === true
|
|
314
|
-
? [{ columns: [name] }]
|
|
315
|
-
: Array.isArray(override.index) ? override.index : [override.index]
|
|
316
|
-
indexes.push(...specs.map(spec => ({ ...spec, columns: spec.columns ?? [name] })))
|
|
317
|
-
}
|
|
318
292
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
326
|
-
}
|
|
293
|
+
if (override.check != null) {
|
|
294
|
+
checks.push({
|
|
295
|
+
name: `${resolvedTable}_${column.column}_check`,
|
|
296
|
+
expression: override.check.replace(/\{\{col\}\}/g, quoteIdent(column.column))
|
|
297
|
+
})
|
|
298
|
+
}
|
|
327
299
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
300
|
+
/** A string enum becomes a CHECK — a native PG enum can't drop values and can't be altered in a transaction. */
|
|
301
|
+
if (property.enum != null && !column.secure && override.type == null
|
|
302
|
+
&& baseType(property) === 'string' && property.enum.every(value => typeof value === 'string')) {
|
|
303
|
+
const values = (property.enum as string[]).map(value => `'${value.replace(/'/g, "''")}'`).join(', ')
|
|
304
|
+
const predicate = column.notNull
|
|
305
|
+
? `${quoteIdent(column.column)} IN (${values})`
|
|
306
|
+
: `${quoteIdent(column.column)} IS NULL OR ${quoteIdent(column.column)} IN (${values})`
|
|
307
|
+
checks.push({ name: `${resolvedTable}_${column.column}_enum`, expression: predicate })
|
|
308
|
+
}
|
|
333
309
|
}
|
|
334
310
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
? `${quoteIdent(column.column)} IN (${values})`
|
|
341
|
-
: `${quoteIdent(column.column)} IS NULL OR ${quoteIdent(column.column)} IN (${values})`
|
|
342
|
-
checks.push({ name: `${resolvedTable}_${column.column}_enum`, expression: predicate })
|
|
311
|
+
const byProperty: Record<string, ColumnSpec> = {}
|
|
312
|
+
const byColumn: Record<string, ColumnSpec> = {}
|
|
313
|
+
for (const column of columns) {
|
|
314
|
+
byProperty[column.property] = column
|
|
315
|
+
byColumn[column.column] = column
|
|
343
316
|
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const byProperty: Record<string, ColumnSpec> = {}
|
|
347
|
-
const byColumn: Record<string, ColumnSpec> = {}
|
|
348
|
-
for (const column of columns) {
|
|
349
|
-
byProperty[column.property] = column
|
|
350
|
-
byColumn[column.column] = column
|
|
351
|
-
}
|
|
352
317
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
column.notNull = true
|
|
361
|
-
|
|
362
|
-
return column.column
|
|
363
|
-
})
|
|
364
|
-
} else {
|
|
365
|
-
const explicit = columns.filter(column => column.primaryKey)
|
|
366
|
-
if (explicit.length > 0) {
|
|
367
|
-
primaryKey = explicit.map(column => {
|
|
318
|
+
let primaryKey: string[] = []
|
|
319
|
+
if (root.primaryKey != null && root.primaryKey.length > 0) {
|
|
320
|
+
primaryKey = root.primaryKey.map(property => {
|
|
321
|
+
const column = byProperty[property]
|
|
322
|
+
if (column == null) {
|
|
323
|
+
throw new UnsupportedArgumentError(`primary-key:${property}`)
|
|
324
|
+
}
|
|
368
325
|
column.notNull = true
|
|
326
|
+
|
|
369
327
|
return column.column
|
|
370
328
|
})
|
|
371
329
|
} else {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
id = {
|
|
379
|
-
property: ID_FIELD, column: ID_FIELD, sqlType: DEF_SQL_TYPE, jsonType: 'string',
|
|
380
|
-
notNull: true, primaryKey: true, secure: false, jsonb: false, array: false,
|
|
381
|
-
managed: true, defaultRaw: DEF_ID_DEFAULT
|
|
382
|
-
}
|
|
383
|
-
columns.unshift(id)
|
|
384
|
-
byProperty[ID_FIELD] = id
|
|
385
|
-
byColumn[ID_FIELD] = id
|
|
330
|
+
const explicit = columns.filter(column => column.primaryKey)
|
|
331
|
+
if (explicit.length > 0) {
|
|
332
|
+
primaryKey = explicit.map(column => {
|
|
333
|
+
column.notNull = true
|
|
334
|
+
return column.column
|
|
335
|
+
})
|
|
386
336
|
} else {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Every OwlMeans record is addressed by `id`, so the column is synthesized when the
|
|
339
|
+
* schema doesn't declare it — `create()` and `load()` both assume it exists.
|
|
340
|
+
*/
|
|
341
|
+
let id = byProperty[ID_FIELD]
|
|
342
|
+
if (id == null) {
|
|
343
|
+
id = {
|
|
344
|
+
property: ID_FIELD, column: ID_FIELD, sqlType: DEF_SQL_TYPE, jsonType: 'string',
|
|
345
|
+
notNull: true, primaryKey: true, secure: false, jsonb: false, array: false,
|
|
346
|
+
managed: true, defaultRaw: DEF_ID_DEFAULT
|
|
347
|
+
}
|
|
348
|
+
columns.unshift(id)
|
|
349
|
+
byProperty[ID_FIELD] = id
|
|
350
|
+
byColumn[ID_FIELD] = id
|
|
351
|
+
} else {
|
|
352
|
+
id.notNull = true
|
|
353
|
+
id.primaryKey = true
|
|
354
|
+
if (id.defaultRaw == null && id.defaultLiteral === undefined) {
|
|
355
|
+
id.defaultRaw = id.sqlType === 'uuid' ? 'gen_random_uuid()' : DEF_ID_DEFAULT
|
|
356
|
+
}
|
|
391
357
|
}
|
|
358
|
+
primaryKey = [id.column]
|
|
392
359
|
}
|
|
393
|
-
primaryKey = [id.column]
|
|
394
360
|
}
|
|
395
|
-
}
|
|
396
361
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
362
|
+
/** Index and unique specs address properties; map them onto physical columns once, here. */
|
|
363
|
+
const mapColumns = (names: string[]): string[] => names.map(name => {
|
|
364
|
+
const column = byProperty[name]
|
|
365
|
+
if (column == null) {
|
|
366
|
+
throw new UnsupportedArgumentError(`unknown-property:${name}`)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return column.column
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
const resolvedIndexes: PgIndexSpec[] = byName(indexes.map(spec => {
|
|
373
|
+
const list = spec.columns == null
|
|
374
|
+
? []
|
|
375
|
+
: mapColumns(Array.isArray(spec.columns) ? spec.columns : [spec.columns])
|
|
376
|
+
const suffix = spec.unique === true ? 'uq' : 'idx'
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
...spec,
|
|
380
|
+
columns: list,
|
|
381
|
+
method: spec.method ?? PgIndexMethod.BTree,
|
|
382
|
+
name: pgIdentifier(spec.name ?? `${resolvedTable}_${list.join('_')}_${suffix}`)
|
|
383
|
+
}
|
|
384
|
+
}), alias, 'index')
|
|
403
385
|
|
|
404
|
-
|
|
405
|
-
|
|
386
|
+
const resolvedUniques: PgUniqueSpec[] = byName(uniques.map(spec => {
|
|
387
|
+
const list = mapColumns(spec.columns)
|
|
406
388
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
? []
|
|
410
|
-
: mapColumns(Array.isArray(spec.columns) ? spec.columns : [spec.columns])
|
|
411
|
-
const suffix = spec.unique === true ? 'uq' : 'idx'
|
|
389
|
+
return { columns: list, name: pgIdentifier(spec.name ?? `${resolvedTable}_${list.join('_')}_key`) }
|
|
390
|
+
}), alias, 'unique')
|
|
412
391
|
|
|
413
392
|
return {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
393
|
+
alias,
|
|
394
|
+
schema: resolvedSchema,
|
|
395
|
+
table: resolvedTable,
|
|
396
|
+
qualified: qualify(resolvedSchema, resolvedTable),
|
|
397
|
+
columns,
|
|
398
|
+
byProperty,
|
|
399
|
+
byColumn,
|
|
400
|
+
primaryKey,
|
|
401
|
+
uniques: resolvedUniques,
|
|
402
|
+
checks: checks.map(check => ({ ...check, name: pgIdentifier(check.name ?? `${resolvedTable}_check`) })),
|
|
403
|
+
indexes: resolvedIndexes,
|
|
404
|
+
references,
|
|
405
|
+
unmanaged: [
|
|
406
|
+
...(root.unmanaged ?? []).map(name => byProperty[name]?.column ?? pgIdentifier(name)),
|
|
407
|
+
...columns.filter(column => !column.managed).map(column => column.column)
|
|
408
|
+
],
|
|
409
|
+
autoSync: root.autoSync ?? autoSync,
|
|
410
|
+
...(root.comment != null ? { comment: root.comment } : {})
|
|
418
411
|
}
|
|
419
|
-
}), alias, 'index')
|
|
420
|
-
|
|
421
|
-
const resolvedUniques: PgUniqueSpec[] = byName(uniques.map(spec => {
|
|
422
|
-
const list = mapColumns(spec.columns)
|
|
423
|
-
|
|
424
|
-
return { columns: list, name: pgIdentifier(spec.name ?? `${resolvedTable}_${list.join('_')}_key`) }
|
|
425
|
-
}), alias, 'unique')
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
alias,
|
|
429
|
-
schema: resolvedSchema,
|
|
430
|
-
table: resolvedTable,
|
|
431
|
-
qualified: qualify(resolvedSchema, resolvedTable),
|
|
432
|
-
columns,
|
|
433
|
-
byProperty,
|
|
434
|
-
byColumn,
|
|
435
|
-
primaryKey,
|
|
436
|
-
uniques: resolvedUniques,
|
|
437
|
-
checks: checks.map(check => ({ ...check, name: pgIdentifier(check.name ?? `${resolvedTable}_check`) })),
|
|
438
|
-
indexes: resolvedIndexes,
|
|
439
|
-
references,
|
|
440
|
-
unmanaged: [
|
|
441
|
-
...(root.unmanaged ?? []).map(name => byProperty[name]?.column ?? pgIdentifier(name)),
|
|
442
|
-
...columns.filter(column => !column.managed).map(column => column.column)
|
|
443
|
-
],
|
|
444
|
-
autoSync: root.autoSync ?? autoSync,
|
|
445
|
-
...(root.comment != null ? { comment: root.comment } : {})
|
|
446
412
|
}
|
|
413
|
+
|
|
414
|
+
return { toFormatType, schemaToTableSpec }
|
|
447
415
|
}
|
|
448
416
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
417
|
+
export const pgSchemaHelper = createPgSchemaHelper()
|
|
418
|
+
|
|
419
|
+
/** @deprecated compat:factory-refactor — use `pgSchemaHelper.schemaToTableSpec(…)` */
|
|
420
|
+
export const schemaToTableSpec = (
|
|
421
|
+
alias: string, schema: AnySchema | undefined, pgSchema: string, table: string,
|
|
422
|
+
autoSync: boolean, extraIndexes: PgIndexSpec[] = []
|
|
423
|
+
): TableSpec => pgSchemaHelper.schemaToTableSpec(alias, schema, pgSchema, table, autoSync, extraIndexes)
|