@constructive-io/graphql-query 3.2.5 → 3.3.0
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 +411 -65
- package/ast.d.ts +4 -4
- package/ast.js +24 -9
- package/client/error.d.ts +95 -0
- package/client/error.js +277 -0
- package/client/execute.d.ts +57 -0
- package/client/execute.js +124 -0
- package/client/index.d.ts +8 -0
- package/client/index.js +20 -0
- package/client/typed-document.d.ts +31 -0
- package/client/typed-document.js +44 -0
- package/custom-ast.d.ts +22 -8
- package/custom-ast.js +16 -1
- package/esm/ast.js +22 -7
- package/esm/client/error.js +271 -0
- package/esm/client/execute.js +120 -0
- package/esm/client/index.js +8 -0
- package/esm/client/typed-document.js +40 -0
- package/esm/custom-ast.js +16 -1
- package/esm/generators/field-selector.js +381 -0
- package/esm/generators/index.js +13 -0
- package/esm/generators/mutations.js +200 -0
- package/esm/generators/naming-helpers.js +154 -0
- package/esm/generators/select.js +661 -0
- package/esm/index.js +30 -0
- package/esm/introspect/index.js +9 -0
- package/esm/introspect/infer-tables.js +697 -0
- package/esm/introspect/schema-query.js +120 -0
- package/esm/introspect/transform-schema.js +271 -0
- package/esm/introspect/transform.js +38 -0
- package/esm/meta-object/convert.js +3 -0
- package/esm/meta-object/format.json +11 -41
- package/esm/meta-object/validate.js +20 -4
- package/esm/query-builder.js +14 -18
- package/esm/types/index.js +18 -0
- package/esm/types/introspection.js +54 -0
- package/esm/types/mutation.js +4 -0
- package/esm/types/query.js +4 -0
- package/esm/types/schema.js +5 -0
- package/esm/types/selection.js +4 -0
- package/esm/utils.js +69 -0
- package/generators/field-selector.d.ts +30 -0
- package/generators/field-selector.js +387 -0
- package/generators/index.d.ts +9 -0
- package/generators/index.js +42 -0
- package/generators/mutations.d.ts +30 -0
- package/generators/mutations.js +238 -0
- package/generators/naming-helpers.d.ts +48 -0
- package/generators/naming-helpers.js +169 -0
- package/generators/select.d.ts +39 -0
- package/generators/select.js +705 -0
- package/index.d.ts +19 -0
- package/index.js +34 -1
- package/introspect/index.d.ts +9 -0
- package/introspect/index.js +25 -0
- package/introspect/infer-tables.d.ts +42 -0
- package/introspect/infer-tables.js +700 -0
- package/introspect/schema-query.d.ts +20 -0
- package/introspect/schema-query.js +123 -0
- package/introspect/transform-schema.d.ts +86 -0
- package/introspect/transform-schema.js +281 -0
- package/introspect/transform.d.ts +20 -0
- package/introspect/transform.js +43 -0
- package/meta-object/convert.d.ts +3 -0
- package/meta-object/convert.js +3 -0
- package/meta-object/format.json +11 -41
- package/meta-object/validate.d.ts +8 -3
- package/meta-object/validate.js +20 -4
- package/package.json +4 -3
- package/query-builder.d.ts +11 -12
- package/query-builder.js +25 -29
- package/{types.d.ts → types/core.d.ts} +25 -18
- package/types/index.d.ts +12 -0
- package/types/index.js +34 -0
- package/types/introspection.d.ts +121 -0
- package/types/introspection.js +62 -0
- package/types/mutation.d.ts +45 -0
- package/types/mutation.js +5 -0
- package/types/query.d.ts +91 -0
- package/types/query.js +5 -0
- package/types/schema.d.ts +265 -0
- package/types/schema.js +6 -0
- package/types/selection.d.ts +43 -0
- package/types/selection.js +5 -0
- package/utils.d.ts +17 -0
- package/utils.js +72 -0
- /package/esm/{types.js → types/core.js} +0 -0
- /package/{types.js → types/core.js} +0 -0
package/custom-ast.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { FieldNode } from 'graphql';
|
|
2
|
+
import type { CleanField } from './types/schema';
|
|
3
|
+
import type { MetaField } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Get custom AST for MetaField type - handles PostgreSQL types that need subfield selections
|
|
6
|
+
*/
|
|
7
|
+
export declare function getCustomAst(fieldDefn?: MetaField): FieldNode | null;
|
|
3
8
|
/**
|
|
4
9
|
* Generate custom AST for CleanField type - handles GraphQL types that need subfield selections
|
|
5
10
|
*/
|
|
6
|
-
export declare function getCustomAstForCleanField(field: CleanField):
|
|
11
|
+
export declare function getCustomAstForCleanField(field: CleanField): FieldNode;
|
|
7
12
|
/**
|
|
8
13
|
* Check if a CleanField requires subfield selection based on its GraphQL type
|
|
9
14
|
*/
|
|
@@ -11,11 +16,20 @@ export declare function requiresSubfieldSelection(field: CleanField): boolean;
|
|
|
11
16
|
/**
|
|
12
17
|
* Generate AST for GeometryPoint type
|
|
13
18
|
*/
|
|
14
|
-
export declare function geometryPointAst(name: string):
|
|
19
|
+
export declare function geometryPointAst(name: string): FieldNode;
|
|
15
20
|
/**
|
|
16
21
|
* Generate AST for GeometryGeometryCollection type
|
|
17
22
|
*/
|
|
18
|
-
export declare function geometryCollectionAst(name: string):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
export declare function geometryCollectionAst(name: string): FieldNode;
|
|
24
|
+
/**
|
|
25
|
+
* Generate AST for generic geometry type (returns geojson)
|
|
26
|
+
*/
|
|
27
|
+
export declare function geometryAst(name: string): FieldNode;
|
|
28
|
+
/**
|
|
29
|
+
* Generate AST for interval type
|
|
30
|
+
*/
|
|
31
|
+
export declare function intervalAst(name: string): FieldNode;
|
|
32
|
+
/**
|
|
33
|
+
* Check if an object has interval type shape
|
|
34
|
+
*/
|
|
35
|
+
export declare function isIntervalType(obj: unknown): boolean;
|
package/custom-ast.js
CHANGED
|
@@ -43,6 +43,9 @@ exports.intervalAst = intervalAst;
|
|
|
43
43
|
exports.isIntervalType = isIntervalType;
|
|
44
44
|
const t = __importStar(require("gql-ast"));
|
|
45
45
|
const graphql_1 = require("graphql");
|
|
46
|
+
/**
|
|
47
|
+
* Get custom AST for MetaField type - handles PostgreSQL types that need subfield selections
|
|
48
|
+
*/
|
|
46
49
|
function getCustomAst(fieldDefn) {
|
|
47
50
|
if (!fieldDefn) {
|
|
48
51
|
return null;
|
|
@@ -152,6 +155,7 @@ function geometryCollectionAst(name) {
|
|
|
152
155
|
t.field({
|
|
153
156
|
name: 'geometries',
|
|
154
157
|
selectionSet: t.selectionSet({
|
|
158
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
159
|
selections: [inlineFragment], // gql-ast limitation with inline fragments
|
|
156
160
|
}),
|
|
157
161
|
}),
|
|
@@ -159,6 +163,9 @@ function geometryCollectionAst(name) {
|
|
|
159
163
|
}),
|
|
160
164
|
});
|
|
161
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Generate AST for generic geometry type (returns geojson)
|
|
168
|
+
*/
|
|
162
169
|
function geometryAst(name) {
|
|
163
170
|
return t.field({
|
|
164
171
|
name,
|
|
@@ -167,6 +174,9 @@ function geometryAst(name) {
|
|
|
167
174
|
}),
|
|
168
175
|
});
|
|
169
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Generate AST for interval type
|
|
179
|
+
*/
|
|
170
180
|
function intervalAst(name) {
|
|
171
181
|
return t.field({
|
|
172
182
|
name,
|
|
@@ -185,6 +195,11 @@ function intervalAst(name) {
|
|
|
185
195
|
function toFieldArray(strArr) {
|
|
186
196
|
return strArr.map((fieldName) => t.field({ name: fieldName }));
|
|
187
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if an object has interval type shape
|
|
200
|
+
*/
|
|
188
201
|
function isIntervalType(obj) {
|
|
189
|
-
|
|
202
|
+
if (!obj || typeof obj !== 'object')
|
|
203
|
+
return false;
|
|
204
|
+
return ['days', 'hours', 'minutes', 'months', 'seconds', 'years'].every((key) => Object.prototype.hasOwnProperty.call(obj, key));
|
|
190
205
|
}
|
package/esm/ast.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as t from 'gql-ast';
|
|
2
|
-
import { OperationTypeNode
|
|
3
|
-
import { camelize, singularize } from '
|
|
2
|
+
import { OperationTypeNode } from 'graphql';
|
|
3
|
+
import { camelize, singularize } from 'inflekt';
|
|
4
4
|
import { getCustomAst } from './custom-ast';
|
|
5
5
|
const NON_MUTABLE_PROPS = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
|
|
6
6
|
const objectToArray = (obj) => Object.keys(obj).map((k) => ({
|
|
@@ -48,7 +48,7 @@ const createGqlMutation = ({ operationName, mutationName, selectArgs, selections
|
|
|
48
48
|
],
|
|
49
49
|
});
|
|
50
50
|
};
|
|
51
|
-
export const getAll = ({ queryName, operationName,
|
|
51
|
+
export const getAll = ({ queryName, operationName, selection, }) => {
|
|
52
52
|
const selections = getSelections(selection);
|
|
53
53
|
const opSel = [
|
|
54
54
|
t.field({
|
|
@@ -170,7 +170,10 @@ export const getMany = ({ builder, queryName, operationName, query, selection, }
|
|
|
170
170
|
t.argument({ name: 'offset', value: t.variable({ name: 'offset' }) }),
|
|
171
171
|
t.argument({ name: 'after', value: t.variable({ name: 'after' }) }),
|
|
172
172
|
t.argument({ name: 'before', value: t.variable({ name: 'before' }) }),
|
|
173
|
-
t.argument({
|
|
173
|
+
t.argument({
|
|
174
|
+
name: 'condition',
|
|
175
|
+
value: t.variable({ name: 'condition' }),
|
|
176
|
+
}),
|
|
174
177
|
t.argument({ name: 'filter', value: t.variable({ name: 'filter' }) }),
|
|
175
178
|
t.argument({ name: 'orderBy', value: t.variable({ name: 'orderBy' }) }),
|
|
176
179
|
];
|
|
@@ -419,9 +422,21 @@ export const deleteOne = ({ mutationName, operationName, mutation, }) => {
|
|
|
419
422
|
};
|
|
420
423
|
export function getSelections(selection = []) {
|
|
421
424
|
const selectionAst = (field) => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
+
if (typeof field === 'string') {
|
|
426
|
+
return t.field({ name: field });
|
|
427
|
+
}
|
|
428
|
+
// Check if fieldDefn has MetaField shape (has type.pgType)
|
|
429
|
+
const fieldDefn = field.fieldDefn;
|
|
430
|
+
if (fieldDefn &&
|
|
431
|
+
'type' in fieldDefn &&
|
|
432
|
+
fieldDefn.type &&
|
|
433
|
+
typeof fieldDefn.type === 'object' &&
|
|
434
|
+
'pgType' in fieldDefn.type) {
|
|
435
|
+
const customAst = getCustomAst(fieldDefn);
|
|
436
|
+
if (customAst)
|
|
437
|
+
return customAst;
|
|
438
|
+
}
|
|
439
|
+
return t.field({ name: field.name });
|
|
425
440
|
};
|
|
426
441
|
return selection
|
|
427
442
|
.map((selectionDefn) => {
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error handling for GraphQL operations
|
|
3
|
+
* Provides consistent error types and parsing for PostGraphile responses
|
|
4
|
+
*/
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Error Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
export const DataErrorType = {
|
|
9
|
+
// Network/Connection errors
|
|
10
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
11
|
+
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
|
12
|
+
// Validation errors
|
|
13
|
+
VALIDATION_FAILED: 'VALIDATION_FAILED',
|
|
14
|
+
REQUIRED_FIELD_MISSING: 'REQUIRED_FIELD_MISSING',
|
|
15
|
+
INVALID_MUTATION_DATA: 'INVALID_MUTATION_DATA',
|
|
16
|
+
// Query errors
|
|
17
|
+
QUERY_GENERATION_FAILED: 'QUERY_GENERATION_FAILED',
|
|
18
|
+
QUERY_EXECUTION_FAILED: 'QUERY_EXECUTION_FAILED',
|
|
19
|
+
// Permission errors
|
|
20
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
21
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
22
|
+
// Schema errors
|
|
23
|
+
TABLE_NOT_FOUND: 'TABLE_NOT_FOUND',
|
|
24
|
+
// Request errors
|
|
25
|
+
BAD_REQUEST: 'BAD_REQUEST',
|
|
26
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
27
|
+
// GraphQL-specific errors
|
|
28
|
+
GRAPHQL_ERROR: 'GRAPHQL_ERROR',
|
|
29
|
+
// PostgreSQL constraint errors (surfaced via PostGraphile)
|
|
30
|
+
UNIQUE_VIOLATION: 'UNIQUE_VIOLATION',
|
|
31
|
+
FOREIGN_KEY_VIOLATION: 'FOREIGN_KEY_VIOLATION',
|
|
32
|
+
NOT_NULL_VIOLATION: 'NOT_NULL_VIOLATION',
|
|
33
|
+
CHECK_VIOLATION: 'CHECK_VIOLATION',
|
|
34
|
+
EXCLUSION_VIOLATION: 'EXCLUSION_VIOLATION',
|
|
35
|
+
// Generic errors
|
|
36
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Standard error class for data layer operations
|
|
40
|
+
*/
|
|
41
|
+
export class DataError extends Error {
|
|
42
|
+
type;
|
|
43
|
+
code;
|
|
44
|
+
originalError;
|
|
45
|
+
context;
|
|
46
|
+
tableName;
|
|
47
|
+
fieldName;
|
|
48
|
+
constraint;
|
|
49
|
+
constructor(type, message, options = {}) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'DataError';
|
|
52
|
+
this.type = type;
|
|
53
|
+
this.code = options.code;
|
|
54
|
+
this.originalError = options.originalError;
|
|
55
|
+
this.context = options.context;
|
|
56
|
+
this.tableName = options.tableName;
|
|
57
|
+
this.fieldName = options.fieldName;
|
|
58
|
+
this.constraint = options.constraint;
|
|
59
|
+
if (Error.captureStackTrace) {
|
|
60
|
+
Error.captureStackTrace(this, DataError);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
getUserMessage() {
|
|
64
|
+
switch (this.type) {
|
|
65
|
+
case DataErrorType.NETWORK_ERROR:
|
|
66
|
+
return 'Network error. Please check your connection and try again.';
|
|
67
|
+
case DataErrorType.TIMEOUT_ERROR:
|
|
68
|
+
return 'Request timed out. Please try again.';
|
|
69
|
+
case DataErrorType.UNAUTHORIZED:
|
|
70
|
+
return 'You are not authorized. Please log in and try again.';
|
|
71
|
+
case DataErrorType.FORBIDDEN:
|
|
72
|
+
return 'You do not have permission to access this resource.';
|
|
73
|
+
case DataErrorType.VALIDATION_FAILED:
|
|
74
|
+
return 'Validation failed. Please check your input and try again.';
|
|
75
|
+
case DataErrorType.REQUIRED_FIELD_MISSING:
|
|
76
|
+
return this.fieldName
|
|
77
|
+
? `The field "${this.fieldName}" is required.`
|
|
78
|
+
: 'A required field is missing.';
|
|
79
|
+
case DataErrorType.UNIQUE_VIOLATION:
|
|
80
|
+
return this.fieldName
|
|
81
|
+
? `A record with this ${this.fieldName} already exists.`
|
|
82
|
+
: 'A record with this value already exists.';
|
|
83
|
+
case DataErrorType.FOREIGN_KEY_VIOLATION:
|
|
84
|
+
return 'This record references a record that does not exist.';
|
|
85
|
+
case DataErrorType.NOT_NULL_VIOLATION:
|
|
86
|
+
return this.fieldName
|
|
87
|
+
? `The field "${this.fieldName}" cannot be empty.`
|
|
88
|
+
: 'A required field cannot be empty.';
|
|
89
|
+
case DataErrorType.CHECK_VIOLATION:
|
|
90
|
+
return this.fieldName
|
|
91
|
+
? `The value for "${this.fieldName}" is not valid.`
|
|
92
|
+
: 'The value does not meet the required constraints.';
|
|
93
|
+
default:
|
|
94
|
+
return this.message || 'An unexpected error occurred.';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
isRetryable() {
|
|
98
|
+
return (this.type === DataErrorType.NETWORK_ERROR ||
|
|
99
|
+
this.type === DataErrorType.TIMEOUT_ERROR);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// PostgreSQL Error Codes
|
|
104
|
+
// ============================================================================
|
|
105
|
+
export const PG_ERROR_CODES = {
|
|
106
|
+
UNIQUE_VIOLATION: '23505',
|
|
107
|
+
FOREIGN_KEY_VIOLATION: '23503',
|
|
108
|
+
NOT_NULL_VIOLATION: '23502',
|
|
109
|
+
CHECK_VIOLATION: '23514',
|
|
110
|
+
EXCLUSION_VIOLATION: '23P01',
|
|
111
|
+
NUMERIC_VALUE_OUT_OF_RANGE: '22003',
|
|
112
|
+
STRING_DATA_RIGHT_TRUNCATION: '22001',
|
|
113
|
+
INVALID_TEXT_REPRESENTATION: '22P02',
|
|
114
|
+
DATETIME_FIELD_OVERFLOW: '22008',
|
|
115
|
+
UNDEFINED_TABLE: '42P01',
|
|
116
|
+
UNDEFINED_COLUMN: '42703',
|
|
117
|
+
INSUFFICIENT_PRIVILEGE: '42501',
|
|
118
|
+
};
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Error Factory
|
|
121
|
+
// ============================================================================
|
|
122
|
+
export const createError = {
|
|
123
|
+
network: (originalError) => new DataError(DataErrorType.NETWORK_ERROR, 'Network error occurred', {
|
|
124
|
+
originalError,
|
|
125
|
+
}),
|
|
126
|
+
timeout: (originalError) => new DataError(DataErrorType.TIMEOUT_ERROR, 'Request timed out', {
|
|
127
|
+
originalError,
|
|
128
|
+
}),
|
|
129
|
+
unauthorized: (message = 'Authentication required') => new DataError(DataErrorType.UNAUTHORIZED, message),
|
|
130
|
+
forbidden: (message = 'Access forbidden') => new DataError(DataErrorType.FORBIDDEN, message),
|
|
131
|
+
badRequest: (message, code) => new DataError(DataErrorType.BAD_REQUEST, message, { code }),
|
|
132
|
+
notFound: (message = 'Resource not found') => new DataError(DataErrorType.NOT_FOUND, message),
|
|
133
|
+
graphql: (message, code) => new DataError(DataErrorType.GRAPHQL_ERROR, message, { code }),
|
|
134
|
+
uniqueViolation: (message, fieldName, constraint) => new DataError(DataErrorType.UNIQUE_VIOLATION, message, {
|
|
135
|
+
fieldName,
|
|
136
|
+
constraint,
|
|
137
|
+
code: '23505',
|
|
138
|
+
}),
|
|
139
|
+
foreignKeyViolation: (message, fieldName, constraint) => new DataError(DataErrorType.FOREIGN_KEY_VIOLATION, message, {
|
|
140
|
+
fieldName,
|
|
141
|
+
constraint,
|
|
142
|
+
code: '23503',
|
|
143
|
+
}),
|
|
144
|
+
notNullViolation: (message, fieldName, constraint) => new DataError(DataErrorType.NOT_NULL_VIOLATION, message, {
|
|
145
|
+
fieldName,
|
|
146
|
+
constraint,
|
|
147
|
+
code: '23502',
|
|
148
|
+
}),
|
|
149
|
+
unknown: (originalError) => new DataError(DataErrorType.UNKNOWN_ERROR, originalError.message, {
|
|
150
|
+
originalError,
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
function parseGraphQLErrorCode(code) {
|
|
154
|
+
if (!code)
|
|
155
|
+
return DataErrorType.UNKNOWN_ERROR;
|
|
156
|
+
const normalized = code.toUpperCase();
|
|
157
|
+
// GraphQL standard codes
|
|
158
|
+
if (normalized === 'UNAUTHENTICATED')
|
|
159
|
+
return DataErrorType.UNAUTHORIZED;
|
|
160
|
+
if (normalized === 'FORBIDDEN')
|
|
161
|
+
return DataErrorType.FORBIDDEN;
|
|
162
|
+
if (normalized === 'GRAPHQL_VALIDATION_FAILED')
|
|
163
|
+
return DataErrorType.QUERY_GENERATION_FAILED;
|
|
164
|
+
// PostgreSQL SQLSTATE codes
|
|
165
|
+
if (code === PG_ERROR_CODES.UNIQUE_VIOLATION)
|
|
166
|
+
return DataErrorType.UNIQUE_VIOLATION;
|
|
167
|
+
if (code === PG_ERROR_CODES.FOREIGN_KEY_VIOLATION)
|
|
168
|
+
return DataErrorType.FOREIGN_KEY_VIOLATION;
|
|
169
|
+
if (code === PG_ERROR_CODES.NOT_NULL_VIOLATION)
|
|
170
|
+
return DataErrorType.NOT_NULL_VIOLATION;
|
|
171
|
+
if (code === PG_ERROR_CODES.CHECK_VIOLATION)
|
|
172
|
+
return DataErrorType.CHECK_VIOLATION;
|
|
173
|
+
if (code === PG_ERROR_CODES.EXCLUSION_VIOLATION)
|
|
174
|
+
return DataErrorType.EXCLUSION_VIOLATION;
|
|
175
|
+
return DataErrorType.UNKNOWN_ERROR;
|
|
176
|
+
}
|
|
177
|
+
function classifyByMessage(message) {
|
|
178
|
+
const lower = message.toLowerCase();
|
|
179
|
+
if (lower.includes('timeout') || lower.includes('timed out')) {
|
|
180
|
+
return DataErrorType.TIMEOUT_ERROR;
|
|
181
|
+
}
|
|
182
|
+
if (lower.includes('network') ||
|
|
183
|
+
lower.includes('fetch') ||
|
|
184
|
+
lower.includes('failed to fetch')) {
|
|
185
|
+
return DataErrorType.NETWORK_ERROR;
|
|
186
|
+
}
|
|
187
|
+
if (lower.includes('unauthorized') ||
|
|
188
|
+
lower.includes('authentication required')) {
|
|
189
|
+
return DataErrorType.UNAUTHORIZED;
|
|
190
|
+
}
|
|
191
|
+
if (lower.includes('forbidden') || lower.includes('permission')) {
|
|
192
|
+
return DataErrorType.FORBIDDEN;
|
|
193
|
+
}
|
|
194
|
+
if (lower.includes('duplicate key') || lower.includes('already exists')) {
|
|
195
|
+
return DataErrorType.UNIQUE_VIOLATION;
|
|
196
|
+
}
|
|
197
|
+
if (lower.includes('foreign key constraint')) {
|
|
198
|
+
return DataErrorType.FOREIGN_KEY_VIOLATION;
|
|
199
|
+
}
|
|
200
|
+
if (lower.includes('not-null constraint') ||
|
|
201
|
+
lower.includes('null value in column')) {
|
|
202
|
+
return DataErrorType.NOT_NULL_VIOLATION;
|
|
203
|
+
}
|
|
204
|
+
return DataErrorType.UNKNOWN_ERROR;
|
|
205
|
+
}
|
|
206
|
+
function extractFieldFromError(message, constraint, column) {
|
|
207
|
+
if (column)
|
|
208
|
+
return column;
|
|
209
|
+
const columnMatch = message.match(/column\s+"?([a-z_][a-z0-9_]*)"?/i);
|
|
210
|
+
if (columnMatch)
|
|
211
|
+
return columnMatch[1];
|
|
212
|
+
if (constraint) {
|
|
213
|
+
const constraintMatch = constraint.match(/_([a-z_][a-z0-9_]*)_(?:key|fkey|check|pkey)$/i);
|
|
214
|
+
if (constraintMatch)
|
|
215
|
+
return constraintMatch[1];
|
|
216
|
+
}
|
|
217
|
+
const keyMatch = message.match(/Key\s+\(([a-z_][a-z0-9_]*)\)/i);
|
|
218
|
+
if (keyMatch)
|
|
219
|
+
return keyMatch[1];
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Parse any error into a DataError
|
|
224
|
+
*/
|
|
225
|
+
export function parseGraphQLError(error) {
|
|
226
|
+
if (error instanceof DataError) {
|
|
227
|
+
return error;
|
|
228
|
+
}
|
|
229
|
+
// GraphQL error object
|
|
230
|
+
if (error &&
|
|
231
|
+
typeof error === 'object' &&
|
|
232
|
+
'message' in error &&
|
|
233
|
+
typeof error.message === 'string') {
|
|
234
|
+
const gqlError = error;
|
|
235
|
+
const extCode = gqlError.extensions?.code;
|
|
236
|
+
const mappedType = parseGraphQLErrorCode(extCode);
|
|
237
|
+
const column = gqlError.extensions?.column;
|
|
238
|
+
const constraint = gqlError.extensions?.constraint;
|
|
239
|
+
const fieldName = extractFieldFromError(gqlError.message, constraint, column);
|
|
240
|
+
if (mappedType !== DataErrorType.UNKNOWN_ERROR) {
|
|
241
|
+
return new DataError(mappedType, gqlError.message, {
|
|
242
|
+
code: extCode,
|
|
243
|
+
fieldName,
|
|
244
|
+
constraint,
|
|
245
|
+
context: gqlError.extensions,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// Fallback: classify by message
|
|
249
|
+
const fallbackType = classifyByMessage(gqlError.message);
|
|
250
|
+
return new DataError(fallbackType, gqlError.message, {
|
|
251
|
+
code: extCode,
|
|
252
|
+
fieldName,
|
|
253
|
+
constraint,
|
|
254
|
+
context: gqlError.extensions,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// Standard Error
|
|
258
|
+
if (error instanceof Error) {
|
|
259
|
+
const type = classifyByMessage(error.message);
|
|
260
|
+
return new DataError(type, error.message, { originalError: error });
|
|
261
|
+
}
|
|
262
|
+
// Unknown
|
|
263
|
+
const message = typeof error === 'string' ? error : 'Unknown error occurred';
|
|
264
|
+
return new DataError(DataErrorType.UNKNOWN_ERROR, message);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Check if value is a DataError
|
|
268
|
+
*/
|
|
269
|
+
export function isDataError(error) {
|
|
270
|
+
return error instanceof DataError;
|
|
271
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL execution utilities
|
|
3
|
+
*/
|
|
4
|
+
import { print } from 'graphql';
|
|
5
|
+
import { createError, parseGraphQLError } from './error';
|
|
6
|
+
import { TypedDocumentString } from './typed-document';
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Helpers
|
|
9
|
+
// ============================================================================
|
|
10
|
+
function documentToString(document) {
|
|
11
|
+
if (typeof document === 'string')
|
|
12
|
+
return document;
|
|
13
|
+
if (document instanceof TypedDocumentString)
|
|
14
|
+
return document.toString();
|
|
15
|
+
// DocumentNode
|
|
16
|
+
if (document && typeof document === 'object' && 'kind' in document) {
|
|
17
|
+
return print(document);
|
|
18
|
+
}
|
|
19
|
+
throw createError.badRequest('Invalid GraphQL document');
|
|
20
|
+
}
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Execute Function
|
|
23
|
+
// ============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Execute a GraphQL operation against an endpoint
|
|
26
|
+
*/
|
|
27
|
+
export async function execute(endpoint, document, variables, options = {}) {
|
|
28
|
+
const { headers = {}, timeout = 30000, signal } = options;
|
|
29
|
+
// Create timeout controller
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
32
|
+
// Combine signals if provided
|
|
33
|
+
const combinedSignal = signal
|
|
34
|
+
? AbortSignal.any([signal, controller.signal])
|
|
35
|
+
: controller.signal;
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(endpoint, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
Accept: 'application/graphql-response+json, application/json',
|
|
42
|
+
...headers,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
query: documentToString(document),
|
|
46
|
+
...(variables !== undefined && { variables }),
|
|
47
|
+
}),
|
|
48
|
+
signal: combinedSignal,
|
|
49
|
+
});
|
|
50
|
+
clearTimeout(timeoutId);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw await handleHttpError(response);
|
|
53
|
+
}
|
|
54
|
+
const result = await response.json();
|
|
55
|
+
if (result.errors?.length) {
|
|
56
|
+
throw parseGraphQLError(result.errors[0]);
|
|
57
|
+
}
|
|
58
|
+
return result.data;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
clearTimeout(timeoutId);
|
|
62
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
63
|
+
throw createError.timeout();
|
|
64
|
+
}
|
|
65
|
+
if (error instanceof Error && error.message.includes('fetch')) {
|
|
66
|
+
throw createError.network(error);
|
|
67
|
+
}
|
|
68
|
+
throw parseGraphQLError(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function handleHttpError(response) {
|
|
72
|
+
const { status, statusText } = response;
|
|
73
|
+
if (status === 401) {
|
|
74
|
+
return createError.unauthorized('Authentication required');
|
|
75
|
+
}
|
|
76
|
+
if (status === 403) {
|
|
77
|
+
return createError.forbidden('Access forbidden');
|
|
78
|
+
}
|
|
79
|
+
if (status === 404) {
|
|
80
|
+
return createError.notFound('GraphQL endpoint not found');
|
|
81
|
+
}
|
|
82
|
+
// Try to extract error from response body
|
|
83
|
+
try {
|
|
84
|
+
const body = await response.json();
|
|
85
|
+
if (body.errors?.length) {
|
|
86
|
+
return parseGraphQLError(body.errors[0]);
|
|
87
|
+
}
|
|
88
|
+
if (body.message) {
|
|
89
|
+
return createError.badRequest(body.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Couldn't parse response
|
|
94
|
+
}
|
|
95
|
+
return createError.badRequest(`Request failed: ${status} ${statusText}`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create a GraphQL client instance
|
|
99
|
+
*/
|
|
100
|
+
export function createGraphQLClient(options) {
|
|
101
|
+
const { endpoint, headers: defaultHeaders = {}, timeout: defaultTimeout = 30000, } = options;
|
|
102
|
+
return {
|
|
103
|
+
/**
|
|
104
|
+
* Execute a GraphQL operation
|
|
105
|
+
*/
|
|
106
|
+
async execute(document, variables, options = {}) {
|
|
107
|
+
return execute(endpoint, document, variables, {
|
|
108
|
+
headers: { ...defaultHeaders, ...options.headers },
|
|
109
|
+
timeout: options.timeout ?? defaultTimeout,
|
|
110
|
+
signal: options.signal,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
/**
|
|
114
|
+
* Get the endpoint URL
|
|
115
|
+
*/
|
|
116
|
+
getEndpoint() {
|
|
117
|
+
return endpoint;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client barrel export
|
|
3
|
+
*
|
|
4
|
+
* Re-exports client utilities for GraphQL execution and error handling.
|
|
5
|
+
*/
|
|
6
|
+
export { TypedDocumentString, } from './typed-document';
|
|
7
|
+
export { DataError, DataErrorType, PG_ERROR_CODES, createError, parseGraphQLError, isDataError, } from './error';
|
|
8
|
+
export { execute, createGraphQLClient, } from './execute';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypedDocumentString - Type-safe wrapper for GraphQL documents
|
|
3
|
+
* Compatible with GraphQL codegen client preset
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Enhanced TypedDocumentString with type inference capabilities
|
|
7
|
+
* Compatible with GraphQL codegen client preset
|
|
8
|
+
*/
|
|
9
|
+
export class TypedDocumentString extends String {
|
|
10
|
+
/** Same shape as the codegen implementation for structural typing */
|
|
11
|
+
__apiType;
|
|
12
|
+
__meta__;
|
|
13
|
+
value;
|
|
14
|
+
constructor(value, meta) {
|
|
15
|
+
super(value);
|
|
16
|
+
this.value = value;
|
|
17
|
+
this.__meta__ = {
|
|
18
|
+
hash: this.generateHash(value),
|
|
19
|
+
...meta,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
generateHash(value) {
|
|
23
|
+
let hash = 0;
|
|
24
|
+
for (let i = 0; i < value.length; i++) {
|
|
25
|
+
const char = value.charCodeAt(i);
|
|
26
|
+
hash = (hash << 5) - hash + char;
|
|
27
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
28
|
+
}
|
|
29
|
+
return Math.abs(hash).toString(36);
|
|
30
|
+
}
|
|
31
|
+
toString() {
|
|
32
|
+
return this.value;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the hash for caching purposes
|
|
36
|
+
*/
|
|
37
|
+
getHash() {
|
|
38
|
+
return this.__meta__?.hash;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/esm/custom-ast.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as t from 'gql-ast';
|
|
2
2
|
import { Kind } from 'graphql';
|
|
3
|
+
/**
|
|
4
|
+
* Get custom AST for MetaField type - handles PostgreSQL types that need subfield selections
|
|
5
|
+
*/
|
|
3
6
|
export function getCustomAst(fieldDefn) {
|
|
4
7
|
if (!fieldDefn) {
|
|
5
8
|
return null;
|
|
@@ -109,6 +112,7 @@ export function geometryCollectionAst(name) {
|
|
|
109
112
|
t.field({
|
|
110
113
|
name: 'geometries',
|
|
111
114
|
selectionSet: t.selectionSet({
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
116
|
selections: [inlineFragment], // gql-ast limitation with inline fragments
|
|
113
117
|
}),
|
|
114
118
|
}),
|
|
@@ -116,6 +120,9 @@ export function geometryCollectionAst(name) {
|
|
|
116
120
|
}),
|
|
117
121
|
});
|
|
118
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Generate AST for generic geometry type (returns geojson)
|
|
125
|
+
*/
|
|
119
126
|
export function geometryAst(name) {
|
|
120
127
|
return t.field({
|
|
121
128
|
name,
|
|
@@ -124,6 +131,9 @@ export function geometryAst(name) {
|
|
|
124
131
|
}),
|
|
125
132
|
});
|
|
126
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Generate AST for interval type
|
|
136
|
+
*/
|
|
127
137
|
export function intervalAst(name) {
|
|
128
138
|
return t.field({
|
|
129
139
|
name,
|
|
@@ -142,6 +152,11 @@ export function intervalAst(name) {
|
|
|
142
152
|
function toFieldArray(strArr) {
|
|
143
153
|
return strArr.map((fieldName) => t.field({ name: fieldName }));
|
|
144
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if an object has interval type shape
|
|
157
|
+
*/
|
|
145
158
|
export function isIntervalType(obj) {
|
|
146
|
-
|
|
159
|
+
if (!obj || typeof obj !== 'object')
|
|
160
|
+
return false;
|
|
161
|
+
return ['days', 'hours', 'minutes', 'months', 'seconds', 'years'].every((key) => Object.prototype.hasOwnProperty.call(obj, key));
|
|
147
162
|
}
|