@travetto/model-postgres 8.0.0-alpha.26 → 8.0.0-alpha.27
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 +79 -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.27",
|
|
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.25",
|
|
35
|
+
"@travetto/model-sql": "^8.0.0-alpha.27",
|
|
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,93 @@ 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
|
+
let currentPayload: unknown;
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
currentPayload = value.map(item => {
|
|
85
|
+
let itemPayload: unknown = item;
|
|
86
|
+
for (let index = context.subPath!.length - 1; index >= 0; index--) {
|
|
87
|
+
itemPayload = { [context.subPath![index]]: itemPayload };
|
|
88
|
+
}
|
|
89
|
+
return itemPayload;
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
currentPayload = value;
|
|
93
|
+
for (let index = context.subPath.length - 1; index >= 0; index--) {
|
|
94
|
+
currentPayload = { [context.subPath[index]]: currentPayload };
|
|
95
|
+
}
|
|
96
|
+
currentPayload = [currentPayload];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (context.arrayPath && context.arrayPath.length > 1) {
|
|
100
|
+
for (let index = context.arrayPath.length - 1; index >= 1; index--) {
|
|
101
|
+
currentPayload = { [context.arrayPath[index]]: currentPayload };
|
|
102
|
+
}
|
|
103
|
+
return currentPayload;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return currentPayload;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#getPostgresArrayTarget(context: ResolvedPathContext): {
|
|
110
|
+
isNative: boolean;
|
|
111
|
+
sqlPath: string;
|
|
112
|
+
jsonbPath: string;
|
|
113
|
+
buildPayload: (val: unknown) => unknown;
|
|
114
|
+
} {
|
|
115
|
+
const arrayField = context.arrayField ?? context.leafField;
|
|
116
|
+
const isTopLevel = (context.arrayPath?.length ?? 1) === 1;
|
|
117
|
+
const hasSubPath = (context.subPath?.length ?? 0) > 0;
|
|
118
|
+
const isScalarArray = arrayField ? !SchemaRegistryIndex.has(arrayField.type) : true;
|
|
119
|
+
const isNative = isTopLevel && !hasSubPath && isScalarArray;
|
|
120
|
+
|
|
121
|
+
const targetPath =
|
|
122
|
+
hasSubPath && context.arrayPath && context.arrayPath.length > 0 ? this.escapeIdentifier(context.arrayPath[0]) : context.sqlPath;
|
|
123
|
+
|
|
124
|
+
const jsonbPath = isTopLevel && !hasSubPath ? targetPath : `(${targetPath})::jsonb`;
|
|
125
|
+
const buildPayload = (value: unknown): unknown => this.#buildContainmentPayload(value, context);
|
|
126
|
+
|
|
127
|
+
return { isNative, sqlPath: context.sqlPath, jsonbPath, buildPayload };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
compileArrayAll(context: ResolvedPathContext, identifier: string, value: unknown[]): { sql: string; formatted: unknown } {
|
|
131
|
+
const target = this.#getPostgresArrayTarget(context);
|
|
132
|
+
if (target.isNative) {
|
|
133
|
+
return { sql: `${target.sqlPath} @> ${identifier}`, formatted: value };
|
|
134
|
+
}
|
|
135
|
+
return { sql: `${target.jsonbPath} @> ${identifier}::jsonb`, formatted: JSONUtil.toUTF8(target.buildPayload(value)) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
compileArrayEquals(context: ResolvedPathContext, identifier: string, values: unknown): { sql: string; formatted: unknown } {
|
|
139
|
+
const target = this.#getPostgresArrayTarget(context);
|
|
140
|
+
if (target.isNative) {
|
|
99
141
|
if (Array.isArray(values)) {
|
|
100
|
-
return { sql: `${sqlPath} @> ${identifier}`, formatted: values };
|
|
142
|
+
return { sql: `${target.sqlPath} @> ${identifier}`, formatted: values };
|
|
101
143
|
}
|
|
102
|
-
return { sql: `${identifier} = ANY(${sqlPath})`, formatted: values };
|
|
144
|
+
return { sql: `${identifier} = ANY(${target.sqlPath})`, formatted: values };
|
|
103
145
|
}
|
|
104
|
-
|
|
105
|
-
const val = Array.isArray(values) ? values : [values];
|
|
106
|
-
return { sql: `${jsonbPath} @> ${identifier}::jsonb`, formatted: JSONUtil.toUTF8(val) };
|
|
146
|
+
return { sql: `${target.jsonbPath} @> ${identifier}::jsonb`, formatted: JSONUtil.toUTF8(target.buildPayload(values)) };
|
|
107
147
|
}
|
|
108
148
|
|
|
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 };
|
|
149
|
+
compileArrayAny(context: ResolvedPathContext, identifier: string, values: unknown[]): { sql: string; formatted: unknown } {
|
|
150
|
+
const target = this.#getPostgresArrayTarget(context);
|
|
151
|
+
if (target.isNative) {
|
|
152
|
+
return { sql: `${target.sqlPath} && ${identifier}`, formatted: values };
|
|
118
153
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
return { sql: `${jsonbPath} @> ANY(${identifier}::jsonb[])`, formatted };
|
|
154
|
+
const formatted = values.map(v => JSONUtil.toUTF8(target.buildPayload(v)));
|
|
155
|
+
return { sql: `${target.jsonbPath} @> ANY(${identifier}::jsonb[])`, formatted };
|
|
122
156
|
}
|
|
123
157
|
|
|
124
|
-
compileArrayExists(
|
|
125
|
-
|
|
126
|
-
|
|
158
|
+
compileArrayExists(context: ResolvedPathContext, identifier?: string): { sql: string } {
|
|
159
|
+
const target = this.#getPostgresArrayTarget(context);
|
|
160
|
+
if (target.isNative) {
|
|
161
|
+
return { sql: `(${target.sqlPath} IS NOT NULL AND cardinality(${target.sqlPath}) > 0)` };
|
|
127
162
|
}
|
|
128
|
-
|
|
129
|
-
return { sql: `(${sqlPath} IS NOT NULL AND ${jsonbPath} <> '[]'::jsonb)` };
|
|
163
|
+
return { sql: `(${target.jsonbPath} IS NOT NULL AND ${target.jsonbPath} <> '[]'::jsonb)` };
|
|
130
164
|
}
|
|
131
165
|
|
|
132
166
|
getRegexOperator(caseInsensitive: boolean): string {
|