@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-postgres",
3
- "version": "8.0.0-alpha.26",
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.22",
32
- "@travetto/context": "^8.0.0-alpha.20",
33
- "@travetto/model": "^8.0.0-alpha.23",
34
- "@travetto/model-query": "^8.0.0-alpha.24",
35
- "@travetto/model-sql": "^8.0.0-alpha.26",
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.28"
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
- const code = error && typeof error === 'object' && 'code' in error ? castTo<Record<string, unknown>>(error).code : undefined;
90
- switch (code) {
91
- case '42P07':
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
- case '23505':
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
- compileArrayAll(
78
- sqlPath: string,
79
- identifier: string,
80
- value: unknown[],
81
- field: SchemaFieldConfig,
82
- topLevel?: boolean
83
- ): { sql: string; formatted: unknown } {
84
- if (topLevel && !SchemaRegistryIndex.has(field.type)) {
85
- return { sql: `${sqlPath} @> ${identifier}`, formatted: value };
86
- }
87
- const jsonbPath = topLevel ? sqlPath : `(${sqlPath})::jsonb`;
88
- return { sql: `${jsonbPath} @> ${identifier}::jsonb`, formatted: JSONUtil.toUTF8(value) };
89
- }
90
-
91
- compileArrayEquals(
92
- sqlPath: string,
93
- identifier: string,
94
- values: unknown,
95
- field: SchemaFieldConfig,
96
- topLevel?: boolean
97
- ): { sql: string; formatted: unknown } {
98
- if (topLevel && !SchemaRegistryIndex.has(field.type)) {
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
- const jsonbPath = topLevel ? sqlPath : `(${sqlPath})::jsonb`;
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
- sqlPath: string,
111
- identifier: string,
112
- values: unknown[],
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 jsonbPath = topLevel ? sqlPath : `(${sqlPath})::jsonb`;
120
- const formatted = values.map(v => JSONUtil.toUTF8(Array.isArray(v) ? v : [v]));
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(sqlPath: string, identifier: string, field: SchemaFieldConfig, topLevel?: boolean): { sql: string } {
125
- if (topLevel && !SchemaRegistryIndex.has(field.type)) {
126
- return { sql: `(${sqlPath} IS NOT NULL AND cardinality(${sqlPath}) > 0)` };
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
- const jsonbPath = topLevel ? sqlPath : `(${sqlPath})::jsonb`;
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 {