@constructive-io/graphql-server 3.1.1 → 4.0.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.
Files changed (77) hide show
  1. package/errors/404-message.js +1 -1
  2. package/errors/api-errors.d.ts +200 -0
  3. package/errors/api-errors.js +276 -0
  4. package/esm/errors/404-message.js +1 -1
  5. package/esm/errors/api-errors.js +261 -0
  6. package/esm/index.js +2 -0
  7. package/esm/middleware/api.js +355 -277
  8. package/esm/middleware/auth.js +25 -7
  9. package/esm/middleware/error-handler.js +86 -0
  10. package/esm/middleware/favicon.js +12 -0
  11. package/esm/middleware/graphile.js +149 -64
  12. package/esm/options.js +232 -0
  13. package/esm/schema.js +24 -11
  14. package/esm/server.js +41 -5
  15. package/index.d.ts +1 -0
  16. package/index.js +2 -0
  17. package/middleware/api.d.ts +3 -15
  18. package/middleware/api.js +359 -283
  19. package/middleware/auth.js +25 -7
  20. package/middleware/error-handler.d.ts +4 -0
  21. package/middleware/error-handler.js +94 -0
  22. package/middleware/favicon.d.ts +2 -0
  23. package/middleware/favicon.js +16 -0
  24. package/middleware/graphile.d.ts +14 -0
  25. package/middleware/graphile.js +149 -64
  26. package/options.d.ts +131 -0
  27. package/options.js +244 -0
  28. package/package.json +23 -24
  29. package/schema.d.ts +2 -2
  30. package/schema.js +23 -10
  31. package/server.d.ts +24 -2
  32. package/server.js +39 -3
  33. package/codegen/orm/client.d.ts +0 -55
  34. package/codegen/orm/client.js +0 -75
  35. package/codegen/orm/index.d.ts +0 -36
  36. package/codegen/orm/index.js +0 -59
  37. package/codegen/orm/input-types.d.ts +0 -20140
  38. package/codegen/orm/input-types.js +0 -2
  39. package/codegen/orm/models/api.d.ts +0 -42
  40. package/codegen/orm/models/api.js +0 -76
  41. package/codegen/orm/models/domain.d.ts +0 -42
  42. package/codegen/orm/models/domain.js +0 -76
  43. package/codegen/orm/models/index.d.ts +0 -7
  44. package/codegen/orm/models/index.js +0 -12
  45. package/codegen/orm/mutation/index.d.ts +0 -7
  46. package/codegen/orm/mutation/index.js +0 -7
  47. package/codegen/orm/query/index.d.ts +0 -20
  48. package/codegen/orm/query/index.js +0 -24
  49. package/codegen/orm/query-builder.d.ts +0 -81
  50. package/codegen/orm/query-builder.js +0 -496
  51. package/codegen/orm/select-types.d.ts +0 -83
  52. package/codegen/orm/select-types.js +0 -7
  53. package/codegen/orm/types.d.ts +0 -6
  54. package/codegen/orm/types.js +0 -23
  55. package/esm/codegen/orm/client.js +0 -70
  56. package/esm/codegen/orm/index.js +0 -39
  57. package/esm/codegen/orm/input-types.js +0 -1
  58. package/esm/codegen/orm/models/api.js +0 -72
  59. package/esm/codegen/orm/models/domain.js +0 -72
  60. package/esm/codegen/orm/models/index.js +0 -7
  61. package/esm/codegen/orm/mutation/index.js +0 -4
  62. package/esm/codegen/orm/query/index.js +0 -21
  63. package/esm/codegen/orm/query-builder.js +0 -452
  64. package/esm/codegen/orm/select-types.js +0 -6
  65. package/esm/codegen/orm/types.js +0 -7
  66. package/esm/middleware/gql.js +0 -116
  67. package/esm/plugins/PublicKeySignature.js +0 -114
  68. package/esm/scripts/codegen-schema.js +0 -71
  69. package/esm/scripts/create-bucket.js +0 -40
  70. package/middleware/gql.d.ts +0 -164
  71. package/middleware/gql.js +0 -121
  72. package/plugins/PublicKeySignature.d.ts +0 -11
  73. package/plugins/PublicKeySignature.js +0 -121
  74. package/scripts/codegen-schema.d.ts +0 -1
  75. package/scripts/codegen-schema.js +0 -76
  76. package/scripts/create-bucket.d.ts +0 -1
  77. package/scripts/create-bucket.js +0 -42
@@ -141,7 +141,7 @@ exports.default = (message = "We’re really sorry about that. The service you w
141
141
  }
142
142
 
143
143
  body {
144
- background-color: #dde7e9;
144
+ background-color: #F3F6FA;
145
145
  color: #01A1FF;
146
146
  font-family: 'Fjalla One', sans-serif;
147
147
  position: relative;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Typed API Error System
3
+ *
4
+ * Provides strongly-typed error hierarchy for the GraphQL server with:
5
+ * - Consistent error classification
6
+ * - HTTP status mapping
7
+ * - Serialization for API responses and logging
8
+ *
9
+ * @module errors/api-errors
10
+ */
11
+ /**
12
+ * Centralized error code definitions for external use and type safety.
13
+ * Use these constants instead of string literals for error code comparisons.
14
+ */
15
+ export declare const ErrorCodes: {
16
+ readonly DOMAIN_NOT_FOUND: "DOMAIN_NOT_FOUND";
17
+ readonly API_NOT_FOUND: "API_NOT_FOUND";
18
+ readonly NO_VALID_SCHEMAS: "NO_VALID_SCHEMAS";
19
+ readonly SCHEMA_INVALID: "SCHEMA_INVALID";
20
+ readonly SCHEMA_ACCESS_DENIED: "SCHEMA_ACCESS_DENIED";
21
+ readonly HANDLER_ERROR: "HANDLER_ERROR";
22
+ readonly DATABASE_CONNECTION_ERROR: "DATABASE_CONNECTION_ERROR";
23
+ readonly AMBIGUOUS_TENANT: "AMBIGUOUS_TENANT";
24
+ readonly ADMIN_AUTH_REQUIRED: "ADMIN_AUTH_REQUIRED";
25
+ };
26
+ /**
27
+ * Type alias for valid error codes.
28
+ * Derived from the ErrorCodes constant for type safety.
29
+ */
30
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
31
+ /**
32
+ * Base class for all API errors.
33
+ *
34
+ * Provides consistent structure for error handling including:
35
+ * - Machine-readable error codes
36
+ * - HTTP status code mapping
37
+ * - Optional debugging context
38
+ * - JSON serialization for API responses
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * throw new ApiError('CUSTOM_ERROR', 400, 'Something went wrong', { detail: 'info' });
43
+ * ```
44
+ */
45
+ export declare class ApiError extends Error {
46
+ readonly code: string;
47
+ readonly statusCode: number;
48
+ readonly context?: Record<string, unknown>;
49
+ constructor(code: string, statusCode: number, message: string, context?: Record<string, unknown>);
50
+ /**
51
+ * Serializes the error for API responses and structured logging.
52
+ *
53
+ * @returns Object with error details suitable for JSON serialization
54
+ */
55
+ toJSON(): Record<string, unknown>;
56
+ }
57
+ /**
58
+ * Thrown when a domain lookup fails to find a matching API.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * throw new DomainNotFoundError('example.com', 'api');
63
+ * // Results in: "No API configured for domain: api.example.com"
64
+ * ```
65
+ */
66
+ export declare class DomainNotFoundError extends ApiError {
67
+ constructor(domain: string, subdomain: string | null);
68
+ }
69
+ /**
70
+ * Thrown when a specific API cannot be found by its ID.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * throw new ApiNotFoundError('api-123');
75
+ * ```
76
+ */
77
+ export declare class ApiNotFoundError extends ApiError {
78
+ constructor(apiId: string);
79
+ }
80
+ /**
81
+ * Thrown when no valid schemas are found for an API.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * throw new NoValidSchemasError('api-123');
86
+ * ```
87
+ */
88
+ export declare class NoValidSchemasError extends ApiError {
89
+ constructor(apiId: string);
90
+ }
91
+ /**
92
+ * Thrown when schema validation fails.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * throw new SchemaValidationError('Invalid schema structure', { field: 'name' });
97
+ * ```
98
+ */
99
+ export declare class SchemaValidationError extends ApiError {
100
+ constructor(message: string, context?: Record<string, unknown>);
101
+ }
102
+ /**
103
+ * Thrown when a request handler cannot be created.
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * throw new HandlerCreationError('Failed to create PostGraphile handler', { reason: 'timeout' });
108
+ * ```
109
+ */
110
+ export declare class HandlerCreationError extends ApiError {
111
+ constructor(message: string, context?: Record<string, unknown>);
112
+ }
113
+ /**
114
+ * Thrown when the database connection fails.
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * throw new DatabaseConnectionError('Connection timeout', { host: 'db.example.com' });
119
+ * ```
120
+ */
121
+ export declare class DatabaseConnectionError extends ApiError {
122
+ constructor(message: string, context?: Record<string, unknown>);
123
+ }
124
+ /**
125
+ * Thrown when a tenant attempts to access schemas they do not own.
126
+ * Returns 403 Forbidden to indicate the schemas exist but access is denied.
127
+ *
128
+ * Security Note: This error intentionally does not reveal which specific
129
+ * schemas exist to prevent information disclosure.
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * throw new SchemaAccessDeniedError(['schema1', 'schema2'], 'db-123');
134
+ * ```
135
+ */
136
+ export declare class SchemaAccessDeniedError extends ApiError {
137
+ constructor(schemas: string[], databaseId: string);
138
+ }
139
+ /**
140
+ * Thrown when domain resolution is ambiguous (multiple APIs match).
141
+ * This is a security concern as it indicates potential misconfiguration
142
+ * that could lead to unpredictable tenant routing.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * throw new AmbiguousTenantError('example.com', 'api', 2);
147
+ * ```
148
+ */
149
+ export declare class AmbiguousTenantError extends ApiError {
150
+ constructor(domain: string, subdomain: string | null, matchCount: number);
151
+ }
152
+ /**
153
+ * Thrown when admin authentication is required but not provided or invalid.
154
+ * Used for private API endpoints that require explicit admin credentials.
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * throw new AdminAuthRequiredError('Missing authorization header');
159
+ * ```
160
+ */
161
+ export declare class AdminAuthRequiredError extends ApiError {
162
+ constructor(reason: string);
163
+ }
164
+ /**
165
+ * Check if an error is an instance of ApiError or any of its subclasses.
166
+ *
167
+ * @param error - The value to check
168
+ * @returns True if error is an ApiError instance
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * try {
173
+ * await resolveApi(req);
174
+ * } catch (error) {
175
+ * if (isApiError(error)) {
176
+ * // TypeScript knows error has code, statusCode, context
177
+ * console.log(error.code);
178
+ * res.status(error.statusCode).json(error.toJSON());
179
+ * }
180
+ * }
181
+ * ```
182
+ */
183
+ export declare function isApiError(error: unknown): error is ApiError;
184
+ /**
185
+ * Check if an error has a specific error code.
186
+ * Returns false for non-ApiError values.
187
+ *
188
+ * @param error - The value to check
189
+ * @param code - The error code to match against
190
+ * @returns True if error is an ApiError with the specified code
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * if (hasErrorCode(error, ErrorCodes.DOMAIN_NOT_FOUND)) {
195
+ * // Handle domain not found specifically
196
+ * logDomainMisconfiguration(error.context?.fullDomain);
197
+ * }
198
+ * ```
199
+ */
200
+ export declare function hasErrorCode(error: unknown, code: string): error is ApiError;
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ /**
3
+ * Typed API Error System
4
+ *
5
+ * Provides strongly-typed error hierarchy for the GraphQL server with:
6
+ * - Consistent error classification
7
+ * - HTTP status mapping
8
+ * - Serialization for API responses and logging
9
+ *
10
+ * @module errors/api-errors
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.AdminAuthRequiredError = exports.AmbiguousTenantError = exports.SchemaAccessDeniedError = exports.DatabaseConnectionError = exports.HandlerCreationError = exports.SchemaValidationError = exports.NoValidSchemasError = exports.ApiNotFoundError = exports.DomainNotFoundError = exports.ApiError = exports.ErrorCodes = void 0;
14
+ exports.isApiError = isApiError;
15
+ exports.hasErrorCode = hasErrorCode;
16
+ // =============================================================================
17
+ // Error Codes Constant
18
+ // =============================================================================
19
+ /**
20
+ * Centralized error code definitions for external use and type safety.
21
+ * Use these constants instead of string literals for error code comparisons.
22
+ */
23
+ exports.ErrorCodes = {
24
+ DOMAIN_NOT_FOUND: 'DOMAIN_NOT_FOUND',
25
+ API_NOT_FOUND: 'API_NOT_FOUND',
26
+ NO_VALID_SCHEMAS: 'NO_VALID_SCHEMAS',
27
+ SCHEMA_INVALID: 'SCHEMA_INVALID',
28
+ SCHEMA_ACCESS_DENIED: 'SCHEMA_ACCESS_DENIED',
29
+ HANDLER_ERROR: 'HANDLER_ERROR',
30
+ DATABASE_CONNECTION_ERROR: 'DATABASE_CONNECTION_ERROR',
31
+ AMBIGUOUS_TENANT: 'AMBIGUOUS_TENANT',
32
+ ADMIN_AUTH_REQUIRED: 'ADMIN_AUTH_REQUIRED',
33
+ };
34
+ // =============================================================================
35
+ // Base Error Class
36
+ // =============================================================================
37
+ /**
38
+ * Base class for all API errors.
39
+ *
40
+ * Provides consistent structure for error handling including:
41
+ * - Machine-readable error codes
42
+ * - HTTP status code mapping
43
+ * - Optional debugging context
44
+ * - JSON serialization for API responses
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * throw new ApiError('CUSTOM_ERROR', 400, 'Something went wrong', { detail: 'info' });
49
+ * ```
50
+ */
51
+ class ApiError extends Error {
52
+ code;
53
+ statusCode;
54
+ context;
55
+ constructor(code, statusCode, message, context) {
56
+ super(message);
57
+ this.code = code;
58
+ this.statusCode = statusCode;
59
+ this.context = context;
60
+ this.name = 'ApiError';
61
+ // Ensure instanceof checks work correctly with ES5 transpilation
62
+ Object.setPrototypeOf(this, new.target.prototype);
63
+ // Capture stack trace for V8 engines (Node.js)
64
+ // Points to error origin rather than base class constructor
65
+ if (Error.captureStackTrace) {
66
+ Error.captureStackTrace(this, this.constructor);
67
+ }
68
+ }
69
+ /**
70
+ * Serializes the error for API responses and structured logging.
71
+ *
72
+ * @returns Object with error details suitable for JSON serialization
73
+ */
74
+ toJSON() {
75
+ return {
76
+ name: this.name,
77
+ code: this.code,
78
+ message: this.message,
79
+ statusCode: this.statusCode,
80
+ ...(this.context && { context: this.context }),
81
+ };
82
+ }
83
+ }
84
+ exports.ApiError = ApiError;
85
+ // =============================================================================
86
+ // Error Subclasses
87
+ // =============================================================================
88
+ /**
89
+ * Thrown when a domain lookup fails to find a matching API.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * throw new DomainNotFoundError('example.com', 'api');
94
+ * // Results in: "No API configured for domain: api.example.com"
95
+ * ```
96
+ */
97
+ class DomainNotFoundError extends ApiError {
98
+ constructor(domain, subdomain) {
99
+ const fullDomain = subdomain ? `${subdomain}.${domain}` : domain;
100
+ super(exports.ErrorCodes.DOMAIN_NOT_FOUND, 404, `No API configured for domain: ${fullDomain}`, { domain, subdomain, fullDomain });
101
+ this.name = 'DomainNotFoundError';
102
+ }
103
+ }
104
+ exports.DomainNotFoundError = DomainNotFoundError;
105
+ /**
106
+ * Thrown when a specific API cannot be found by its ID.
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * throw new ApiNotFoundError('api-123');
111
+ * ```
112
+ */
113
+ class ApiNotFoundError extends ApiError {
114
+ constructor(apiId) {
115
+ super(exports.ErrorCodes.API_NOT_FOUND, 404, `API not found: ${apiId}`, { apiId });
116
+ this.name = 'ApiNotFoundError';
117
+ }
118
+ }
119
+ exports.ApiNotFoundError = ApiNotFoundError;
120
+ /**
121
+ * Thrown when no valid schemas are found for an API.
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * throw new NoValidSchemasError('api-123');
126
+ * ```
127
+ */
128
+ class NoValidSchemasError extends ApiError {
129
+ constructor(apiId) {
130
+ super(exports.ErrorCodes.NO_VALID_SCHEMAS, 404, `No valid schemas found for API: ${apiId}`, { apiId });
131
+ this.name = 'NoValidSchemasError';
132
+ }
133
+ }
134
+ exports.NoValidSchemasError = NoValidSchemasError;
135
+ /**
136
+ * Thrown when schema validation fails.
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * throw new SchemaValidationError('Invalid schema structure', { field: 'name' });
141
+ * ```
142
+ */
143
+ class SchemaValidationError extends ApiError {
144
+ constructor(message, context) {
145
+ super(exports.ErrorCodes.SCHEMA_INVALID, 400, message, context);
146
+ this.name = 'SchemaValidationError';
147
+ }
148
+ }
149
+ exports.SchemaValidationError = SchemaValidationError;
150
+ /**
151
+ * Thrown when a request handler cannot be created.
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * throw new HandlerCreationError('Failed to create PostGraphile handler', { reason: 'timeout' });
156
+ * ```
157
+ */
158
+ class HandlerCreationError extends ApiError {
159
+ constructor(message, context) {
160
+ super(exports.ErrorCodes.HANDLER_ERROR, 500, message, context);
161
+ this.name = 'HandlerCreationError';
162
+ }
163
+ }
164
+ exports.HandlerCreationError = HandlerCreationError;
165
+ /**
166
+ * Thrown when the database connection fails.
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * throw new DatabaseConnectionError('Connection timeout', { host: 'db.example.com' });
171
+ * ```
172
+ */
173
+ class DatabaseConnectionError extends ApiError {
174
+ constructor(message, context) {
175
+ super(exports.ErrorCodes.DATABASE_CONNECTION_ERROR, 503, message, context);
176
+ this.name = 'DatabaseConnectionError';
177
+ }
178
+ }
179
+ exports.DatabaseConnectionError = DatabaseConnectionError;
180
+ /**
181
+ * Thrown when a tenant attempts to access schemas they do not own.
182
+ * Returns 403 Forbidden to indicate the schemas exist but access is denied.
183
+ *
184
+ * Security Note: This error intentionally does not reveal which specific
185
+ * schemas exist to prevent information disclosure.
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * throw new SchemaAccessDeniedError(['schema1', 'schema2'], 'db-123');
190
+ * ```
191
+ */
192
+ class SchemaAccessDeniedError extends ApiError {
193
+ constructor(schemas, databaseId) {
194
+ super(exports.ErrorCodes.SCHEMA_ACCESS_DENIED, 403, `Access denied: requested schemas are not associated with tenant`, { schemas, databaseId });
195
+ this.name = 'SchemaAccessDeniedError';
196
+ }
197
+ }
198
+ exports.SchemaAccessDeniedError = SchemaAccessDeniedError;
199
+ /**
200
+ * Thrown when domain resolution is ambiguous (multiple APIs match).
201
+ * This is a security concern as it indicates potential misconfiguration
202
+ * that could lead to unpredictable tenant routing.
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * throw new AmbiguousTenantError('example.com', 'api', 2);
207
+ * ```
208
+ */
209
+ class AmbiguousTenantError extends ApiError {
210
+ constructor(domain, subdomain, matchCount) {
211
+ const fullDomain = subdomain ? `${subdomain}.${domain}` : domain;
212
+ super(exports.ErrorCodes.AMBIGUOUS_TENANT, 500, `Ambiguous tenant resolution: multiple APIs (${matchCount}) match domain ${fullDomain}`, { domain, subdomain, fullDomain, matchCount });
213
+ this.name = 'AmbiguousTenantError';
214
+ }
215
+ }
216
+ exports.AmbiguousTenantError = AmbiguousTenantError;
217
+ /**
218
+ * Thrown when admin authentication is required but not provided or invalid.
219
+ * Used for private API endpoints that require explicit admin credentials.
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * throw new AdminAuthRequiredError('Missing authorization header');
224
+ * ```
225
+ */
226
+ class AdminAuthRequiredError extends ApiError {
227
+ constructor(reason) {
228
+ super(exports.ErrorCodes.ADMIN_AUTH_REQUIRED, 401, `Admin authentication required: ${reason}`, { reason });
229
+ this.name = 'AdminAuthRequiredError';
230
+ }
231
+ }
232
+ exports.AdminAuthRequiredError = AdminAuthRequiredError;
233
+ // =============================================================================
234
+ // Type Guards
235
+ // =============================================================================
236
+ /**
237
+ * Check if an error is an instance of ApiError or any of its subclasses.
238
+ *
239
+ * @param error - The value to check
240
+ * @returns True if error is an ApiError instance
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * try {
245
+ * await resolveApi(req);
246
+ * } catch (error) {
247
+ * if (isApiError(error)) {
248
+ * // TypeScript knows error has code, statusCode, context
249
+ * console.log(error.code);
250
+ * res.status(error.statusCode).json(error.toJSON());
251
+ * }
252
+ * }
253
+ * ```
254
+ */
255
+ function isApiError(error) {
256
+ return error instanceof ApiError;
257
+ }
258
+ /**
259
+ * Check if an error has a specific error code.
260
+ * Returns false for non-ApiError values.
261
+ *
262
+ * @param error - The value to check
263
+ * @param code - The error code to match against
264
+ * @returns True if error is an ApiError with the specified code
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * if (hasErrorCode(error, ErrorCodes.DOMAIN_NOT_FOUND)) {
269
+ * // Handle domain not found specifically
270
+ * logDomainMisconfiguration(error.context?.fullDomain);
271
+ * }
272
+ * ```
273
+ */
274
+ function hasErrorCode(error, code) {
275
+ return isApiError(error) && error.code === code;
276
+ }
@@ -139,7 +139,7 @@ export default (message = "We’re really sorry about that. The service you were
139
139
  }
140
140
 
141
141
  body {
142
- background-color: #dde7e9;
142
+ background-color: #F3F6FA;
143
143
  color: #01A1FF;
144
144
  font-family: 'Fjalla One', sans-serif;
145
145
  position: relative;