@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 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.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.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.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.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,93 @@ 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
+ 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
- const jsonbPath = topLevel ? sqlPath : `(${sqlPath})::jsonb`;
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
- 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 };
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 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 };
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(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)` };
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
- const jsonbPath = topLevel ? sqlPath : `(${sqlPath})::jsonb`;
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 {