@constructive-io/graphql-server 5.7.5 → 5.10.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.
@@ -1,5 +1,6 @@
1
1
  import './types'; // for Request type
2
2
  import crypto from 'node:crypto';
3
+ import { classify, parse } from '@constructive-io/errors';
3
4
  import { getNodeEnv } from '@pgpmjs/env';
4
5
  import { Logger } from '@pgpmjs/logger';
5
6
  import { createGraphileInstance, graphileCache } from 'graphile-cache';
@@ -12,130 +13,73 @@ import { HandlerCreationError } from '../errors/api-errors';
12
13
  import { AuthCookiePlugin } from '../plugins/auth-cookie-plugin';
13
14
  import { observeGraphileBuild } from './observability/graphile-build-stats';
14
15
  const maskErrorLog = new Logger('graphile:maskError');
15
- const SAFE_ERROR_CODES = new Set([
16
- // GraphQL standard
16
+ /**
17
+ * GraphQL framework protocol codes. These originate in the GraphQL/grafast
18
+ * transport layer (not in constructive-db), so they are not Constructive domain
19
+ * codes in the `@constructive-io/errors` registry. They are always safe to
20
+ * surface — they carry no sensitive detail. Everything else (auth, account,
21
+ * resource, constraint, and every constructive-db code) is classified by the
22
+ * registry, which is the single source of truth for public vs. internal.
23
+ */
24
+ const GRAPHQL_PROTOCOL_CODES = new Set([
17
25
  'GRAPHQL_VALIDATION_FAILED',
18
26
  'GRAPHQL_PARSE_FAILED',
19
27
  'PERSISTED_QUERY_NOT_FOUND',
20
- 'PERSISTED_QUERY_NOT_SUPPORTED',
21
- // Auth
22
- 'UNAUTHENTICATED',
23
- 'NOT_AUTHENTICATED',
24
- 'USER_NOT_AUTHENTICATED',
25
- 'FORBIDDEN',
26
- 'BAD_USER_INPUT',
27
- 'INCORRECT_PASSWORD',
28
- 'PASSWORD_INSECURE',
29
- 'ACCOUNT_LOCKED',
30
- 'ACCOUNT_LOCKED_EXCEED_ATTEMPTS',
31
- 'ACCOUNT_DISABLED',
32
- 'ACCOUNT_EXISTS',
33
- 'ACCOUNT_NOT_FOUND',
34
- 'USER_NOT_FOUND',
35
- 'INVALID_USER',
36
- 'INVALID_TOKEN',
37
- 'INVALID_CODE',
38
- 'NO_PRIMARY_EMAIL',
39
- 'NO_CREDENTIALS',
40
- 'PASSWORD_LEN',
41
- 'INVITE_NOT_FOUND',
42
- 'INVITE_LIMIT',
43
- 'INVITE_EMAIL_NOT_FOUND',
44
- 'EMAIL_NOT_VERIFIED',
45
- 'PROFILE_ASSIGNMENT_REQUIRES_EMAIL_INVITE',
46
- 'ASSIGN_PROFILES_PERMISSION_REQUIRED',
47
- 'PROFILE_NOT_FOUND',
48
- 'PROFILE_EXCEEDS_PERMISSIONS',
49
- 'MEMBERSHIP_NOT_FOUND',
50
- 'INVALID_CREDENTIALS',
51
- // Auth method toggles (app-level allow_* settings)
52
- 'SIGN_UP_DISABLED',
53
- 'PASSWORD_SIGN_IN_DISABLED',
54
- 'PASSWORD_SIGN_UP_DISABLED',
55
- 'SSO_SIGN_IN_DISABLED',
56
- 'SSO_SIGN_UP_DISABLED',
57
- 'SSO_ACCOUNT_NOT_FOUND',
58
- 'CONNECTED_ACCOUNT_NOT_FOUND',
59
- 'MAGIC_LINK_SIGN_IN_DISABLED',
60
- 'MAGIC_LINK_SIGN_UP_DISABLED',
61
- 'EMAIL_OTP_SIGN_IN_DISABLED',
62
- 'SMS_SIGN_IN_DISABLED',
63
- 'SMS_SIGN_UP_DISABLED',
64
- // CSRF
65
- 'CSRF_TOKEN_REQUIRED',
66
- 'INVALID_CSRF_TOKEN',
67
- // Rate limiting / throttling
68
- 'TOO_MANY_REQUESTS',
69
- 'PASSWORD_RESET_LOCKED_EXCEED_ATTEMPTS',
70
- // TOTP / MFA / step-up
71
- 'TOTP_NOT_ENABLED',
72
- 'TOTP_ALREADY_ENABLED',
73
- 'TOTP_SETUP_NOT_INITIATED',
74
- 'MFA_REQUIRED',
75
- 'MFA_CHALLENGE_EXPIRED',
76
- 'INVALID_MFA_CHALLENGE',
77
- 'STEP_UP_REQUIRED',
78
- 'STEP_UP_REQUIRED_PASSWORD',
79
- 'STEP_UP_REQUIRED_PASSWORD_OR_MFA',
80
- // Sessions / API keys
81
- 'SESSION_NOT_FOUND',
82
- 'API_KEY_NOT_FOUND',
83
- 'CANNOT_DISCONNECT_LAST_AUTH_METHOD',
84
- 'CANNOT_REVOKE_CURRENT_SESSION',
85
- // Account / resource operations
86
- 'NOT_FOUND',
87
- 'NULL_VALUES_DISALLOWED',
88
- 'OBJECT_NOT_FOUND',
89
- 'OBJECT_NO_UPDATE',
90
- 'LIMIT_REACHED',
91
- 'REQUIRES_ONE_OWNER',
92
- 'DELETE_FIRST',
93
- 'REF_NOT_FOUND',
94
- 'CROSS_DATABASE_REF',
95
- 'GROUPS_REQ_ENTITIES',
96
- 'ALREADY_SCHEDULED',
97
- 'SINGLETON_TABLE',
98
- // Entity/field immutability
99
- 'IMMUTABLE_FIELD',
100
- 'IMMUTABLE_PROPS',
101
- 'IMMUTABLE_PEOPLESTAMPS',
102
- 'IMMUTABLE_TIMESTAMPS',
103
- 'CONST_TYPE_FIELDS_IMMUTABLE',
104
- // PublicKeySignature
105
- 'FEATURE_DISABLED',
106
- 'INVALID_PUBLIC_KEY',
107
- 'INVALID_MESSAGE',
108
- 'INVALID_SIGNATURE',
109
- 'NO_ACCOUNT_EXISTS',
110
- 'BAD_SIGNIN',
111
- // Upload
112
- 'UPLOAD_MIMETYPE',
113
- // PostgreSQL constraint violations (surfaced by PostGraphile)
114
- '23505', // unique_violation
115
- '23503', // foreign_key_violation
116
- '23502', // not_null_violation
117
- '23514', // check_violation
118
- '23P01' // exclusion_violation
28
+ 'PERSISTED_QUERY_NOT_SUPPORTED'
119
29
  ]);
30
+ /** A code is safe to surface when the registry classifies it public, or it is a
31
+ * GraphQL framework protocol code. */
32
+ const isPublicCode = (code) => Boolean(code) && (classify(code) === 'public' || GRAPHQL_PROTOCOL_CODES.has(code));
120
33
  /**
121
- * Production-aware error masking function.
34
+ * Normalize any GraphQL/database error into a canonical Constructive shape.
122
35
  *
123
- * In development: returns errors as-is for debugging.
124
- * In production: returns errors with explicit codes from the SAFE_ERROR_CODES
125
- * allowlist as-is, but masks unexpected/database errors with a reference ID
126
- * and logs the original.
36
+ * Database errors surface through Grafast without a populated `extensions.code`
37
+ * (the semantic code lives in the message, and any SQLSTATE/DETAIL lives on the
38
+ * underlying pg error at `originalError`). We parse `originalError` first so we
39
+ * can recover the structured code, then fall back to the GraphQL error itself.
40
+ */
41
+ const normalizeError = (error) => {
42
+ const original = error.originalError;
43
+ const fromOriginal = original ? parse(original) : null;
44
+ const parsed = fromOriginal?.code ? fromOriginal : parse(error);
45
+ return { code: parsed.code, context: parsed.context, class: parsed.class };
46
+ };
47
+ /**
48
+ * Production-aware error handling backed by `@constructive-io/errors`.
49
+ *
50
+ * 1. Enrich `extensions.code`/`class`/`context` from the parsed error so clients
51
+ * always receive a machine-readable code (fixing the gap where database
52
+ * errors reached clients as a bare message with empty `extensions`).
53
+ * 2. Surface public (registered/allowlisted) errors as-is.
54
+ * 3. In development, pass everything through (enriched) for debugging.
55
+ * 4. In production, mask internal/unknown errors behind a reference ID and log
56
+ * the original.
127
57
  */
128
58
  const maskError = (error) => {
129
- if (getNodeEnv() === 'development') {
130
- return error;
59
+ const { code, context, class: errorClass } = normalizeError(error);
60
+ // Lift the structured code onto extensions for every recognized error so
61
+ // clients always receive a machine-readable code (`extensions` is read-only
62
+ // on GraphQLError, so we build a formatted error rather than mutating it).
63
+ const extensions = { ...error.extensions };
64
+ if (code) {
65
+ extensions.code = code;
66
+ extensions.class = errorClass;
67
+ if (Object.keys(context).length > 0) {
68
+ extensions.context = context;
69
+ }
131
70
  }
132
- // Only expose errors with codes on the safe allowlist.
133
- // Note: grafserv strips originalError and internal extensions before
134
- // serializing to the client, so returning the full error object is safe here.
135
- if (error.extensions?.code && SAFE_ERROR_CODES.has(error.extensions.code)) {
136
- return error;
71
+ const effectiveCode = code ?? error.extensions?.code;
72
+ if (isPublicCode(effectiveCode) || getNodeEnv() === 'development') {
73
+ // Note: grafserv strips originalError and internal extensions before
74
+ // serializing to the client, so returning the enriched error is safe.
75
+ return {
76
+ message: error.message,
77
+ ...(error.locations ? { locations: error.locations } : {}),
78
+ ...(error.path ? { path: error.path } : {}),
79
+ extensions,
80
+ };
137
81
  }
138
- // Mask unexpected/database errors with a reference ID
82
+ // Mask internal/unknown errors with a reference ID.
139
83
  const errorId = crypto.randomBytes(8).toString('hex');
140
84
  maskErrorLog.error(`[masked-error:${errorId}]`, error);
141
85
  return {
@@ -9,6 +9,7 @@ exports.getInFlightKeys = getInFlightKeys;
9
9
  exports.clearInFlightMap = clearInFlightMap;
10
10
  require("./types"); // for Request type
11
11
  const node_crypto_1 = __importDefault(require("node:crypto"));
12
+ const errors_1 = require("@constructive-io/errors");
12
13
  const env_1 = require("@pgpmjs/env");
13
14
  const logger_1 = require("@pgpmjs/logger");
14
15
  const graphile_cache_1 = require("graphile-cache");
@@ -21,130 +22,73 @@ const api_errors_1 = require("../errors/api-errors");
21
22
  const auth_cookie_plugin_1 = require("../plugins/auth-cookie-plugin");
22
23
  const graphile_build_stats_1 = require("./observability/graphile-build-stats");
23
24
  const maskErrorLog = new logger_1.Logger('graphile:maskError');
24
- const SAFE_ERROR_CODES = new Set([
25
- // GraphQL standard
25
+ /**
26
+ * GraphQL framework protocol codes. These originate in the GraphQL/grafast
27
+ * transport layer (not in constructive-db), so they are not Constructive domain
28
+ * codes in the `@constructive-io/errors` registry. They are always safe to
29
+ * surface — they carry no sensitive detail. Everything else (auth, account,
30
+ * resource, constraint, and every constructive-db code) is classified by the
31
+ * registry, which is the single source of truth for public vs. internal.
32
+ */
33
+ const GRAPHQL_PROTOCOL_CODES = new Set([
26
34
  'GRAPHQL_VALIDATION_FAILED',
27
35
  'GRAPHQL_PARSE_FAILED',
28
36
  'PERSISTED_QUERY_NOT_FOUND',
29
- 'PERSISTED_QUERY_NOT_SUPPORTED',
30
- // Auth
31
- 'UNAUTHENTICATED',
32
- 'NOT_AUTHENTICATED',
33
- 'USER_NOT_AUTHENTICATED',
34
- 'FORBIDDEN',
35
- 'BAD_USER_INPUT',
36
- 'INCORRECT_PASSWORD',
37
- 'PASSWORD_INSECURE',
38
- 'ACCOUNT_LOCKED',
39
- 'ACCOUNT_LOCKED_EXCEED_ATTEMPTS',
40
- 'ACCOUNT_DISABLED',
41
- 'ACCOUNT_EXISTS',
42
- 'ACCOUNT_NOT_FOUND',
43
- 'USER_NOT_FOUND',
44
- 'INVALID_USER',
45
- 'INVALID_TOKEN',
46
- 'INVALID_CODE',
47
- 'NO_PRIMARY_EMAIL',
48
- 'NO_CREDENTIALS',
49
- 'PASSWORD_LEN',
50
- 'INVITE_NOT_FOUND',
51
- 'INVITE_LIMIT',
52
- 'INVITE_EMAIL_NOT_FOUND',
53
- 'EMAIL_NOT_VERIFIED',
54
- 'PROFILE_ASSIGNMENT_REQUIRES_EMAIL_INVITE',
55
- 'ASSIGN_PROFILES_PERMISSION_REQUIRED',
56
- 'PROFILE_NOT_FOUND',
57
- 'PROFILE_EXCEEDS_PERMISSIONS',
58
- 'MEMBERSHIP_NOT_FOUND',
59
- 'INVALID_CREDENTIALS',
60
- // Auth method toggles (app-level allow_* settings)
61
- 'SIGN_UP_DISABLED',
62
- 'PASSWORD_SIGN_IN_DISABLED',
63
- 'PASSWORD_SIGN_UP_DISABLED',
64
- 'SSO_SIGN_IN_DISABLED',
65
- 'SSO_SIGN_UP_DISABLED',
66
- 'SSO_ACCOUNT_NOT_FOUND',
67
- 'CONNECTED_ACCOUNT_NOT_FOUND',
68
- 'MAGIC_LINK_SIGN_IN_DISABLED',
69
- 'MAGIC_LINK_SIGN_UP_DISABLED',
70
- 'EMAIL_OTP_SIGN_IN_DISABLED',
71
- 'SMS_SIGN_IN_DISABLED',
72
- 'SMS_SIGN_UP_DISABLED',
73
- // CSRF
74
- 'CSRF_TOKEN_REQUIRED',
75
- 'INVALID_CSRF_TOKEN',
76
- // Rate limiting / throttling
77
- 'TOO_MANY_REQUESTS',
78
- 'PASSWORD_RESET_LOCKED_EXCEED_ATTEMPTS',
79
- // TOTP / MFA / step-up
80
- 'TOTP_NOT_ENABLED',
81
- 'TOTP_ALREADY_ENABLED',
82
- 'TOTP_SETUP_NOT_INITIATED',
83
- 'MFA_REQUIRED',
84
- 'MFA_CHALLENGE_EXPIRED',
85
- 'INVALID_MFA_CHALLENGE',
86
- 'STEP_UP_REQUIRED',
87
- 'STEP_UP_REQUIRED_PASSWORD',
88
- 'STEP_UP_REQUIRED_PASSWORD_OR_MFA',
89
- // Sessions / API keys
90
- 'SESSION_NOT_FOUND',
91
- 'API_KEY_NOT_FOUND',
92
- 'CANNOT_DISCONNECT_LAST_AUTH_METHOD',
93
- 'CANNOT_REVOKE_CURRENT_SESSION',
94
- // Account / resource operations
95
- 'NOT_FOUND',
96
- 'NULL_VALUES_DISALLOWED',
97
- 'OBJECT_NOT_FOUND',
98
- 'OBJECT_NO_UPDATE',
99
- 'LIMIT_REACHED',
100
- 'REQUIRES_ONE_OWNER',
101
- 'DELETE_FIRST',
102
- 'REF_NOT_FOUND',
103
- 'CROSS_DATABASE_REF',
104
- 'GROUPS_REQ_ENTITIES',
105
- 'ALREADY_SCHEDULED',
106
- 'SINGLETON_TABLE',
107
- // Entity/field immutability
108
- 'IMMUTABLE_FIELD',
109
- 'IMMUTABLE_PROPS',
110
- 'IMMUTABLE_PEOPLESTAMPS',
111
- 'IMMUTABLE_TIMESTAMPS',
112
- 'CONST_TYPE_FIELDS_IMMUTABLE',
113
- // PublicKeySignature
114
- 'FEATURE_DISABLED',
115
- 'INVALID_PUBLIC_KEY',
116
- 'INVALID_MESSAGE',
117
- 'INVALID_SIGNATURE',
118
- 'NO_ACCOUNT_EXISTS',
119
- 'BAD_SIGNIN',
120
- // Upload
121
- 'UPLOAD_MIMETYPE',
122
- // PostgreSQL constraint violations (surfaced by PostGraphile)
123
- '23505', // unique_violation
124
- '23503', // foreign_key_violation
125
- '23502', // not_null_violation
126
- '23514', // check_violation
127
- '23P01' // exclusion_violation
37
+ 'PERSISTED_QUERY_NOT_SUPPORTED'
128
38
  ]);
39
+ /** A code is safe to surface when the registry classifies it public, or it is a
40
+ * GraphQL framework protocol code. */
41
+ const isPublicCode = (code) => Boolean(code) && ((0, errors_1.classify)(code) === 'public' || GRAPHQL_PROTOCOL_CODES.has(code));
129
42
  /**
130
- * Production-aware error masking function.
43
+ * Normalize any GraphQL/database error into a canonical Constructive shape.
131
44
  *
132
- * In development: returns errors as-is for debugging.
133
- * In production: returns errors with explicit codes from the SAFE_ERROR_CODES
134
- * allowlist as-is, but masks unexpected/database errors with a reference ID
135
- * and logs the original.
45
+ * Database errors surface through Grafast without a populated `extensions.code`
46
+ * (the semantic code lives in the message, and any SQLSTATE/DETAIL lives on the
47
+ * underlying pg error at `originalError`). We parse `originalError` first so we
48
+ * can recover the structured code, then fall back to the GraphQL error itself.
49
+ */
50
+ const normalizeError = (error) => {
51
+ const original = error.originalError;
52
+ const fromOriginal = original ? (0, errors_1.parse)(original) : null;
53
+ const parsed = fromOriginal?.code ? fromOriginal : (0, errors_1.parse)(error);
54
+ return { code: parsed.code, context: parsed.context, class: parsed.class };
55
+ };
56
+ /**
57
+ * Production-aware error handling backed by `@constructive-io/errors`.
58
+ *
59
+ * 1. Enrich `extensions.code`/`class`/`context` from the parsed error so clients
60
+ * always receive a machine-readable code (fixing the gap where database
61
+ * errors reached clients as a bare message with empty `extensions`).
62
+ * 2. Surface public (registered/allowlisted) errors as-is.
63
+ * 3. In development, pass everything through (enriched) for debugging.
64
+ * 4. In production, mask internal/unknown errors behind a reference ID and log
65
+ * the original.
136
66
  */
137
67
  const maskError = (error) => {
138
- if ((0, env_1.getNodeEnv)() === 'development') {
139
- return error;
68
+ const { code, context, class: errorClass } = normalizeError(error);
69
+ // Lift the structured code onto extensions for every recognized error so
70
+ // clients always receive a machine-readable code (`extensions` is read-only
71
+ // on GraphQLError, so we build a formatted error rather than mutating it).
72
+ const extensions = { ...error.extensions };
73
+ if (code) {
74
+ extensions.code = code;
75
+ extensions.class = errorClass;
76
+ if (Object.keys(context).length > 0) {
77
+ extensions.context = context;
78
+ }
140
79
  }
141
- // Only expose errors with codes on the safe allowlist.
142
- // Note: grafserv strips originalError and internal extensions before
143
- // serializing to the client, so returning the full error object is safe here.
144
- if (error.extensions?.code && SAFE_ERROR_CODES.has(error.extensions.code)) {
145
- return error;
80
+ const effectiveCode = code ?? error.extensions?.code;
81
+ if (isPublicCode(effectiveCode) || (0, env_1.getNodeEnv)() === 'development') {
82
+ // Note: grafserv strips originalError and internal extensions before
83
+ // serializing to the client, so returning the enriched error is safe.
84
+ return {
85
+ message: error.message,
86
+ ...(error.locations ? { locations: error.locations } : {}),
87
+ ...(error.path ? { path: error.path } : {}),
88
+ extensions,
89
+ };
146
90
  }
147
- // Mask unexpected/database errors with a reference ID
91
+ // Mask internal/unknown errors with a reference ID.
148
92
  const errorId = node_crypto_1.default.randomBytes(8).toString('hex');
149
93
  maskErrorLog.error(`[masked-error:${errorId}]`, error);
150
94
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-server",
3
- "version": "5.7.5",
3
+ "version": "5.10.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Constructive GraphQL Server",
6
6
  "main": "index.js",
@@ -41,39 +41,40 @@
41
41
  "backend"
42
42
  ],
43
43
  "dependencies": {
44
- "@constructive-io/csrf": "^0.21.0",
45
- "@constructive-io/express-context": "^0.18.0",
46
- "@constructive-io/graphql-env": "^3.23.0",
47
- "@constructive-io/graphql-types": "^3.22.0",
48
- "@constructive-io/query-builder": "^3.4.0",
49
- "@constructive-io/s3-utils": "^2.24.0",
50
- "@constructive-io/url-domains": "^2.23.0",
44
+ "@constructive-io/csrf": "^0.23.0",
45
+ "@constructive-io/errors": "^0.4.0",
46
+ "@constructive-io/express-context": "^0.20.0",
47
+ "@constructive-io/graphql-env": "^3.25.0",
48
+ "@constructive-io/graphql-types": "^3.24.0",
49
+ "@constructive-io/query-builder": "^3.6.0",
50
+ "@constructive-io/s3-utils": "^2.26.0",
51
+ "@constructive-io/url-domains": "^2.25.0",
51
52
  "@graphile-contrib/pg-many-to-many": "2.0.0-rc.2",
52
- "@pgpmjs/env": "^2.33.0",
53
- "@pgpmjs/logger": "^2.18.0",
54
- "@pgpmjs/server-utils": "^3.19.0",
55
- "@pgpmjs/types": "^2.40.0",
56
- "agentic-server": "0.16.2",
53
+ "@pgpmjs/env": "^2.35.0",
54
+ "@pgpmjs/logger": "^2.20.0",
55
+ "@pgpmjs/server-utils": "^3.21.0",
56
+ "@pgpmjs/types": "^2.43.0",
57
+ "agentic-server": "0.18.0",
57
58
  "cors": "^2.8.6",
58
59
  "deepmerge": "^4.3.1",
59
60
  "express": "^5.2.1",
60
- "gql-ast": "^3.17.0",
61
+ "gql-ast": "^3.19.0",
61
62
  "grafast": "1.0.2",
62
63
  "grafserv": "1.0.0",
63
64
  "graphile-build": "5.0.2",
64
65
  "graphile-build-pg": "5.0.2",
65
- "graphile-cache": "^4.4.0",
66
+ "graphile-cache": "^4.6.0",
66
67
  "graphile-config": "1.0.1",
67
- "graphile-function-bindings": "^1.5.2",
68
- "graphile-settings": "^6.5.5",
68
+ "graphile-function-bindings": "^1.7.0",
69
+ "graphile-settings": "^6.7.0",
69
70
  "graphile-utils": "5.0.1",
70
71
  "graphql": "16.13.0",
71
72
  "graphql-upload": "^13.0.0",
72
73
  "lru-cache": "^11.2.7",
73
74
  "pg": "^8.21.0",
74
- "pg-cache": "^3.19.0",
75
- "pg-env": "^1.22.0",
76
- "pg-query-context": "^2.23.0",
75
+ "pg-cache": "^3.21.0",
76
+ "pg-env": "^1.24.0",
77
+ "pg-query-context": "^2.25.0",
77
78
  "pg-sql2": "5.0.1",
78
79
  "postgraphile": "5.0.3",
79
80
  "request-ip": "^3.3.0"
@@ -87,10 +88,10 @@
87
88
  "@types/pg": "^8.20.0",
88
89
  "@types/request-ip": "^0.0.41",
89
90
  "cookie-parser": "^1.4.7",
90
- "graphile-test": "5.5.2",
91
+ "graphile-test": "5.7.0",
91
92
  "makage": "^0.3.0",
92
93
  "nodemon": "^3.1.14",
93
94
  "ts-node": "^10.9.2"
94
95
  },
95
- "gitHead": "6f85eefed51d88b3a857ff36d20da44b7ebb47c9"
96
+ "gitHead": "3ba26a9dfa342feff5a2216aafea962132d51621"
96
97
  }