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