@travetto/model-postgres 8.0.0-alpha.26 → 8.0.0-alpha.28
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/package.json +7 -7
- package/src/connection.ts +14 -9
- package/src/dialect.ts +101 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-postgres",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PostgreSQL backing for the travetto model module, with real-time modeling support for SQL schemas.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
"directory": "module/model-postgres"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/context": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/model-query": "^8.0.0-alpha.
|
|
35
|
-
"@travetto/model-sql": "^8.0.0-alpha.
|
|
31
|
+
"@travetto/config": "^8.0.0-alpha.23",
|
|
32
|
+
"@travetto/context": "^8.0.0-alpha.21",
|
|
33
|
+
"@travetto/model": "^8.0.0-alpha.24",
|
|
34
|
+
"@travetto/model-query": "^8.0.0-alpha.26",
|
|
35
|
+
"@travetto/model-sql": "^8.0.0-alpha.28",
|
|
36
36
|
"@types/pg": "^8.20.0",
|
|
37
37
|
"pg": "^8.22.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
40
|
+
"@travetto/cli": "^8.0.0-alpha.29"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"@travetto/cli": {
|
package/src/connection.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { type Pool, type PoolClient, default as pg } from 'pg';
|
|
1
|
+
import { type DatabaseError, type Pool, type PoolClient, default as pg } from 'pg';
|
|
2
2
|
|
|
3
3
|
import type { AsyncContext } from '@travetto/context';
|
|
4
4
|
import { Injectable } from '@travetto/di';
|
|
5
|
-
import { ExistsError } from '@travetto/model';
|
|
5
|
+
import { ExistsError, UniqueError } from '@travetto/model';
|
|
6
6
|
import { SQLConnection } from '@travetto/model-sql';
|
|
7
7
|
import { castTo, ShutdownManager } from '@travetto/runtime';
|
|
8
8
|
|
|
9
9
|
import type { PostgresModelConfig } from './config.ts';
|
|
10
10
|
import { PostgresDialect } from './dialect.ts';
|
|
11
11
|
|
|
12
|
+
function isPgDatabaseError(error: unknown): error is DatabaseError {
|
|
13
|
+
return !!error && typeof error === 'object' && 'code' in error;
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* PostgreSQL connection manager
|
|
14
18
|
*/
|
|
@@ -86,15 +90,16 @@ export class PostgresConnection extends SQLConnection<PoolClient> {
|
|
|
86
90
|
const records: Type[] = [...result.rows].map(row => ({ ...row }));
|
|
87
91
|
return { count: result.rowCount ?? 0, records };
|
|
88
92
|
} catch (error) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
if (isPgDatabaseError(error)) {
|
|
94
|
+
if (error.code === '23505') {
|
|
95
|
+
const constraint = error.constraint ?? (error.detail ? 'index' : 'query');
|
|
96
|
+
const detail = error.detail ?? query;
|
|
97
|
+
throw new UniqueError('index', constraint, { detail, query });
|
|
98
|
+
} else if (error.code === '42P07') {
|
|
92
99
|
throw new ExistsError('index', query);
|
|
93
|
-
|
|
94
|
-
throw new ExistsError('query', query);
|
|
95
|
-
default:
|
|
96
|
-
throw error;
|
|
100
|
+
}
|
|
97
101
|
}
|
|
102
|
+
throw error;
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
}
|
package/src/dialect.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AbstractANSI99Dialect, type TableContext } from '@travetto/model-sql';
|
|
1
|
+
import { AbstractANSI99Dialect, type ResolvedPathContext, type TableContext } from '@travetto/model-sql';
|
|
2
2
|
import { type Class, castTo, JSONUtil } from '@travetto/runtime';
|
|
3
3
|
import { type SchemaFieldConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
4
4
|
|
|
@@ -74,59 +74,115 @@ export class PostgresDialect extends AbstractANSI99Dialect {
|
|
|
74
74
|
return `$${index}`;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
77
|
+
#buildContainmentPayload(value: unknown, context: ResolvedPathContext): unknown {
|
|
78
|
+
if (!context.subPath || context.subPath.length === 0) {
|
|
79
|
+
return Array.isArray(value) ? value : [value];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isArraySegment = new Array<boolean>(context.subPath.length).fill(false);
|
|
83
|
+
let currentClass: Class | undefined = context.arrayField?.type;
|
|
84
|
+
for (let index = 0; index < context.subPath.length; index++) {
|
|
85
|
+
if (!currentClass) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const segment = context.subPath[index];
|
|
89
|
+
const classConfig = SchemaRegistryIndex.getOptional(currentClass)?.get();
|
|
90
|
+
const fieldConfig = classConfig?.fields[segment];
|
|
91
|
+
if (fieldConfig) {
|
|
92
|
+
if (fieldConfig.array) {
|
|
93
|
+
isArraySegment[index] = true;
|
|
94
|
+
}
|
|
95
|
+
currentClass = fieldConfig.type;
|
|
96
|
+
} else {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const buildPayloadForValue = (item: unknown): unknown => {
|
|
102
|
+
let itemPayload: unknown = item;
|
|
103
|
+
for (let index = context.subPath!.length - 1; index >= 0; index--) {
|
|
104
|
+
const segment = context.subPath![index];
|
|
105
|
+
if (isArraySegment[index]) {
|
|
106
|
+
itemPayload = { [segment]: Array.isArray(itemPayload) ? itemPayload : [itemPayload] };
|
|
107
|
+
} else {
|
|
108
|
+
itemPayload = { [segment]: itemPayload };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return itemPayload;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
let currentPayload: unknown;
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
currentPayload = value.map(item => buildPayloadForValue(item));
|
|
117
|
+
} else {
|
|
118
|
+
currentPayload = [buildPayloadForValue(value)];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (context.arrayPath && context.arrayPath.length > 1) {
|
|
122
|
+
for (let index = context.arrayPath.length - 1; index >= 1; index--) {
|
|
123
|
+
currentPayload = { [context.arrayPath[index]]: currentPayload };
|
|
124
|
+
}
|
|
125
|
+
return currentPayload;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return currentPayload;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#getPostgresArrayTarget(context: ResolvedPathContext): {
|
|
132
|
+
isNative: boolean;
|
|
133
|
+
sqlPath: string;
|
|
134
|
+
jsonbPath: string;
|
|
135
|
+
buildPayload: (val: unknown) => unknown;
|
|
136
|
+
} {
|
|
137
|
+
const arrayField = context.arrayField ?? context.leafField;
|
|
138
|
+
const isTopLevel = (context.arrayPath?.length ?? 1) === 1;
|
|
139
|
+
const hasSubPath = (context.subPath?.length ?? 0) > 0;
|
|
140
|
+
const isScalarArray = arrayField ? !SchemaRegistryIndex.has(arrayField.type) : true;
|
|
141
|
+
const isNative = isTopLevel && !hasSubPath && isScalarArray;
|
|
142
|
+
|
|
143
|
+
const targetPath =
|
|
144
|
+
hasSubPath && context.arrayPath && context.arrayPath.length > 0 ? this.escapeIdentifier(context.arrayPath[0]) : context.sqlPath;
|
|
145
|
+
|
|
146
|
+
const jsonbPath = isTopLevel && !hasSubPath ? targetPath : `(${targetPath})::jsonb`;
|
|
147
|
+
const buildPayload = (value: unknown): unknown => this.#buildContainmentPayload(value, context);
|
|
148
|
+
|
|
149
|
+
return { isNative, sqlPath: context.sqlPath, jsonbPath, buildPayload };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
compileArrayAll(context: ResolvedPathContext, identifier: string, value: unknown[]): { sql: string; formatted: unknown } {
|
|
153
|
+
const target = this.#getPostgresArrayTarget(context);
|
|
154
|
+
if (target.isNative) {
|
|
155
|
+
return { sql: `${target.sqlPath} @> ${identifier}`, formatted: value };
|
|
156
|
+
}
|
|
157
|
+
return { sql: `${target.jsonbPath} @> ${identifier}::jsonb`, formatted: JSONUtil.toUTF8(target.buildPayload(value)) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
compileArrayEquals(context: ResolvedPathContext, identifier: string, values: unknown): { sql: string; formatted: unknown } {
|
|
161
|
+
const target = this.#getPostgresArrayTarget(context);
|
|
162
|
+
if (target.isNative) {
|
|
99
163
|
if (Array.isArray(values)) {
|
|
100
|
-
return { sql: `${sqlPath} @> ${identifier}`, formatted: values };
|
|
164
|
+
return { sql: `${target.sqlPath} @> ${identifier}`, formatted: values };
|
|
101
165
|
}
|
|
102
|
-
return { sql: `${identifier} = ANY(${sqlPath})`, formatted: values };
|
|
166
|
+
return { sql: `${identifier} = ANY(${target.sqlPath})`, formatted: values };
|
|
103
167
|
}
|
|
104
|
-
|
|
105
|
-
const val = Array.isArray(values) ? values : [values];
|
|
106
|
-
return { sql: `${jsonbPath} @> ${identifier}::jsonb`, formatted: JSONUtil.toUTF8(val) };
|
|
168
|
+
return { sql: `${target.jsonbPath} @> ${identifier}::jsonb`, formatted: JSONUtil.toUTF8(target.buildPayload(values)) };
|
|
107
169
|
}
|
|
108
170
|
|
|
109
|
-
compileArrayAny(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
field: SchemaFieldConfig,
|
|
114
|
-
topLevel?: boolean
|
|
115
|
-
): { sql: string; formatted: unknown } {
|
|
116
|
-
if (topLevel && !SchemaRegistryIndex.has(field.type)) {
|
|
117
|
-
return { sql: `${sqlPath} && ${identifier}`, formatted: values };
|
|
171
|
+
compileArrayAny(context: ResolvedPathContext, identifier: string, values: unknown[]): { sql: string; formatted: unknown } {
|
|
172
|
+
const target = this.#getPostgresArrayTarget(context);
|
|
173
|
+
if (target.isNative) {
|
|
174
|
+
return { sql: `${target.sqlPath} && ${identifier}`, formatted: values };
|
|
118
175
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
return { sql: `${jsonbPath} @> ANY(${identifier}::jsonb[])`, formatted };
|
|
176
|
+
const formatted = values.map(v => JSONUtil.toUTF8(target.buildPayload(v)));
|
|
177
|
+
return { sql: `${target.jsonbPath} @> ANY(${identifier}::jsonb[])`, formatted };
|
|
122
178
|
}
|
|
123
179
|
|
|
124
|
-
compileArrayExists(
|
|
125
|
-
|
|
126
|
-
|
|
180
|
+
compileArrayExists(context: ResolvedPathContext, identifier?: string): { sql: string } {
|
|
181
|
+
const target = this.#getPostgresArrayTarget(context);
|
|
182
|
+
if (target.isNative) {
|
|
183
|
+
return { sql: `(${target.sqlPath} IS NOT NULL AND cardinality(${target.sqlPath}) > 0)` };
|
|
127
184
|
}
|
|
128
|
-
|
|
129
|
-
return { sql: `(${sqlPath} IS NOT NULL AND ${jsonbPath} <> '[]'::jsonb)` };
|
|
185
|
+
return { sql: `(${target.jsonbPath} IS NOT NULL AND ${target.jsonbPath} <> '[]'::jsonb)` };
|
|
130
186
|
}
|
|
131
187
|
|
|
132
188
|
getRegexOperator(caseInsensitive: boolean): string {
|