@constructive-io/graphql-query 4.5.4 → 4.7.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 +25 -8
- package/client/error.d.ts +34 -78
- package/client/error.js +54 -264
- package/client/index.d.ts +1 -1
- package/client/index.js +10 -5
- package/esm/client/error.js +44 -261
- package/esm/client/index.js +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ This package is the **canonical source** for PostGraphile query generation logic
|
|
|
28
28
|
- **Mutation generators** — `buildPostGraphileCreate`, `buildPostGraphileUpdate`, `buildPostGraphileDelete`
|
|
29
29
|
- **Introspection pipeline** — `inferTablesFromIntrospection` to convert a GraphQL schema into `CleanTable` metadata
|
|
30
30
|
- **AST builders** — low-level `getAll`, `getMany`, `getOne`, `createOne`, `patchOne`, `deleteOne`
|
|
31
|
-
- **Client utilities** — `TypedDocumentString`, `execute`, `
|
|
31
|
+
- **Client utilities** — `TypedDocumentString`, `execute`, and `@constructive-io/errors` re-exports (`ConstructiveError`, `parseGraphQLError`) for type-safe execution and error handling
|
|
32
32
|
- **Naming helpers** — server-aware inflection functions that respect PostGraphile's schema naming
|
|
33
33
|
|
|
34
34
|
All modules are **browser-safe** (no Node.js APIs). `@constructive-io/graphql-codegen` depends on this package for the core logic and adds Node.js-only features (CLI, file output, watch mode).
|
|
@@ -329,20 +329,36 @@ const { data, errors } = await client.execute(query, {
|
|
|
329
329
|
|
|
330
330
|
## Error Handling
|
|
331
331
|
|
|
332
|
+
Error handling is backed by [`@constructive-io/errors`](../../packages/errors), the
|
|
333
|
+
single source of truth for error codes, their public/internal class, message
|
|
334
|
+
copy (i18n), and cross-source parsing. There is no separate client-side error
|
|
335
|
+
catalog or message string-matching — the client re-exports the canonical
|
|
336
|
+
`ConstructiveError`, `parse`, `format`, and helpers.
|
|
337
|
+
|
|
332
338
|
```ts
|
|
333
|
-
import {
|
|
339
|
+
import {
|
|
340
|
+
ConstructiveError,
|
|
341
|
+
parseGraphQLError,
|
|
342
|
+
format,
|
|
343
|
+
isRetryable,
|
|
344
|
+
} from '@constructive-io/graphql-query';
|
|
334
345
|
|
|
335
346
|
try {
|
|
336
347
|
const result = await client.execute(createQuery, { input: { user: { name: 'Alice' } } });
|
|
337
348
|
if (result.errors) {
|
|
349
|
+
// Normalize any error into a ConstructiveError (machine code + context + class).
|
|
338
350
|
const error = parseGraphQLError(result.errors[0]);
|
|
339
|
-
if (error.
|
|
340
|
-
console.log('Duplicate entry:', error.
|
|
351
|
+
if (error.code === 'UNIQUE_VIOLATION') {
|
|
352
|
+
console.log('Duplicate entry:', error.context.constraint);
|
|
341
353
|
}
|
|
342
354
|
}
|
|
343
355
|
} catch (err) {
|
|
344
|
-
if (err instanceof
|
|
345
|
-
|
|
356
|
+
if (err instanceof ConstructiveError) {
|
|
357
|
+
// Localized, user-facing copy for public codes; retry hint for transport errors.
|
|
358
|
+
console.log(err.code, format(err.code, err.context));
|
|
359
|
+
if (isRetryable(err)) {
|
|
360
|
+
// network/timeout/rate-limit — safe to retry
|
|
361
|
+
}
|
|
346
362
|
}
|
|
347
363
|
}
|
|
348
364
|
```
|
|
@@ -441,8 +457,8 @@ GraphQL Schema (introspection or _meta)
|
|
|
441
457
|
| `TypedDocumentString` | Type-safe GraphQL document wrapper (compatible with codegen client) |
|
|
442
458
|
| `createGraphQLClient(options)` | Create a typed GraphQL client |
|
|
443
459
|
| `execute(url, query, variables, options?)` | Execute a GraphQL query |
|
|
444
|
-
| `
|
|
445
|
-
| `parseGraphQLError(error)` | Parse
|
|
460
|
+
| `ConstructiveError` | Canonical error class (`code`, `context`, `class`, `http`) from `@constructive-io/errors` |
|
|
461
|
+
| `parseGraphQLError(error)` | Parse any error (pg / GraphQL / message / SQLSTATE) into a `ConstructiveError` |
|
|
446
462
|
|
|
447
463
|
### Naming Helpers
|
|
448
464
|
|
|
@@ -504,6 +520,7 @@ Common issues and solutions for pgpm, PostgreSQL, and testing.
|
|
|
504
520
|
### 🧪 Testing
|
|
505
521
|
|
|
506
522
|
* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
|
|
523
|
+
* [pglite-test](https://github.com/constructive-io/constructive/tree/main/postgres/pglite-test): **🪶 Drop-in pgsql-test replacement backed by PGlite** — in-process Postgres, no server required, instance-per-suite isolation.
|
|
507
524
|
* [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
|
|
508
525
|
* [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
|
|
509
526
|
* [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
|
package/client/error.d.ts
CHANGED
|
@@ -1,83 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Error handling for GraphQL operations
|
|
3
|
-
*
|
|
2
|
+
* Error handling for GraphQL operations.
|
|
3
|
+
*
|
|
4
|
+
* This is a thin client-side adapter over the canonical `@constructive-io/errors`
|
|
5
|
+
* package. The package owns the single source of truth for error codes, their
|
|
6
|
+
* public/internal class, message copy (i18n), and cross-source parsing (pg
|
|
7
|
+
* `DatabaseError`, GraphQL `extensions`, structured `DETAIL`, message tokens,
|
|
8
|
+
* and native SQLSTATE). There is no separate client-side error catalog or
|
|
9
|
+
* message string-matching anymore.
|
|
4
10
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly VALIDATION_FAILED: "VALIDATION_FAILED";
|
|
9
|
-
readonly REQUIRED_FIELD_MISSING: "REQUIRED_FIELD_MISSING";
|
|
10
|
-
readonly INVALID_MUTATION_DATA: "INVALID_MUTATION_DATA";
|
|
11
|
-
readonly QUERY_GENERATION_FAILED: "QUERY_GENERATION_FAILED";
|
|
12
|
-
readonly QUERY_EXECUTION_FAILED: "QUERY_EXECUTION_FAILED";
|
|
13
|
-
readonly UNAUTHORIZED: "UNAUTHORIZED";
|
|
14
|
-
readonly FORBIDDEN: "FORBIDDEN";
|
|
15
|
-
readonly TABLE_NOT_FOUND: "TABLE_NOT_FOUND";
|
|
16
|
-
readonly BAD_REQUEST: "BAD_REQUEST";
|
|
17
|
-
readonly NOT_FOUND: "NOT_FOUND";
|
|
18
|
-
readonly GRAPHQL_ERROR: "GRAPHQL_ERROR";
|
|
19
|
-
readonly UNIQUE_VIOLATION: "UNIQUE_VIOLATION";
|
|
20
|
-
readonly FOREIGN_KEY_VIOLATION: "FOREIGN_KEY_VIOLATION";
|
|
21
|
-
readonly NOT_NULL_VIOLATION: "NOT_NULL_VIOLATION";
|
|
22
|
-
readonly CHECK_VIOLATION: "CHECK_VIOLATION";
|
|
23
|
-
readonly EXCLUSION_VIOLATION: "EXCLUSION_VIOLATION";
|
|
24
|
-
readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
|
|
25
|
-
};
|
|
26
|
-
export type DataErrorType = (typeof DataErrorType)[keyof typeof DataErrorType];
|
|
27
|
-
export interface DataErrorOptions {
|
|
28
|
-
tableName?: string;
|
|
29
|
-
fieldName?: string;
|
|
30
|
-
constraint?: string;
|
|
31
|
-
originalError?: Error;
|
|
32
|
-
code?: string;
|
|
33
|
-
context?: Record<string, unknown>;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Standard error class for data layer operations
|
|
37
|
-
*/
|
|
38
|
-
export declare class DataError extends Error {
|
|
39
|
-
readonly type: DataErrorType;
|
|
40
|
-
readonly code?: string;
|
|
41
|
-
readonly originalError?: Error;
|
|
42
|
-
readonly context?: Record<string, unknown>;
|
|
43
|
-
readonly tableName?: string;
|
|
44
|
-
readonly fieldName?: string;
|
|
45
|
-
readonly constraint?: string;
|
|
46
|
-
constructor(type: DataErrorType, message: string, options?: DataErrorOptions);
|
|
47
|
-
getUserMessage(): string;
|
|
48
|
-
isRetryable(): boolean;
|
|
49
|
-
}
|
|
50
|
-
export declare const PG_ERROR_CODES: {
|
|
51
|
-
readonly UNIQUE_VIOLATION: "23505";
|
|
52
|
-
readonly FOREIGN_KEY_VIOLATION: "23503";
|
|
53
|
-
readonly NOT_NULL_VIOLATION: "23502";
|
|
54
|
-
readonly CHECK_VIOLATION: "23514";
|
|
55
|
-
readonly EXCLUSION_VIOLATION: "23P01";
|
|
56
|
-
readonly NUMERIC_VALUE_OUT_OF_RANGE: "22003";
|
|
57
|
-
readonly STRING_DATA_RIGHT_TRUNCATION: "22001";
|
|
58
|
-
readonly INVALID_TEXT_REPRESENTATION: "22P02";
|
|
59
|
-
readonly DATETIME_FIELD_OVERFLOW: "22008";
|
|
60
|
-
readonly UNDEFINED_TABLE: "42P01";
|
|
61
|
-
readonly UNDEFINED_COLUMN: "42703";
|
|
62
|
-
readonly INSUFFICIENT_PRIVILEGE: "42501";
|
|
63
|
-
};
|
|
64
|
-
export declare const createError: {
|
|
65
|
-
network: (originalError?: Error) => DataError;
|
|
66
|
-
timeout: (originalError?: Error) => DataError;
|
|
67
|
-
unauthorized: (message?: string) => DataError;
|
|
68
|
-
forbidden: (message?: string) => DataError;
|
|
69
|
-
badRequest: (message: string, code?: string) => DataError;
|
|
70
|
-
notFound: (message?: string) => DataError;
|
|
71
|
-
graphql: (message: string, code?: string) => DataError;
|
|
72
|
-
uniqueViolation: (message: string, fieldName?: string, constraint?: string) => DataError;
|
|
73
|
-
foreignKeyViolation: (message: string, fieldName?: string, constraint?: string) => DataError;
|
|
74
|
-
notNullViolation: (message: string, fieldName?: string, constraint?: string) => DataError;
|
|
75
|
-
unknown: (originalError: Error) => DataError;
|
|
76
|
-
};
|
|
11
|
+
import { ConstructiveError } from '@constructive-io/errors';
|
|
12
|
+
export { ConstructiveError, classify, format, isPublicCode, parse, type ErrorClass, type ErrorContext, type ParsedError, toError, } from '@constructive-io/errors';
|
|
13
|
+
/** Shape of a single GraphQL error entry in a response `errors[]` array. */
|
|
77
14
|
export interface GraphQLError {
|
|
78
15
|
message: string;
|
|
79
16
|
extensions?: {
|
|
80
17
|
code?: string;
|
|
18
|
+
class?: string;
|
|
81
19
|
} & Record<string, unknown>;
|
|
82
20
|
locations?: Array<{
|
|
83
21
|
line: number;
|
|
@@ -86,10 +24,28 @@ export interface GraphQLError {
|
|
|
86
24
|
path?: Array<string | number>;
|
|
87
25
|
}
|
|
88
26
|
/**
|
|
89
|
-
* Parse any error
|
|
27
|
+
* Parse any error (pg error, GraphQL error/response, `ConstructiveError`, plain
|
|
28
|
+
* `Error`, or string) into a throwable {@link ConstructiveError}.
|
|
90
29
|
*/
|
|
91
|
-
export declare function parseGraphQLError(error: unknown):
|
|
30
|
+
export declare function parseGraphQLError(error: unknown, locale?: string): ConstructiveError;
|
|
31
|
+
/** True when the value is a {@link ConstructiveError}. */
|
|
32
|
+
export declare function isConstructiveError(error: unknown): error is ConstructiveError;
|
|
33
|
+
/** Whether an error is safe to retry (network/timeout/rate-limit). */
|
|
34
|
+
export declare function isRetryable(error: unknown): boolean;
|
|
92
35
|
/**
|
|
93
|
-
*
|
|
36
|
+
* Factory for the transport/HTTP-level errors the client synthesizes itself
|
|
37
|
+
* (before any server/DB response is parsed). All produce a
|
|
38
|
+
* {@link ConstructiveError} from the canonical registry.
|
|
94
39
|
*/
|
|
95
|
-
export declare
|
|
40
|
+
export declare const createError: {
|
|
41
|
+
network: (originalError?: Error) => ConstructiveError;
|
|
42
|
+
timeout: () => ConstructiveError;
|
|
43
|
+
unauthorized: (message?: string) => ConstructiveError;
|
|
44
|
+
forbidden: (message?: string) => ConstructiveError;
|
|
45
|
+
notFound: (message?: string) => ConstructiveError;
|
|
46
|
+
badRequest: (message: string) => ConstructiveError;
|
|
47
|
+
graphql: (error: unknown) => ConstructiveError;
|
|
48
|
+
unknown: (originalError: Error) => ConstructiveError;
|
|
49
|
+
};
|
|
50
|
+
/** Whether an error is safe to surface to end users (public class). */
|
|
51
|
+
export declare function isPublicError(error: unknown): boolean;
|
package/client/error.js
CHANGED
|
@@ -1,277 +1,67 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Error handling for GraphQL operations
|
|
4
|
-
*
|
|
3
|
+
* Error handling for GraphQL operations.
|
|
4
|
+
*
|
|
5
|
+
* This is a thin client-side adapter over the canonical `@constructive-io/errors`
|
|
6
|
+
* package. The package owns the single source of truth for error codes, their
|
|
7
|
+
* public/internal class, message copy (i18n), and cross-source parsing (pg
|
|
8
|
+
* `DatabaseError`, GraphQL `extensions`, structured `DETAIL`, message tokens,
|
|
9
|
+
* and native SQLSTATE). There is no separate client-side error catalog or
|
|
10
|
+
* message string-matching anymore.
|
|
5
11
|
*/
|
|
6
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.createError = exports.
|
|
13
|
+
exports.createError = exports.toError = exports.parse = exports.isPublicCode = exports.format = exports.classify = exports.ConstructiveError = void 0;
|
|
8
14
|
exports.parseGraphQLError = parseGraphQLError;
|
|
9
|
-
exports.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
INVALID_MUTATION_DATA: 'INVALID_MUTATION_DATA',
|
|
21
|
-
// Query errors
|
|
22
|
-
QUERY_GENERATION_FAILED: 'QUERY_GENERATION_FAILED',
|
|
23
|
-
QUERY_EXECUTION_FAILED: 'QUERY_EXECUTION_FAILED',
|
|
24
|
-
// Permission errors
|
|
25
|
-
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
26
|
-
FORBIDDEN: 'FORBIDDEN',
|
|
27
|
-
// Schema errors
|
|
28
|
-
TABLE_NOT_FOUND: 'TABLE_NOT_FOUND',
|
|
29
|
-
// Request errors
|
|
30
|
-
BAD_REQUEST: 'BAD_REQUEST',
|
|
31
|
-
NOT_FOUND: 'NOT_FOUND',
|
|
32
|
-
// GraphQL-specific errors
|
|
33
|
-
GRAPHQL_ERROR: 'GRAPHQL_ERROR',
|
|
34
|
-
// PostgreSQL constraint errors (surfaced via PostGraphile)
|
|
35
|
-
UNIQUE_VIOLATION: 'UNIQUE_VIOLATION',
|
|
36
|
-
FOREIGN_KEY_VIOLATION: 'FOREIGN_KEY_VIOLATION',
|
|
37
|
-
NOT_NULL_VIOLATION: 'NOT_NULL_VIOLATION',
|
|
38
|
-
CHECK_VIOLATION: 'CHECK_VIOLATION',
|
|
39
|
-
EXCLUSION_VIOLATION: 'EXCLUSION_VIOLATION',
|
|
40
|
-
// Generic errors
|
|
41
|
-
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
42
|
-
};
|
|
15
|
+
exports.isConstructiveError = isConstructiveError;
|
|
16
|
+
exports.isRetryable = isRetryable;
|
|
17
|
+
exports.isPublicError = isPublicError;
|
|
18
|
+
const errors_1 = require("@constructive-io/errors");
|
|
19
|
+
var errors_2 = require("@constructive-io/errors");
|
|
20
|
+
Object.defineProperty(exports, "ConstructiveError", { enumerable: true, get: function () { return errors_2.ConstructiveError; } });
|
|
21
|
+
Object.defineProperty(exports, "classify", { enumerable: true, get: function () { return errors_2.classify; } });
|
|
22
|
+
Object.defineProperty(exports, "format", { enumerable: true, get: function () { return errors_2.format; } });
|
|
23
|
+
Object.defineProperty(exports, "isPublicCode", { enumerable: true, get: function () { return errors_2.isPublicCode; } });
|
|
24
|
+
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return errors_2.parse; } });
|
|
25
|
+
Object.defineProperty(exports, "toError", { enumerable: true, get: function () { return errors_2.toError; } });
|
|
43
26
|
/**
|
|
44
|
-
*
|
|
27
|
+
* Parse any error (pg error, GraphQL error/response, `ConstructiveError`, plain
|
|
28
|
+
* `Error`, or string) into a throwable {@link ConstructiveError}.
|
|
45
29
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
code;
|
|
49
|
-
originalError;
|
|
50
|
-
context;
|
|
51
|
-
tableName;
|
|
52
|
-
fieldName;
|
|
53
|
-
constraint;
|
|
54
|
-
constructor(type, message, options = {}) {
|
|
55
|
-
super(message);
|
|
56
|
-
this.name = 'DataError';
|
|
57
|
-
this.type = type;
|
|
58
|
-
this.code = options.code;
|
|
59
|
-
this.originalError = options.originalError;
|
|
60
|
-
this.context = options.context;
|
|
61
|
-
this.tableName = options.tableName;
|
|
62
|
-
this.fieldName = options.fieldName;
|
|
63
|
-
this.constraint = options.constraint;
|
|
64
|
-
if (Error.captureStackTrace) {
|
|
65
|
-
Error.captureStackTrace(this, DataError);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
getUserMessage() {
|
|
69
|
-
switch (this.type) {
|
|
70
|
-
case exports.DataErrorType.NETWORK_ERROR:
|
|
71
|
-
return 'Network error. Please check your connection and try again.';
|
|
72
|
-
case exports.DataErrorType.TIMEOUT_ERROR:
|
|
73
|
-
return 'Request timed out. Please try again.';
|
|
74
|
-
case exports.DataErrorType.UNAUTHORIZED:
|
|
75
|
-
return 'You are not authorized. Please log in and try again.';
|
|
76
|
-
case exports.DataErrorType.FORBIDDEN:
|
|
77
|
-
return 'You do not have permission to access this resource.';
|
|
78
|
-
case exports.DataErrorType.VALIDATION_FAILED:
|
|
79
|
-
return 'Validation failed. Please check your input and try again.';
|
|
80
|
-
case exports.DataErrorType.REQUIRED_FIELD_MISSING:
|
|
81
|
-
return this.fieldName
|
|
82
|
-
? `The field "${this.fieldName}" is required.`
|
|
83
|
-
: 'A required field is missing.';
|
|
84
|
-
case exports.DataErrorType.UNIQUE_VIOLATION:
|
|
85
|
-
return this.fieldName
|
|
86
|
-
? `A record with this ${this.fieldName} already exists.`
|
|
87
|
-
: 'A record with this value already exists.';
|
|
88
|
-
case exports.DataErrorType.FOREIGN_KEY_VIOLATION:
|
|
89
|
-
return 'This record references a record that does not exist.';
|
|
90
|
-
case exports.DataErrorType.NOT_NULL_VIOLATION:
|
|
91
|
-
return this.fieldName
|
|
92
|
-
? `The field "${this.fieldName}" cannot be empty.`
|
|
93
|
-
: 'A required field cannot be empty.';
|
|
94
|
-
case exports.DataErrorType.CHECK_VIOLATION:
|
|
95
|
-
return this.fieldName
|
|
96
|
-
? `The value for "${this.fieldName}" is not valid.`
|
|
97
|
-
: 'The value does not meet the required constraints.';
|
|
98
|
-
default:
|
|
99
|
-
return this.message || 'An unexpected error occurred.';
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
isRetryable() {
|
|
103
|
-
return (this.type === exports.DataErrorType.NETWORK_ERROR ||
|
|
104
|
-
this.type === exports.DataErrorType.TIMEOUT_ERROR);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
exports.DataError = DataError;
|
|
108
|
-
// ============================================================================
|
|
109
|
-
// PostgreSQL Error Codes
|
|
110
|
-
// ============================================================================
|
|
111
|
-
exports.PG_ERROR_CODES = {
|
|
112
|
-
UNIQUE_VIOLATION: '23505',
|
|
113
|
-
FOREIGN_KEY_VIOLATION: '23503',
|
|
114
|
-
NOT_NULL_VIOLATION: '23502',
|
|
115
|
-
CHECK_VIOLATION: '23514',
|
|
116
|
-
EXCLUSION_VIOLATION: '23P01',
|
|
117
|
-
NUMERIC_VALUE_OUT_OF_RANGE: '22003',
|
|
118
|
-
STRING_DATA_RIGHT_TRUNCATION: '22001',
|
|
119
|
-
INVALID_TEXT_REPRESENTATION: '22P02',
|
|
120
|
-
DATETIME_FIELD_OVERFLOW: '22008',
|
|
121
|
-
UNDEFINED_TABLE: '42P01',
|
|
122
|
-
UNDEFINED_COLUMN: '42703',
|
|
123
|
-
INSUFFICIENT_PRIVILEGE: '42501',
|
|
124
|
-
};
|
|
125
|
-
// ============================================================================
|
|
126
|
-
// Error Factory
|
|
127
|
-
// ============================================================================
|
|
128
|
-
exports.createError = {
|
|
129
|
-
network: (originalError) => new DataError(exports.DataErrorType.NETWORK_ERROR, 'Network error occurred', {
|
|
130
|
-
originalError,
|
|
131
|
-
}),
|
|
132
|
-
timeout: (originalError) => new DataError(exports.DataErrorType.TIMEOUT_ERROR, 'Request timed out', {
|
|
133
|
-
originalError,
|
|
134
|
-
}),
|
|
135
|
-
unauthorized: (message = 'Authentication required') => new DataError(exports.DataErrorType.UNAUTHORIZED, message),
|
|
136
|
-
forbidden: (message = 'Access forbidden') => new DataError(exports.DataErrorType.FORBIDDEN, message),
|
|
137
|
-
badRequest: (message, code) => new DataError(exports.DataErrorType.BAD_REQUEST, message, { code }),
|
|
138
|
-
notFound: (message = 'Resource not found') => new DataError(exports.DataErrorType.NOT_FOUND, message),
|
|
139
|
-
graphql: (message, code) => new DataError(exports.DataErrorType.GRAPHQL_ERROR, message, { code }),
|
|
140
|
-
uniqueViolation: (message, fieldName, constraint) => new DataError(exports.DataErrorType.UNIQUE_VIOLATION, message, {
|
|
141
|
-
fieldName,
|
|
142
|
-
constraint,
|
|
143
|
-
code: '23505',
|
|
144
|
-
}),
|
|
145
|
-
foreignKeyViolation: (message, fieldName, constraint) => new DataError(exports.DataErrorType.FOREIGN_KEY_VIOLATION, message, {
|
|
146
|
-
fieldName,
|
|
147
|
-
constraint,
|
|
148
|
-
code: '23503',
|
|
149
|
-
}),
|
|
150
|
-
notNullViolation: (message, fieldName, constraint) => new DataError(exports.DataErrorType.NOT_NULL_VIOLATION, message, {
|
|
151
|
-
fieldName,
|
|
152
|
-
constraint,
|
|
153
|
-
code: '23502',
|
|
154
|
-
}),
|
|
155
|
-
unknown: (originalError) => new DataError(exports.DataErrorType.UNKNOWN_ERROR, originalError.message, {
|
|
156
|
-
originalError,
|
|
157
|
-
}),
|
|
158
|
-
};
|
|
159
|
-
function parseGraphQLErrorCode(code) {
|
|
160
|
-
if (!code)
|
|
161
|
-
return exports.DataErrorType.UNKNOWN_ERROR;
|
|
162
|
-
const normalized = code.toUpperCase();
|
|
163
|
-
// GraphQL standard codes
|
|
164
|
-
if (normalized === 'UNAUTHENTICATED')
|
|
165
|
-
return exports.DataErrorType.UNAUTHORIZED;
|
|
166
|
-
if (normalized === 'FORBIDDEN')
|
|
167
|
-
return exports.DataErrorType.FORBIDDEN;
|
|
168
|
-
if (normalized === 'GRAPHQL_VALIDATION_FAILED')
|
|
169
|
-
return exports.DataErrorType.QUERY_GENERATION_FAILED;
|
|
170
|
-
// PostgreSQL SQLSTATE codes
|
|
171
|
-
if (code === exports.PG_ERROR_CODES.UNIQUE_VIOLATION)
|
|
172
|
-
return exports.DataErrorType.UNIQUE_VIOLATION;
|
|
173
|
-
if (code === exports.PG_ERROR_CODES.FOREIGN_KEY_VIOLATION)
|
|
174
|
-
return exports.DataErrorType.FOREIGN_KEY_VIOLATION;
|
|
175
|
-
if (code === exports.PG_ERROR_CODES.NOT_NULL_VIOLATION)
|
|
176
|
-
return exports.DataErrorType.NOT_NULL_VIOLATION;
|
|
177
|
-
if (code === exports.PG_ERROR_CODES.CHECK_VIOLATION)
|
|
178
|
-
return exports.DataErrorType.CHECK_VIOLATION;
|
|
179
|
-
if (code === exports.PG_ERROR_CODES.EXCLUSION_VIOLATION)
|
|
180
|
-
return exports.DataErrorType.EXCLUSION_VIOLATION;
|
|
181
|
-
return exports.DataErrorType.UNKNOWN_ERROR;
|
|
30
|
+
function parseGraphQLError(error, locale) {
|
|
31
|
+
return (0, errors_1.toError)(error, locale);
|
|
182
32
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return exports.DataErrorType.TIMEOUT_ERROR;
|
|
187
|
-
}
|
|
188
|
-
if (lower.includes('network') ||
|
|
189
|
-
lower.includes('fetch') ||
|
|
190
|
-
lower.includes('failed to fetch')) {
|
|
191
|
-
return exports.DataErrorType.NETWORK_ERROR;
|
|
192
|
-
}
|
|
193
|
-
if (lower.includes('unauthorized') ||
|
|
194
|
-
lower.includes('authentication required')) {
|
|
195
|
-
return exports.DataErrorType.UNAUTHORIZED;
|
|
196
|
-
}
|
|
197
|
-
if (lower.includes('forbidden') || lower.includes('permission')) {
|
|
198
|
-
return exports.DataErrorType.FORBIDDEN;
|
|
199
|
-
}
|
|
200
|
-
if (lower.includes('duplicate key') || lower.includes('already exists')) {
|
|
201
|
-
return exports.DataErrorType.UNIQUE_VIOLATION;
|
|
202
|
-
}
|
|
203
|
-
if (lower.includes('foreign key constraint')) {
|
|
204
|
-
return exports.DataErrorType.FOREIGN_KEY_VIOLATION;
|
|
205
|
-
}
|
|
206
|
-
if (lower.includes('not-null constraint') ||
|
|
207
|
-
lower.includes('null value in column')) {
|
|
208
|
-
return exports.DataErrorType.NOT_NULL_VIOLATION;
|
|
209
|
-
}
|
|
210
|
-
return exports.DataErrorType.UNKNOWN_ERROR;
|
|
33
|
+
/** True when the value is a {@link ConstructiveError}. */
|
|
34
|
+
function isConstructiveError(error) {
|
|
35
|
+
return error instanceof errors_1.ConstructiveError;
|
|
211
36
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const keyMatch = message.match(/Key\s+\(([a-z_][a-z0-9_]*)\)/i);
|
|
224
|
-
if (keyMatch)
|
|
225
|
-
return keyMatch[1];
|
|
226
|
-
return undefined;
|
|
37
|
+
/** Codes worth automatically retrying (transient transport/rate failures). */
|
|
38
|
+
const RETRYABLE_CODES = new Set([
|
|
39
|
+
'NETWORK_ERROR',
|
|
40
|
+
'TIMEOUT_ERROR',
|
|
41
|
+
'RATE_LIMITED',
|
|
42
|
+
'TOO_MANY_REQUESTS',
|
|
43
|
+
]);
|
|
44
|
+
/** Whether an error is safe to retry (network/timeout/rate-limit). */
|
|
45
|
+
function isRetryable(error) {
|
|
46
|
+
const { code } = (0, errors_1.parse)(error);
|
|
47
|
+
return Boolean(code && RETRYABLE_CODES.has(code));
|
|
227
48
|
}
|
|
228
49
|
/**
|
|
229
|
-
*
|
|
50
|
+
* Factory for the transport/HTTP-level errors the client synthesizes itself
|
|
51
|
+
* (before any server/DB response is parsed). All produce a
|
|
52
|
+
* {@link ConstructiveError} from the canonical registry.
|
|
230
53
|
*/
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const constraint = gqlError.extensions?.constraint;
|
|
245
|
-
const fieldName = extractFieldFromError(gqlError.message, constraint, column);
|
|
246
|
-
if (mappedType !== exports.DataErrorType.UNKNOWN_ERROR) {
|
|
247
|
-
return new DataError(mappedType, gqlError.message, {
|
|
248
|
-
code: extCode,
|
|
249
|
-
fieldName,
|
|
250
|
-
constraint,
|
|
251
|
-
context: gqlError.extensions,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
// Fallback: classify by message
|
|
255
|
-
const fallbackType = classifyByMessage(gqlError.message);
|
|
256
|
-
return new DataError(fallbackType, gqlError.message, {
|
|
257
|
-
code: extCode,
|
|
258
|
-
fieldName,
|
|
259
|
-
constraint,
|
|
260
|
-
context: gqlError.extensions,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
// Standard Error
|
|
264
|
-
if (error instanceof Error) {
|
|
265
|
-
const type = classifyByMessage(error.message);
|
|
266
|
-
return new DataError(type, error.message, { originalError: error });
|
|
267
|
-
}
|
|
268
|
-
// Unknown
|
|
269
|
-
const message = typeof error === 'string' ? error : 'Unknown error occurred';
|
|
270
|
-
return new DataError(exports.DataErrorType.UNKNOWN_ERROR, message);
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Check if value is a DataError
|
|
274
|
-
*/
|
|
275
|
-
function isDataError(error) {
|
|
276
|
-
return error instanceof DataError;
|
|
54
|
+
exports.createError = {
|
|
55
|
+
network: (originalError) => errors_1.errors.NETWORK_ERROR({}, originalError?.message),
|
|
56
|
+
timeout: () => errors_1.errors.TIMEOUT_ERROR(),
|
|
57
|
+
unauthorized: (message = 'Authentication required') => errors_1.errors.UNAUTHENTICATED({}, message),
|
|
58
|
+
forbidden: (message = 'Access forbidden') => errors_1.errors.FORBIDDEN({}, message),
|
|
59
|
+
notFound: (message = 'Resource not found') => errors_1.errors.NOT_FOUND({}, message),
|
|
60
|
+
badRequest: (message) => errors_1.errors.BAD_USER_INPUT({}, message),
|
|
61
|
+
graphql: (error) => (0, errors_1.toError)(error),
|
|
62
|
+
unknown: (originalError) => (0, errors_1.toError)(originalError),
|
|
63
|
+
};
|
|
64
|
+
/** Whether an error is safe to surface to end users (public class). */
|
|
65
|
+
function isPublicError(error) {
|
|
66
|
+
return (0, errors_1.isPublicCode)((0, errors_1.parse)(error).code);
|
|
277
67
|
}
|
package/client/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Re-exports client utilities for GraphQL execution and error handling.
|
|
5
5
|
*/
|
|
6
6
|
export { TypedDocumentString, type DocumentTypeDecoration, } from './typed-document';
|
|
7
|
-
export {
|
|
7
|
+
export { ConstructiveError, classify, createError, format, isConstructiveError, isPublicCode, isPublicError, isRetryable, parse, parseGraphQLError, toError, type ErrorClass, type ErrorContext, type GraphQLError, type ParsedError, } from './error';
|
|
8
8
|
export { execute, createGraphQLClient, type ExecuteOptions, type GraphQLResponse, type GraphQLClientOptions, type GraphQLClient, } from './execute';
|
package/client/index.js
CHANGED
|
@@ -5,16 +5,21 @@
|
|
|
5
5
|
* Re-exports client utilities for GraphQL execution and error handling.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.createGraphQLClient = exports.execute = exports.
|
|
8
|
+
exports.createGraphQLClient = exports.execute = exports.toError = exports.parseGraphQLError = exports.parse = exports.isRetryable = exports.isPublicError = exports.isPublicCode = exports.isConstructiveError = exports.format = exports.createError = exports.classify = exports.ConstructiveError = exports.TypedDocumentString = void 0;
|
|
9
9
|
var typed_document_1 = require("./typed-document");
|
|
10
10
|
Object.defineProperty(exports, "TypedDocumentString", { enumerable: true, get: function () { return typed_document_1.TypedDocumentString; } });
|
|
11
11
|
var error_1 = require("./error");
|
|
12
|
-
Object.defineProperty(exports, "
|
|
13
|
-
Object.defineProperty(exports, "
|
|
14
|
-
Object.defineProperty(exports, "PG_ERROR_CODES", { enumerable: true, get: function () { return error_1.PG_ERROR_CODES; } });
|
|
12
|
+
Object.defineProperty(exports, "ConstructiveError", { enumerable: true, get: function () { return error_1.ConstructiveError; } });
|
|
13
|
+
Object.defineProperty(exports, "classify", { enumerable: true, get: function () { return error_1.classify; } });
|
|
15
14
|
Object.defineProperty(exports, "createError", { enumerable: true, get: function () { return error_1.createError; } });
|
|
15
|
+
Object.defineProperty(exports, "format", { enumerable: true, get: function () { return error_1.format; } });
|
|
16
|
+
Object.defineProperty(exports, "isConstructiveError", { enumerable: true, get: function () { return error_1.isConstructiveError; } });
|
|
17
|
+
Object.defineProperty(exports, "isPublicCode", { enumerable: true, get: function () { return error_1.isPublicCode; } });
|
|
18
|
+
Object.defineProperty(exports, "isPublicError", { enumerable: true, get: function () { return error_1.isPublicError; } });
|
|
19
|
+
Object.defineProperty(exports, "isRetryable", { enumerable: true, get: function () { return error_1.isRetryable; } });
|
|
20
|
+
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return error_1.parse; } });
|
|
16
21
|
Object.defineProperty(exports, "parseGraphQLError", { enumerable: true, get: function () { return error_1.parseGraphQLError; } });
|
|
17
|
-
Object.defineProperty(exports, "
|
|
22
|
+
Object.defineProperty(exports, "toError", { enumerable: true, get: function () { return error_1.toError; } });
|
|
18
23
|
var execute_1 = require("./execute");
|
|
19
24
|
Object.defineProperty(exports, "execute", { enumerable: true, get: function () { return execute_1.execute; } });
|
|
20
25
|
Object.defineProperty(exports, "createGraphQLClient", { enumerable: true, get: function () { return execute_1.createGraphQLClient; } });
|
package/esm/client/error.js
CHANGED
|
@@ -1,271 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Error handling for GraphQL operations
|
|
3
|
-
*
|
|
2
|
+
* Error handling for GraphQL operations.
|
|
3
|
+
*
|
|
4
|
+
* This is a thin client-side adapter over the canonical `@constructive-io/errors`
|
|
5
|
+
* package. The package owns the single source of truth for error codes, their
|
|
6
|
+
* public/internal class, message copy (i18n), and cross-source parsing (pg
|
|
7
|
+
* `DatabaseError`, GraphQL `extensions`, structured `DETAIL`, message tokens,
|
|
8
|
+
* and native SQLSTATE). There is no separate client-side error catalog or
|
|
9
|
+
* message string-matching anymore.
|
|
4
10
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
};
|
|
11
|
+
import { ConstructiveError, errors, isPublicCode, parse, toError, } from '@constructive-io/errors';
|
|
12
|
+
export { ConstructiveError, classify, format, isPublicCode, parse, toError, } from '@constructive-io/errors';
|
|
38
13
|
/**
|
|
39
|
-
*
|
|
14
|
+
* Parse any error (pg error, GraphQL error/response, `ConstructiveError`, plain
|
|
15
|
+
* `Error`, or string) into a throwable {@link ConstructiveError}.
|
|
40
16
|
*/
|
|
41
|
-
export
|
|
42
|
-
|
|
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;
|
|
17
|
+
export function parseGraphQLError(error, locale) {
|
|
18
|
+
return toError(error, locale);
|
|
176
19
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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;
|
|
20
|
+
/** True when the value is a {@link ConstructiveError}. */
|
|
21
|
+
export function isConstructiveError(error) {
|
|
22
|
+
return error instanceof ConstructiveError;
|
|
205
23
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const keyMatch = message.match(/Key\s+\(([a-z_][a-z0-9_]*)\)/i);
|
|
218
|
-
if (keyMatch)
|
|
219
|
-
return keyMatch[1];
|
|
220
|
-
return undefined;
|
|
24
|
+
/** Codes worth automatically retrying (transient transport/rate failures). */
|
|
25
|
+
const RETRYABLE_CODES = new Set([
|
|
26
|
+
'NETWORK_ERROR',
|
|
27
|
+
'TIMEOUT_ERROR',
|
|
28
|
+
'RATE_LIMITED',
|
|
29
|
+
'TOO_MANY_REQUESTS',
|
|
30
|
+
]);
|
|
31
|
+
/** Whether an error is safe to retry (network/timeout/rate-limit). */
|
|
32
|
+
export function isRetryable(error) {
|
|
33
|
+
const { code } = parse(error);
|
|
34
|
+
return Boolean(code && RETRYABLE_CODES.has(code));
|
|
221
35
|
}
|
|
222
36
|
/**
|
|
223
|
-
*
|
|
37
|
+
* Factory for the transport/HTTP-level errors the client synthesizes itself
|
|
38
|
+
* (before any server/DB response is parsed). All produce a
|
|
39
|
+
* {@link ConstructiveError} from the canonical registry.
|
|
224
40
|
*/
|
|
225
|
-
export
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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;
|
|
41
|
+
export const createError = {
|
|
42
|
+
network: (originalError) => errors.NETWORK_ERROR({}, originalError?.message),
|
|
43
|
+
timeout: () => errors.TIMEOUT_ERROR(),
|
|
44
|
+
unauthorized: (message = 'Authentication required') => errors.UNAUTHENTICATED({}, message),
|
|
45
|
+
forbidden: (message = 'Access forbidden') => errors.FORBIDDEN({}, message),
|
|
46
|
+
notFound: (message = 'Resource not found') => errors.NOT_FOUND({}, message),
|
|
47
|
+
badRequest: (message) => errors.BAD_USER_INPUT({}, message),
|
|
48
|
+
graphql: (error) => toError(error),
|
|
49
|
+
unknown: (originalError) => toError(originalError),
|
|
50
|
+
};
|
|
51
|
+
/** Whether an error is safe to surface to end users (public class). */
|
|
52
|
+
export function isPublicError(error) {
|
|
53
|
+
return isPublicCode(parse(error).code);
|
|
271
54
|
}
|
package/esm/client/index.js
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Re-exports client utilities for GraphQL execution and error handling.
|
|
5
5
|
*/
|
|
6
6
|
export { TypedDocumentString, } from './typed-document';
|
|
7
|
-
export {
|
|
7
|
+
export { ConstructiveError, classify, createError, format, isConstructiveError, isPublicCode, isPublicError, isRetryable, parse, parseGraphQLError, toError, } from './error';
|
|
8
8
|
export { execute, createGraphQLClient, } from './execute';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-query",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "Constructive GraphQL Query",
|
|
5
5
|
"author": "Constructive <developers@constructive.io>",
|
|
6
6
|
"main": "index.js",
|
|
@@ -30,14 +30,15 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@0no-co/graphql.web": "^1.1.2",
|
|
33
|
+
"@constructive-io/errors": "^0.3.0",
|
|
33
34
|
"@constructive-io/fetch": "^1.1.1",
|
|
34
|
-
"@constructive-io/graphql-types": "^3.
|
|
35
|
+
"@constructive-io/graphql-types": "^3.23.0",
|
|
35
36
|
"ajv": "^8.20.0",
|
|
36
|
-
"gql-ast": "^3.
|
|
37
|
+
"gql-ast": "^3.18.0",
|
|
37
38
|
"grafast": "1.0.2",
|
|
38
39
|
"graphile-build-pg": "5.0.2",
|
|
39
40
|
"graphile-config": "1.0.1",
|
|
40
|
-
"graphile-settings": "^6.
|
|
41
|
+
"graphile-settings": "^6.6.0",
|
|
41
42
|
"graphql": "16.13.0",
|
|
42
43
|
"inflection": "^3.0.0",
|
|
43
44
|
"inflekt": "^0.7.1",
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"makage": "^0.3.0"
|
|
56
57
|
},
|
|
57
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "bb3269d48cdf2bc8f361bdd79b4833c921c13ec3"
|
|
58
59
|
}
|