graphile-test 3.1.0 → 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.
- package/clean.js +1 -1
- package/context.d.ts +22 -9
- package/context.js +227 -44
- package/esm/clean.js +1 -1
- package/esm/context.js +228 -42
- package/esm/get-connections.js +15 -15
- package/esm/graphile-test.js +53 -22
- package/esm/index.js +3 -4
- package/get-connections.js +15 -15
- package/graphile-test.d.ts +9 -3
- package/graphile-test.js +53 -22
- package/index.d.ts +4 -4
- package/index.js +15 -19
- package/package.json +12 -8
- package/types.d.ts +64 -16
package/clean.js
CHANGED
|
@@ -24,7 +24,7 @@ const pruneDates = (row) => mapValues(row, (v, k) => {
|
|
|
24
24
|
return v;
|
|
25
25
|
});
|
|
26
26
|
exports.pruneDates = pruneDates;
|
|
27
|
-
const pruneIds = (row) => mapValues(row, (v, k) => (k === 'id' || (typeof k === 'string' && k.endsWith('_id'))) &&
|
|
27
|
+
const pruneIds = (row) => mapValues(row, (v, k) => (k === 'id' || k === 'rowId' || (typeof k === 'string' && k.endsWith('_id'))) &&
|
|
28
28
|
(typeof v === 'string' || typeof v === 'number')
|
|
29
29
|
? idReplacement(v)
|
|
30
30
|
: v);
|
package/context.d.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import { DocumentNode, ExecutionResult } from 'graphql';
|
|
1
|
+
import type { DocumentNode, ExecutionResult, GraphQLSchema } from 'graphql';
|
|
2
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
3
|
+
import { makePgService } from 'postgraphile/adaptors/pg';
|
|
2
4
|
import type { Client, Pool } from 'pg';
|
|
3
|
-
import { GetConnectionOpts, GetConnectionResult } from 'pgsql-test';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
export declare const runGraphQLInContext: <T = ExecutionResult>({ input, conn, pgPool, schema, options, authRole, query, variables, reqOptions }: {
|
|
5
|
+
import type { GetConnectionOpts, GetConnectionResult } from 'pgsql-test';
|
|
6
|
+
import type { GetConnectionsInput } from './types';
|
|
7
|
+
interface RunGraphQLOptions {
|
|
7
8
|
input: GetConnectionsInput & GetConnectionOpts;
|
|
8
9
|
conn: GetConnectionResult;
|
|
9
10
|
pgPool: Pool;
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
pgService: ReturnType<typeof makePgService>;
|
|
12
|
+
schema: GraphQLSchema;
|
|
13
|
+
resolvedPreset: GraphileConfig.ResolvedPreset;
|
|
12
14
|
authRole: string;
|
|
13
15
|
query: string | DocumentNode;
|
|
14
16
|
variables?: Record<string, any>;
|
|
15
|
-
reqOptions?: Record<string,
|
|
16
|
-
}
|
|
17
|
+
reqOptions?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Execute a GraphQL query in the v5 context using grafast's execute function.
|
|
21
|
+
*
|
|
22
|
+
* This replaces the v4 withPostGraphileContext pattern with grafast execution.
|
|
23
|
+
* Uses execute() with withPgClient to ensure proper connection handling and pgSettings.
|
|
24
|
+
*/
|
|
25
|
+
export declare const runGraphQLInContext: <T = ExecutionResult>({ input, conn, schema, resolvedPreset, pgService, authRole, query, variables, reqOptions, }: RunGraphQLOptions) => Promise<T>;
|
|
26
|
+
/**
|
|
27
|
+
* Set the PostgreSQL role and session settings on a client connection.
|
|
28
|
+
*/
|
|
17
29
|
export declare function setContextOnClient(pgClient: Client, pgSettings: Record<string, string>, role: string): Promise<void>;
|
|
30
|
+
export {};
|
package/context.js
CHANGED
|
@@ -1,60 +1,243 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.runGraphQLInContext = void 0;
|
|
7
4
|
exports.setContextOnClient = setContextOnClient;
|
|
8
5
|
const graphql_1 = require("graphql");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const grafast_1 = require("grafast");
|
|
7
|
+
/**
|
|
8
|
+
* Find a field node in the document by following the path.
|
|
9
|
+
* This is used to derive locations when grafast doesn't provide them.
|
|
10
|
+
*/
|
|
11
|
+
function findFieldNodeByPath(document, path) {
|
|
12
|
+
if (!path || path.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
// Find the operation definition
|
|
15
|
+
const operation = document.definitions.find((def) => def.kind === graphql_1.Kind.OPERATION_DEFINITION);
|
|
16
|
+
if (!operation)
|
|
17
|
+
return null;
|
|
18
|
+
// Navigate through the path to find the field
|
|
19
|
+
let selections = operation.selectionSet.selections;
|
|
20
|
+
let fieldNode = null;
|
|
21
|
+
for (const segment of path) {
|
|
22
|
+
if (typeof segment === 'number') {
|
|
23
|
+
// Array index - skip, the field is still the same
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// Find the field with this name
|
|
27
|
+
fieldNode = selections.find((sel) => sel.kind === graphql_1.Kind.FIELD && (sel.alias?.value ?? sel.name.value) === segment) ?? null;
|
|
28
|
+
if (!fieldNode)
|
|
29
|
+
return null;
|
|
30
|
+
// Move to nested selections if they exist
|
|
31
|
+
if (fieldNode.selectionSet) {
|
|
32
|
+
selections = fieldNode.selectionSet.selections;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return fieldNode;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Format a GraphQL error to match the v4 PostGraphile format.
|
|
39
|
+
*
|
|
40
|
+
* v5 grafast errors have different formatting:
|
|
41
|
+
* - They include `extensions: {}` even when empty
|
|
42
|
+
* - They may have `locations: undefined` instead of omitting the field
|
|
43
|
+
* - They don't always include locations even when nodes are available
|
|
44
|
+
*
|
|
45
|
+
* This normalizes errors to match v4 format for backward compatibility.
|
|
46
|
+
*
|
|
47
|
+
* @param error - The GraphQL error to format
|
|
48
|
+
* @param document - The original document (used to derive locations from path)
|
|
49
|
+
*/
|
|
50
|
+
function formatErrorToV4(error, document) {
|
|
51
|
+
const formatted = {
|
|
52
|
+
message: error.message,
|
|
53
|
+
};
|
|
54
|
+
// Try to get locations from the error directly first
|
|
55
|
+
let locations = error.locations;
|
|
56
|
+
// If no locations but nodes are available, try to extract from nodes
|
|
57
|
+
if ((!locations || locations.length === 0) && error.nodes && error.nodes.length > 0) {
|
|
58
|
+
const extractedLocations = error.nodes
|
|
59
|
+
.filter(node => node.loc)
|
|
60
|
+
.map(node => {
|
|
61
|
+
const loc = node.loc;
|
|
62
|
+
if (loc && loc.source) {
|
|
63
|
+
// Calculate line and column from the source
|
|
64
|
+
const { startToken } = loc;
|
|
65
|
+
if (startToken) {
|
|
66
|
+
return { line: startToken.line, column: startToken.column };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
})
|
|
71
|
+
.filter((loc) => loc !== null);
|
|
72
|
+
if (extractedLocations.length > 0) {
|
|
73
|
+
locations = extractedLocations;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// If still no locations and we have a path and document, try to derive from path
|
|
77
|
+
if ((!locations || locations.length === 0) && error.path && document) {
|
|
78
|
+
const fieldNode = findFieldNodeByPath(document, error.path);
|
|
79
|
+
if (fieldNode?.loc) {
|
|
80
|
+
const { startToken } = fieldNode.loc;
|
|
81
|
+
if (startToken) {
|
|
82
|
+
locations = [{ line: startToken.line, column: startToken.column }];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Only include locations if it's a non-empty array
|
|
87
|
+
if (locations && Array.isArray(locations) && locations.length > 0) {
|
|
88
|
+
formatted.locations = locations;
|
|
89
|
+
}
|
|
90
|
+
// Only include path if it exists
|
|
91
|
+
if (error.path && error.path.length > 0) {
|
|
92
|
+
formatted.path = error.path;
|
|
93
|
+
}
|
|
94
|
+
// Only include extensions if it has non-empty content
|
|
95
|
+
if (error.extensions && Object.keys(error.extensions).length > 0) {
|
|
96
|
+
formatted.extensions = error.extensions;
|
|
97
|
+
}
|
|
98
|
+
return formatted;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Normalize an ExecutionResult to match v4 PostGraphile output format.
|
|
102
|
+
* This ensures backward compatibility with existing tests and consumers.
|
|
103
|
+
*
|
|
104
|
+
* @param result - The execution result from grafast
|
|
105
|
+
* @param document - The original document (used to derive locations from path)
|
|
106
|
+
*/
|
|
107
|
+
function normalizeResult(result, document) {
|
|
108
|
+
const normalized = {
|
|
109
|
+
data: result.data,
|
|
110
|
+
};
|
|
111
|
+
// Format errors to match v4 style
|
|
112
|
+
if (result.errors && result.errors.length > 0) {
|
|
113
|
+
normalized.errors = result.errors.map(err => formatErrorToV4(err, document));
|
|
114
|
+
}
|
|
115
|
+
// Only include extensions if present and non-empty
|
|
116
|
+
if (result.extensions && Object.keys(result.extensions).length > 0) {
|
|
117
|
+
normalized.extensions = result.extensions;
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Execute a GraphQL query in the v5 context using grafast's execute function.
|
|
123
|
+
*
|
|
124
|
+
* This replaces the v4 withPostGraphileContext pattern with grafast execution.
|
|
125
|
+
* Uses execute() with withPgClient to ensure proper connection handling and pgSettings.
|
|
126
|
+
*/
|
|
127
|
+
const runGraphQLInContext = async ({ input, conn, schema, resolvedPreset, pgService, authRole, query, variables, reqOptions = {}, }) => {
|
|
13
128
|
if (!conn.pg.client) {
|
|
14
129
|
throw new Error('pgClient is required and must be provided externally.');
|
|
15
130
|
}
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
131
|
+
// Get the appropriate connection (root or database-specific)
|
|
132
|
+
const pgConn = input.useRoot ? conn.pg : conn.db;
|
|
133
|
+
const pgClient = pgConn.client;
|
|
134
|
+
// Build pgSettings by merging:
|
|
135
|
+
// 1. Context from db.setContext() (e.g., role, myapp.user_id)
|
|
136
|
+
// 2. Settings from reqOptions.pgSettings (explicit overrides)
|
|
137
|
+
// The role from getContext() takes precedence over authRole default
|
|
138
|
+
const dbContext = pgConn.getContext();
|
|
139
|
+
const reqPgSettings = reqOptions.pgSettings ?? {};
|
|
140
|
+
// Start with authRole as default, then apply db context, then request overrides
|
|
141
|
+
const pgSettings = {
|
|
142
|
+
role: authRole,
|
|
143
|
+
...Object.fromEntries(Object.entries(dbContext).filter(([, v]) => v !== null)),
|
|
144
|
+
...reqPgSettings,
|
|
145
|
+
};
|
|
146
|
+
// Set role and context on the client for direct queries
|
|
147
|
+
await setContextOnClient(pgClient, pgSettings, pgSettings.role);
|
|
148
|
+
await pgConn.ctxQuery();
|
|
149
|
+
// Convert query to DocumentNode if it's a string
|
|
150
|
+
// Also ensure we have location information for error reporting
|
|
151
|
+
let document;
|
|
152
|
+
if (typeof query === 'string') {
|
|
153
|
+
document = (0, graphql_1.parse)(query);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// For DocumentNode (from graphql-tag), re-parse from source to get locations
|
|
157
|
+
// graphql-tag doesn't preserve location info by default
|
|
158
|
+
const source = (0, graphql_1.print)(query);
|
|
159
|
+
document = (0, graphql_1.parse)(source);
|
|
160
|
+
}
|
|
161
|
+
// Build context with pgSettings and withPgClient
|
|
162
|
+
// This is the v5 pattern used by executor.ts and api.ts
|
|
163
|
+
const contextValue = {
|
|
164
|
+
pgSettings,
|
|
165
|
+
// Include additional request options (excluding pgSettings to avoid duplication)
|
|
166
|
+
...Object.fromEntries(Object.entries(reqOptions).filter(([key]) => key !== 'pgSettings')),
|
|
167
|
+
};
|
|
168
|
+
// Provide a custom withPgClient function that uses the test client
|
|
169
|
+
// This ensures GraphQL operations run within the test transaction
|
|
170
|
+
// instead of getting a new connection from the pool
|
|
171
|
+
const withPgClientKey = pgService.withPgClientKey ?? 'withPgClient';
|
|
172
|
+
contextValue[withPgClientKey] = async (_pgSettings, callback) => {
|
|
173
|
+
// Simply use the test client - it's already in a transaction
|
|
174
|
+
// The pgSettings have already been applied above via setContextOnClient
|
|
175
|
+
return callback(pgClient);
|
|
176
|
+
};
|
|
177
|
+
// Check if we're in a transaction by looking at the test client's transaction state
|
|
178
|
+
// When useRoot is true, we might not be in a transaction
|
|
179
|
+
// pgsql-test's `db` client is in a transaction, but `pg` (root) client may not be
|
|
180
|
+
const isInTransaction = !input.useRoot;
|
|
181
|
+
// Wrap the entire query execution in a savepoint if we're in a transaction
|
|
182
|
+
// This matches v4 PostGraphile behavior where each mutation is wrapped in a savepoint
|
|
183
|
+
// allowing the transaction to continue after a database error
|
|
184
|
+
const executionSavepoint = isInTransaction
|
|
185
|
+
? `graphile_exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
186
|
+
: null;
|
|
187
|
+
if (executionSavepoint) {
|
|
188
|
+
await pgClient.query(`SAVEPOINT ${executionSavepoint}`);
|
|
189
|
+
}
|
|
190
|
+
let rawResult;
|
|
191
|
+
try {
|
|
192
|
+
// Execute using grafast's execute function - the v5 execution engine
|
|
193
|
+
rawResult = await (0, grafast_1.execute)({
|
|
45
194
|
schema,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
195
|
+
document,
|
|
196
|
+
variableValues: variables ?? undefined,
|
|
197
|
+
contextValue,
|
|
198
|
+
resolvedPreset,
|
|
49
199
|
});
|
|
50
|
-
|
|
51
|
-
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// Rollback to savepoint on execution error
|
|
203
|
+
if (executionSavepoint) {
|
|
204
|
+
await pgClient.query(`ROLLBACK TO SAVEPOINT ${executionSavepoint}`);
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
// Handle streaming results (subscriptions return AsyncGenerator)
|
|
209
|
+
// For normal queries, we get ExecutionResult directly
|
|
210
|
+
if (Symbol.asyncIterator in rawResult) {
|
|
211
|
+
// This is a subscription/streaming result - not supported in test context
|
|
212
|
+
throw new Error('Streaming results (subscriptions) are not supported in test context');
|
|
213
|
+
}
|
|
214
|
+
const result = rawResult;
|
|
215
|
+
// If there were errors, rollback to savepoint to keep transaction usable
|
|
216
|
+
// This matches v4 behavior where database errors don't abort the transaction
|
|
217
|
+
if (executionSavepoint) {
|
|
218
|
+
if (result.errors && result.errors.length > 0) {
|
|
219
|
+
try {
|
|
220
|
+
await pgClient.query(`ROLLBACK TO SAVEPOINT ${executionSavepoint}`);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Ignore if savepoint doesn't exist
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Release the savepoint on success
|
|
228
|
+
await pgClient.query(`RELEASE SAVEPOINT ${executionSavepoint}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Normalize the result to match v4 PostGraphile format for backward compatibility
|
|
232
|
+
return normalizeResult(result, document);
|
|
52
233
|
};
|
|
53
234
|
exports.runGraphQLInContext = runGraphQLInContext;
|
|
54
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Set the PostgreSQL role and session settings on a client connection.
|
|
237
|
+
*/
|
|
55
238
|
async function setContextOnClient(pgClient, pgSettings, role) {
|
|
56
|
-
await pgClient.query(
|
|
239
|
+
await pgClient.query('SELECT set_config($1, $2, true)', ['role', role]);
|
|
57
240
|
for (const [key, value] of Object.entries(pgSettings)) {
|
|
58
|
-
await pgClient.query(
|
|
241
|
+
await pgClient.query('SELECT set_config($1, $2, true)', [key, String(value)]);
|
|
59
242
|
}
|
|
60
243
|
}
|
package/esm/clean.js
CHANGED
|
@@ -20,7 +20,7 @@ export const pruneDates = (row) => mapValues(row, (v, k) => {
|
|
|
20
20
|
}
|
|
21
21
|
return v;
|
|
22
22
|
});
|
|
23
|
-
export const pruneIds = (row) => mapValues(row, (v, k) => (k === 'id' || (typeof k === 'string' && k.endsWith('_id'))) &&
|
|
23
|
+
export const pruneIds = (row) => mapValues(row, (v, k) => (k === 'id' || k === 'rowId' || (typeof k === 'string' && k.endsWith('_id'))) &&
|
|
24
24
|
(typeof v === 'string' || typeof v === 'number')
|
|
25
25
|
? idReplacement(v)
|
|
26
26
|
: v);
|
package/esm/context.js
CHANGED
|
@@ -1,52 +1,238 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { parse, print, Kind } from 'graphql';
|
|
2
|
+
import { execute } from 'grafast';
|
|
3
|
+
/**
|
|
4
|
+
* Find a field node in the document by following the path.
|
|
5
|
+
* This is used to derive locations when grafast doesn't provide them.
|
|
6
|
+
*/
|
|
7
|
+
function findFieldNodeByPath(document, path) {
|
|
8
|
+
if (!path || path.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
// Find the operation definition
|
|
11
|
+
const operation = document.definitions.find((def) => def.kind === Kind.OPERATION_DEFINITION);
|
|
12
|
+
if (!operation)
|
|
13
|
+
return null;
|
|
14
|
+
// Navigate through the path to find the field
|
|
15
|
+
let selections = operation.selectionSet.selections;
|
|
16
|
+
let fieldNode = null;
|
|
17
|
+
for (const segment of path) {
|
|
18
|
+
if (typeof segment === 'number') {
|
|
19
|
+
// Array index - skip, the field is still the same
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
// Find the field with this name
|
|
23
|
+
fieldNode = selections.find((sel) => sel.kind === Kind.FIELD && (sel.alias?.value ?? sel.name.value) === segment) ?? null;
|
|
24
|
+
if (!fieldNode)
|
|
25
|
+
return null;
|
|
26
|
+
// Move to nested selections if they exist
|
|
27
|
+
if (fieldNode.selectionSet) {
|
|
28
|
+
selections = fieldNode.selectionSet.selections;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return fieldNode;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Format a GraphQL error to match the v4 PostGraphile format.
|
|
35
|
+
*
|
|
36
|
+
* v5 grafast errors have different formatting:
|
|
37
|
+
* - They include `extensions: {}` even when empty
|
|
38
|
+
* - They may have `locations: undefined` instead of omitting the field
|
|
39
|
+
* - They don't always include locations even when nodes are available
|
|
40
|
+
*
|
|
41
|
+
* This normalizes errors to match v4 format for backward compatibility.
|
|
42
|
+
*
|
|
43
|
+
* @param error - The GraphQL error to format
|
|
44
|
+
* @param document - The original document (used to derive locations from path)
|
|
45
|
+
*/
|
|
46
|
+
function formatErrorToV4(error, document) {
|
|
47
|
+
const formatted = {
|
|
48
|
+
message: error.message,
|
|
49
|
+
};
|
|
50
|
+
// Try to get locations from the error directly first
|
|
51
|
+
let locations = error.locations;
|
|
52
|
+
// If no locations but nodes are available, try to extract from nodes
|
|
53
|
+
if ((!locations || locations.length === 0) && error.nodes && error.nodes.length > 0) {
|
|
54
|
+
const extractedLocations = error.nodes
|
|
55
|
+
.filter(node => node.loc)
|
|
56
|
+
.map(node => {
|
|
57
|
+
const loc = node.loc;
|
|
58
|
+
if (loc && loc.source) {
|
|
59
|
+
// Calculate line and column from the source
|
|
60
|
+
const { startToken } = loc;
|
|
61
|
+
if (startToken) {
|
|
62
|
+
return { line: startToken.line, column: startToken.column };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
})
|
|
67
|
+
.filter((loc) => loc !== null);
|
|
68
|
+
if (extractedLocations.length > 0) {
|
|
69
|
+
locations = extractedLocations;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// If still no locations and we have a path and document, try to derive from path
|
|
73
|
+
if ((!locations || locations.length === 0) && error.path && document) {
|
|
74
|
+
const fieldNode = findFieldNodeByPath(document, error.path);
|
|
75
|
+
if (fieldNode?.loc) {
|
|
76
|
+
const { startToken } = fieldNode.loc;
|
|
77
|
+
if (startToken) {
|
|
78
|
+
locations = [{ line: startToken.line, column: startToken.column }];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Only include locations if it's a non-empty array
|
|
83
|
+
if (locations && Array.isArray(locations) && locations.length > 0) {
|
|
84
|
+
formatted.locations = locations;
|
|
85
|
+
}
|
|
86
|
+
// Only include path if it exists
|
|
87
|
+
if (error.path && error.path.length > 0) {
|
|
88
|
+
formatted.path = error.path;
|
|
89
|
+
}
|
|
90
|
+
// Only include extensions if it has non-empty content
|
|
91
|
+
if (error.extensions && Object.keys(error.extensions).length > 0) {
|
|
92
|
+
formatted.extensions = error.extensions;
|
|
93
|
+
}
|
|
94
|
+
return formatted;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Normalize an ExecutionResult to match v4 PostGraphile output format.
|
|
98
|
+
* This ensures backward compatibility with existing tests and consumers.
|
|
99
|
+
*
|
|
100
|
+
* @param result - The execution result from grafast
|
|
101
|
+
* @param document - The original document (used to derive locations from path)
|
|
102
|
+
*/
|
|
103
|
+
function normalizeResult(result, document) {
|
|
104
|
+
const normalized = {
|
|
105
|
+
data: result.data,
|
|
106
|
+
};
|
|
107
|
+
// Format errors to match v4 style
|
|
108
|
+
if (result.errors && result.errors.length > 0) {
|
|
109
|
+
normalized.errors = result.errors.map(err => formatErrorToV4(err, document));
|
|
110
|
+
}
|
|
111
|
+
// Only include extensions if present and non-empty
|
|
112
|
+
if (result.extensions && Object.keys(result.extensions).length > 0) {
|
|
113
|
+
normalized.extensions = result.extensions;
|
|
114
|
+
}
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Execute a GraphQL query in the v5 context using grafast's execute function.
|
|
119
|
+
*
|
|
120
|
+
* This replaces the v4 withPostGraphileContext pattern with grafast execution.
|
|
121
|
+
* Uses execute() with withPgClient to ensure proper connection handling and pgSettings.
|
|
122
|
+
*/
|
|
123
|
+
export const runGraphQLInContext = async ({ input, conn, schema, resolvedPreset, pgService, authRole, query, variables, reqOptions = {}, }) => {
|
|
6
124
|
if (!conn.pg.client) {
|
|
7
125
|
throw new Error('pgClient is required and must be provided externally.');
|
|
8
126
|
}
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
127
|
+
// Get the appropriate connection (root or database-specific)
|
|
128
|
+
const pgConn = input.useRoot ? conn.pg : conn.db;
|
|
129
|
+
const pgClient = pgConn.client;
|
|
130
|
+
// Build pgSettings by merging:
|
|
131
|
+
// 1. Context from db.setContext() (e.g., role, myapp.user_id)
|
|
132
|
+
// 2. Settings from reqOptions.pgSettings (explicit overrides)
|
|
133
|
+
// The role from getContext() takes precedence over authRole default
|
|
134
|
+
const dbContext = pgConn.getContext();
|
|
135
|
+
const reqPgSettings = reqOptions.pgSettings ?? {};
|
|
136
|
+
// Start with authRole as default, then apply db context, then request overrides
|
|
137
|
+
const pgSettings = {
|
|
138
|
+
role: authRole,
|
|
139
|
+
...Object.fromEntries(Object.entries(dbContext).filter(([, v]) => v !== null)),
|
|
140
|
+
...reqPgSettings,
|
|
141
|
+
};
|
|
142
|
+
// Set role and context on the client for direct queries
|
|
143
|
+
await setContextOnClient(pgClient, pgSettings, pgSettings.role);
|
|
144
|
+
await pgConn.ctxQuery();
|
|
145
|
+
// Convert query to DocumentNode if it's a string
|
|
146
|
+
// Also ensure we have location information for error reporting
|
|
147
|
+
let document;
|
|
148
|
+
if (typeof query === 'string') {
|
|
149
|
+
document = parse(query);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// For DocumentNode (from graphql-tag), re-parse from source to get locations
|
|
153
|
+
// graphql-tag doesn't preserve location info by default
|
|
154
|
+
const source = print(query);
|
|
155
|
+
document = parse(source);
|
|
156
|
+
}
|
|
157
|
+
// Build context with pgSettings and withPgClient
|
|
158
|
+
// This is the v5 pattern used by executor.ts and api.ts
|
|
159
|
+
const contextValue = {
|
|
160
|
+
pgSettings,
|
|
161
|
+
// Include additional request options (excluding pgSettings to avoid duplication)
|
|
162
|
+
...Object.fromEntries(Object.entries(reqOptions).filter(([key]) => key !== 'pgSettings')),
|
|
163
|
+
};
|
|
164
|
+
// Provide a custom withPgClient function that uses the test client
|
|
165
|
+
// This ensures GraphQL operations run within the test transaction
|
|
166
|
+
// instead of getting a new connection from the pool
|
|
167
|
+
const withPgClientKey = pgService.withPgClientKey ?? 'withPgClient';
|
|
168
|
+
contextValue[withPgClientKey] = async (_pgSettings, callback) => {
|
|
169
|
+
// Simply use the test client - it's already in a transaction
|
|
170
|
+
// The pgSettings have already been applied above via setContextOnClient
|
|
171
|
+
return callback(pgClient);
|
|
172
|
+
};
|
|
173
|
+
// Check if we're in a transaction by looking at the test client's transaction state
|
|
174
|
+
// When useRoot is true, we might not be in a transaction
|
|
175
|
+
// pgsql-test's `db` client is in a transaction, but `pg` (root) client may not be
|
|
176
|
+
const isInTransaction = !input.useRoot;
|
|
177
|
+
// Wrap the entire query execution in a savepoint if we're in a transaction
|
|
178
|
+
// This matches v4 PostGraphile behavior where each mutation is wrapped in a savepoint
|
|
179
|
+
// allowing the transaction to continue after a database error
|
|
180
|
+
const executionSavepoint = isInTransaction
|
|
181
|
+
? `graphile_exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
182
|
+
: null;
|
|
183
|
+
if (executionSavepoint) {
|
|
184
|
+
await pgClient.query(`SAVEPOINT ${executionSavepoint}`);
|
|
185
|
+
}
|
|
186
|
+
let rawResult;
|
|
187
|
+
try {
|
|
188
|
+
// Execute using grafast's execute function - the v5 execution engine
|
|
189
|
+
rawResult = await execute({
|
|
38
190
|
schema,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
191
|
+
document,
|
|
192
|
+
variableValues: variables ?? undefined,
|
|
193
|
+
contextValue,
|
|
194
|
+
resolvedPreset,
|
|
42
195
|
});
|
|
43
|
-
|
|
44
|
-
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
// Rollback to savepoint on execution error
|
|
199
|
+
if (executionSavepoint) {
|
|
200
|
+
await pgClient.query(`ROLLBACK TO SAVEPOINT ${executionSavepoint}`);
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
// Handle streaming results (subscriptions return AsyncGenerator)
|
|
205
|
+
// For normal queries, we get ExecutionResult directly
|
|
206
|
+
if (Symbol.asyncIterator in rawResult) {
|
|
207
|
+
// This is a subscription/streaming result - not supported in test context
|
|
208
|
+
throw new Error('Streaming results (subscriptions) are not supported in test context');
|
|
209
|
+
}
|
|
210
|
+
const result = rawResult;
|
|
211
|
+
// If there were errors, rollback to savepoint to keep transaction usable
|
|
212
|
+
// This matches v4 behavior where database errors don't abort the transaction
|
|
213
|
+
if (executionSavepoint) {
|
|
214
|
+
if (result.errors && result.errors.length > 0) {
|
|
215
|
+
try {
|
|
216
|
+
await pgClient.query(`ROLLBACK TO SAVEPOINT ${executionSavepoint}`);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Ignore if savepoint doesn't exist
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Release the savepoint on success
|
|
224
|
+
await pgClient.query(`RELEASE SAVEPOINT ${executionSavepoint}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Normalize the result to match v4 PostGraphile format for backward compatibility
|
|
228
|
+
return normalizeResult(result, document);
|
|
45
229
|
};
|
|
46
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Set the PostgreSQL role and session settings on a client connection.
|
|
232
|
+
*/
|
|
47
233
|
export async function setContextOnClient(pgClient, pgSettings, role) {
|
|
48
|
-
await pgClient.query(
|
|
234
|
+
await pgClient.query('SELECT set_config($1, $2, true)', ['role', role]);
|
|
49
235
|
for (const [key, value] of Object.entries(pgSettings)) {
|
|
50
|
-
await pgClient.query(
|
|
236
|
+
await pgClient.query('SELECT set_config($1, $2, true)', [key, String(value)]);
|
|
51
237
|
}
|
|
52
238
|
}
|
package/esm/get-connections.js
CHANGED
|
@@ -28,7 +28,7 @@ const createConnectionsBase = async (input, seedAdapters) => {
|
|
|
28
28
|
teardown,
|
|
29
29
|
baseQuery,
|
|
30
30
|
baseQueryPositional,
|
|
31
|
-
gqlContext
|
|
31
|
+
gqlContext,
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
34
|
// ============================================================================
|
|
@@ -44,7 +44,7 @@ export const getConnectionsObject = async (input, seedAdapters) => {
|
|
|
44
44
|
db,
|
|
45
45
|
teardown,
|
|
46
46
|
query: baseQuery,
|
|
47
|
-
gqlContext
|
|
47
|
+
gqlContext,
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
50
|
/**
|
|
@@ -57,7 +57,7 @@ export const getConnectionsObjectUnwrapped = async (input, seedAdapters) => {
|
|
|
57
57
|
pg,
|
|
58
58
|
db,
|
|
59
59
|
teardown,
|
|
60
|
-
query
|
|
60
|
+
query,
|
|
61
61
|
};
|
|
62
62
|
};
|
|
63
63
|
/**
|
|
@@ -75,7 +75,7 @@ export const getConnectionsObjectWithLogging = async (input, seedAdapters) => {
|
|
|
75
75
|
pg,
|
|
76
76
|
db,
|
|
77
77
|
teardown,
|
|
78
|
-
query
|
|
78
|
+
query,
|
|
79
79
|
};
|
|
80
80
|
};
|
|
81
81
|
/**
|
|
@@ -94,7 +94,7 @@ export const getConnectionsObjectWithTiming = async (input, seedAdapters) => {
|
|
|
94
94
|
pg,
|
|
95
95
|
db,
|
|
96
96
|
teardown,
|
|
97
|
-
query
|
|
97
|
+
query,
|
|
98
98
|
};
|
|
99
99
|
};
|
|
100
100
|
// ============================================================================
|
|
@@ -109,7 +109,7 @@ export const getConnections = async (input, seedAdapters) => {
|
|
|
109
109
|
pg,
|
|
110
110
|
db,
|
|
111
111
|
teardown,
|
|
112
|
-
query: baseQueryPositional
|
|
112
|
+
query: baseQueryPositional,
|
|
113
113
|
};
|
|
114
114
|
};
|
|
115
115
|
/**
|
|
@@ -117,12 +117,12 @@ export const getConnections = async (input, seedAdapters) => {
|
|
|
117
117
|
*/
|
|
118
118
|
export const getConnectionsUnwrapped = async (input, seedAdapters) => {
|
|
119
119
|
const { pg, db, teardown, baseQueryPositional } = await createConnectionsBase(input, seedAdapters);
|
|
120
|
-
const query = async (
|
|
120
|
+
const query = async (queryDoc, variables, commit, reqOptions) => unwrap(await baseQueryPositional(queryDoc, variables, commit, reqOptions));
|
|
121
121
|
return {
|
|
122
122
|
pg,
|
|
123
123
|
db,
|
|
124
124
|
teardown,
|
|
125
|
-
query
|
|
125
|
+
query,
|
|
126
126
|
};
|
|
127
127
|
};
|
|
128
128
|
/**
|
|
@@ -130,9 +130,9 @@ export const getConnectionsUnwrapped = async (input, seedAdapters) => {
|
|
|
130
130
|
*/
|
|
131
131
|
export const getConnectionsWithLogging = async (input, seedAdapters) => {
|
|
132
132
|
const { pg, db, teardown, baseQueryPositional } = await createConnectionsBase(input, seedAdapters);
|
|
133
|
-
const query = async (
|
|
134
|
-
console.log('Executing positional GraphQL query:',
|
|
135
|
-
const result = await baseQueryPositional(
|
|
133
|
+
const query = async (queryDoc, variables, commit, reqOptions) => {
|
|
134
|
+
console.log('Executing positional GraphQL query:', queryDoc);
|
|
135
|
+
const result = await baseQueryPositional(queryDoc, variables, commit, reqOptions);
|
|
136
136
|
console.log('GraphQL result:', result);
|
|
137
137
|
return result;
|
|
138
138
|
};
|
|
@@ -140,7 +140,7 @@ export const getConnectionsWithLogging = async (input, seedAdapters) => {
|
|
|
140
140
|
pg,
|
|
141
141
|
db,
|
|
142
142
|
teardown,
|
|
143
|
-
query
|
|
143
|
+
query,
|
|
144
144
|
};
|
|
145
145
|
};
|
|
146
146
|
/**
|
|
@@ -148,9 +148,9 @@ export const getConnectionsWithLogging = async (input, seedAdapters) => {
|
|
|
148
148
|
*/
|
|
149
149
|
export const getConnectionsWithTiming = async (input, seedAdapters) => {
|
|
150
150
|
const { pg, db, teardown, baseQueryPositional } = await createConnectionsBase(input, seedAdapters);
|
|
151
|
-
const query = async (
|
|
151
|
+
const query = async (queryDoc, variables, commit, reqOptions) => {
|
|
152
152
|
const start = Date.now();
|
|
153
|
-
const result = await baseQueryPositional(
|
|
153
|
+
const result = await baseQueryPositional(queryDoc, variables, commit, reqOptions);
|
|
154
154
|
const duration = Date.now() - start;
|
|
155
155
|
console.log(`Positional GraphQL query took ${duration}ms`);
|
|
156
156
|
return result;
|
|
@@ -159,6 +159,6 @@ export const getConnectionsWithTiming = async (input, seedAdapters) => {
|
|
|
159
159
|
pg,
|
|
160
160
|
db,
|
|
161
161
|
teardown,
|
|
162
|
-
query
|
|
162
|
+
query,
|
|
163
163
|
};
|
|
164
164
|
};
|
package/esm/graphile-test.js
CHANGED
|
@@ -1,42 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeSchema } from 'graphile-build';
|
|
2
|
+
import { defaultPreset as graphileBuildDefaultPreset } from 'graphile-build';
|
|
3
|
+
import { defaultPreset as graphileBuildPgDefaultPreset } from 'graphile-build-pg';
|
|
4
|
+
import { makePgService } from 'postgraphile/adaptors/pg';
|
|
2
5
|
import { runGraphQLInContext } from './context';
|
|
6
|
+
/**
|
|
7
|
+
* Minimal preset that provides core functionality without Node/Relay.
|
|
8
|
+
* This matches the pattern from graphile-settings.
|
|
9
|
+
*/
|
|
10
|
+
const MinimalPreset = {
|
|
11
|
+
extends: [graphileBuildDefaultPreset, graphileBuildPgDefaultPreset],
|
|
12
|
+
disablePlugins: ['NodePlugin'],
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Creates a GraphQL test context using PostGraphile v5 preset-based API.
|
|
16
|
+
*
|
|
17
|
+
* @param input - Configuration including schemas and optional preset
|
|
18
|
+
* @param conn - Database connection result from pgsql-test
|
|
19
|
+
* @returns GraphQL test context with setup, teardown, and query functions
|
|
20
|
+
*/
|
|
3
21
|
export const GraphQLTest = (input, conn) => {
|
|
4
|
-
const { schemas, authRole,
|
|
22
|
+
const { schemas, authRole, preset: userPreset } = input;
|
|
5
23
|
let schema;
|
|
6
|
-
let
|
|
24
|
+
let resolvedPreset;
|
|
25
|
+
let pgService;
|
|
7
26
|
const pgPool = conn.manager.getPool(conn.pg.config);
|
|
8
27
|
const setup = async () => {
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
...(
|
|
28
|
+
// Create the pgService - this will be used for withPgClient
|
|
29
|
+
pgService = makePgService({
|
|
30
|
+
pool: pgPool,
|
|
31
|
+
schemas,
|
|
32
|
+
});
|
|
33
|
+
// Build the complete preset by extending the minimal preset
|
|
34
|
+
// with user-provided preset configuration
|
|
35
|
+
const completePreset = {
|
|
36
|
+
extends: [
|
|
37
|
+
MinimalPreset,
|
|
38
|
+
...(userPreset?.extends ?? []),
|
|
39
|
+
],
|
|
40
|
+
...(userPreset?.disablePlugins && { disablePlugins: userPreset.disablePlugins }),
|
|
41
|
+
...(userPreset?.plugins && { plugins: userPreset.plugins }),
|
|
42
|
+
...(userPreset?.schema && { schema: userPreset.schema }),
|
|
43
|
+
...(userPreset?.grafast && { grafast: userPreset.grafast }),
|
|
44
|
+
pgServices: [pgService],
|
|
22
45
|
};
|
|
23
|
-
|
|
46
|
+
// Use makeSchema from graphile-build to create the schema
|
|
47
|
+
const result = await makeSchema(completePreset);
|
|
48
|
+
schema = result.schema;
|
|
49
|
+
resolvedPreset = result.resolvedPreset;
|
|
50
|
+
};
|
|
51
|
+
const teardown = async () => {
|
|
52
|
+
// Optional cleanup - schema is garbage collected
|
|
24
53
|
};
|
|
25
|
-
const teardown = async () => { };
|
|
26
54
|
const query = async (opts) => {
|
|
27
55
|
return await runGraphQLInContext({
|
|
28
56
|
input,
|
|
29
57
|
schema,
|
|
30
|
-
|
|
31
|
-
authRole,
|
|
58
|
+
resolvedPreset,
|
|
59
|
+
authRole: authRole ?? 'anonymous',
|
|
32
60
|
pgPool,
|
|
61
|
+
pgService,
|
|
33
62
|
conn,
|
|
34
|
-
|
|
63
|
+
query: opts.query,
|
|
64
|
+
variables: opts.variables,
|
|
65
|
+
reqOptions: opts.reqOptions,
|
|
35
66
|
});
|
|
36
67
|
};
|
|
37
68
|
return {
|
|
38
69
|
setup,
|
|
39
70
|
teardown,
|
|
40
|
-
query
|
|
71
|
+
query,
|
|
41
72
|
};
|
|
42
73
|
};
|
package/esm/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export * from './types';
|
|
1
|
+
export { runGraphQLInContext, setContextOnClient } from './context';
|
|
2
|
+
export { getConnections, getConnectionsObject, getConnectionsObjectUnwrapped, getConnectionsObjectWithLogging, getConnectionsObjectWithTiming, getConnectionsUnwrapped, getConnectionsWithLogging, getConnectionsWithTiming, } from './get-connections';
|
|
3
|
+
export { GraphQLTest } from './graphile-test';
|
|
5
4
|
export { seed, snapshot } from 'pgsql-test';
|
package/get-connections.js
CHANGED
|
@@ -31,7 +31,7 @@ const createConnectionsBase = async (input, seedAdapters) => {
|
|
|
31
31
|
teardown,
|
|
32
32
|
baseQuery,
|
|
33
33
|
baseQueryPositional,
|
|
34
|
-
gqlContext
|
|
34
|
+
gqlContext,
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
// ============================================================================
|
|
@@ -47,7 +47,7 @@ const getConnectionsObject = async (input, seedAdapters) => {
|
|
|
47
47
|
db,
|
|
48
48
|
teardown,
|
|
49
49
|
query: baseQuery,
|
|
50
|
-
gqlContext
|
|
50
|
+
gqlContext,
|
|
51
51
|
};
|
|
52
52
|
};
|
|
53
53
|
exports.getConnectionsObject = getConnectionsObject;
|
|
@@ -61,7 +61,7 @@ const getConnectionsObjectUnwrapped = async (input, seedAdapters) => {
|
|
|
61
61
|
pg,
|
|
62
62
|
db,
|
|
63
63
|
teardown,
|
|
64
|
-
query
|
|
64
|
+
query,
|
|
65
65
|
};
|
|
66
66
|
};
|
|
67
67
|
exports.getConnectionsObjectUnwrapped = getConnectionsObjectUnwrapped;
|
|
@@ -80,7 +80,7 @@ const getConnectionsObjectWithLogging = async (input, seedAdapters) => {
|
|
|
80
80
|
pg,
|
|
81
81
|
db,
|
|
82
82
|
teardown,
|
|
83
|
-
query
|
|
83
|
+
query,
|
|
84
84
|
};
|
|
85
85
|
};
|
|
86
86
|
exports.getConnectionsObjectWithLogging = getConnectionsObjectWithLogging;
|
|
@@ -100,7 +100,7 @@ const getConnectionsObjectWithTiming = async (input, seedAdapters) => {
|
|
|
100
100
|
pg,
|
|
101
101
|
db,
|
|
102
102
|
teardown,
|
|
103
|
-
query
|
|
103
|
+
query,
|
|
104
104
|
};
|
|
105
105
|
};
|
|
106
106
|
exports.getConnectionsObjectWithTiming = getConnectionsObjectWithTiming;
|
|
@@ -116,7 +116,7 @@ const getConnections = async (input, seedAdapters) => {
|
|
|
116
116
|
pg,
|
|
117
117
|
db,
|
|
118
118
|
teardown,
|
|
119
|
-
query: baseQueryPositional
|
|
119
|
+
query: baseQueryPositional,
|
|
120
120
|
};
|
|
121
121
|
};
|
|
122
122
|
exports.getConnections = getConnections;
|
|
@@ -125,12 +125,12 @@ exports.getConnections = getConnections;
|
|
|
125
125
|
*/
|
|
126
126
|
const getConnectionsUnwrapped = async (input, seedAdapters) => {
|
|
127
127
|
const { pg, db, teardown, baseQueryPositional } = await createConnectionsBase(input, seedAdapters);
|
|
128
|
-
const query = async (
|
|
128
|
+
const query = async (queryDoc, variables, commit, reqOptions) => unwrap(await baseQueryPositional(queryDoc, variables, commit, reqOptions));
|
|
129
129
|
return {
|
|
130
130
|
pg,
|
|
131
131
|
db,
|
|
132
132
|
teardown,
|
|
133
|
-
query
|
|
133
|
+
query,
|
|
134
134
|
};
|
|
135
135
|
};
|
|
136
136
|
exports.getConnectionsUnwrapped = getConnectionsUnwrapped;
|
|
@@ -139,9 +139,9 @@ exports.getConnectionsUnwrapped = getConnectionsUnwrapped;
|
|
|
139
139
|
*/
|
|
140
140
|
const getConnectionsWithLogging = async (input, seedAdapters) => {
|
|
141
141
|
const { pg, db, teardown, baseQueryPositional } = await createConnectionsBase(input, seedAdapters);
|
|
142
|
-
const query = async (
|
|
143
|
-
console.log('Executing positional GraphQL query:',
|
|
144
|
-
const result = await baseQueryPositional(
|
|
142
|
+
const query = async (queryDoc, variables, commit, reqOptions) => {
|
|
143
|
+
console.log('Executing positional GraphQL query:', queryDoc);
|
|
144
|
+
const result = await baseQueryPositional(queryDoc, variables, commit, reqOptions);
|
|
145
145
|
console.log('GraphQL result:', result);
|
|
146
146
|
return result;
|
|
147
147
|
};
|
|
@@ -149,7 +149,7 @@ const getConnectionsWithLogging = async (input, seedAdapters) => {
|
|
|
149
149
|
pg,
|
|
150
150
|
db,
|
|
151
151
|
teardown,
|
|
152
|
-
query
|
|
152
|
+
query,
|
|
153
153
|
};
|
|
154
154
|
};
|
|
155
155
|
exports.getConnectionsWithLogging = getConnectionsWithLogging;
|
|
@@ -158,9 +158,9 @@ exports.getConnectionsWithLogging = getConnectionsWithLogging;
|
|
|
158
158
|
*/
|
|
159
159
|
const getConnectionsWithTiming = async (input, seedAdapters) => {
|
|
160
160
|
const { pg, db, teardown, baseQueryPositional } = await createConnectionsBase(input, seedAdapters);
|
|
161
|
-
const query = async (
|
|
161
|
+
const query = async (queryDoc, variables, commit, reqOptions) => {
|
|
162
162
|
const start = Date.now();
|
|
163
|
-
const result = await baseQueryPositional(
|
|
163
|
+
const result = await baseQueryPositional(queryDoc, variables, commit, reqOptions);
|
|
164
164
|
const duration = Date.now() - start;
|
|
165
165
|
console.log(`Positional GraphQL query took ${duration}ms`);
|
|
166
166
|
return result;
|
|
@@ -169,7 +169,7 @@ const getConnectionsWithTiming = async (input, seedAdapters) => {
|
|
|
169
169
|
pg,
|
|
170
170
|
db,
|
|
171
171
|
teardown,
|
|
172
|
-
query
|
|
172
|
+
query,
|
|
173
173
|
};
|
|
174
174
|
};
|
|
175
175
|
exports.getConnectionsWithTiming = getConnectionsWithTiming;
|
package/graphile-test.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { GetConnectionOpts, GetConnectionResult } from 'pgsql-test';
|
|
2
|
-
import type { GraphQLTestContext } from './types';
|
|
3
|
-
|
|
1
|
+
import type { GetConnectionOpts, GetConnectionResult } from 'pgsql-test';
|
|
2
|
+
import type { GraphQLTestContext, GetConnectionsInput } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a GraphQL test context using PostGraphile v5 preset-based API.
|
|
5
|
+
*
|
|
6
|
+
* @param input - Configuration including schemas and optional preset
|
|
7
|
+
* @param conn - Database connection result from pgsql-test
|
|
8
|
+
* @returns GraphQL test context with setup, teardown, and query functions
|
|
9
|
+
*/
|
|
4
10
|
export declare const GraphQLTest: (input: GetConnectionsInput & GetConnectionOpts, conn: GetConnectionResult) => GraphQLTestContext;
|
package/graphile-test.js
CHANGED
|
@@ -1,46 +1,77 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GraphQLTest = void 0;
|
|
4
|
-
const
|
|
4
|
+
const graphile_build_1 = require("graphile-build");
|
|
5
|
+
const graphile_build_2 = require("graphile-build");
|
|
6
|
+
const graphile_build_pg_1 = require("graphile-build-pg");
|
|
7
|
+
const pg_1 = require("postgraphile/adaptors/pg");
|
|
5
8
|
const context_1 = require("./context");
|
|
9
|
+
/**
|
|
10
|
+
* Minimal preset that provides core functionality without Node/Relay.
|
|
11
|
+
* This matches the pattern from graphile-settings.
|
|
12
|
+
*/
|
|
13
|
+
const MinimalPreset = {
|
|
14
|
+
extends: [graphile_build_2.defaultPreset, graphile_build_pg_1.defaultPreset],
|
|
15
|
+
disablePlugins: ['NodePlugin'],
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Creates a GraphQL test context using PostGraphile v5 preset-based API.
|
|
19
|
+
*
|
|
20
|
+
* @param input - Configuration including schemas and optional preset
|
|
21
|
+
* @param conn - Database connection result from pgsql-test
|
|
22
|
+
* @returns GraphQL test context with setup, teardown, and query functions
|
|
23
|
+
*/
|
|
6
24
|
const GraphQLTest = (input, conn) => {
|
|
7
|
-
const { schemas, authRole,
|
|
25
|
+
const { schemas, authRole, preset: userPreset } = input;
|
|
8
26
|
let schema;
|
|
9
|
-
let
|
|
27
|
+
let resolvedPreset;
|
|
28
|
+
let pgService;
|
|
10
29
|
const pgPool = conn.manager.getPool(conn.pg.config);
|
|
11
30
|
const setup = async () => {
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
...(
|
|
31
|
+
// Create the pgService - this will be used for withPgClient
|
|
32
|
+
pgService = (0, pg_1.makePgService)({
|
|
33
|
+
pool: pgPool,
|
|
34
|
+
schemas,
|
|
35
|
+
});
|
|
36
|
+
// Build the complete preset by extending the minimal preset
|
|
37
|
+
// with user-provided preset configuration
|
|
38
|
+
const completePreset = {
|
|
39
|
+
extends: [
|
|
40
|
+
MinimalPreset,
|
|
41
|
+
...(userPreset?.extends ?? []),
|
|
42
|
+
],
|
|
43
|
+
...(userPreset?.disablePlugins && { disablePlugins: userPreset.disablePlugins }),
|
|
44
|
+
...(userPreset?.plugins && { plugins: userPreset.plugins }),
|
|
45
|
+
...(userPreset?.schema && { schema: userPreset.schema }),
|
|
46
|
+
...(userPreset?.grafast && { grafast: userPreset.grafast }),
|
|
47
|
+
pgServices: [pgService],
|
|
25
48
|
};
|
|
26
|
-
|
|
49
|
+
// Use makeSchema from graphile-build to create the schema
|
|
50
|
+
const result = await (0, graphile_build_1.makeSchema)(completePreset);
|
|
51
|
+
schema = result.schema;
|
|
52
|
+
resolvedPreset = result.resolvedPreset;
|
|
53
|
+
};
|
|
54
|
+
const teardown = async () => {
|
|
55
|
+
// Optional cleanup - schema is garbage collected
|
|
27
56
|
};
|
|
28
|
-
const teardown = async () => { };
|
|
29
57
|
const query = async (opts) => {
|
|
30
58
|
return await (0, context_1.runGraphQLInContext)({
|
|
31
59
|
input,
|
|
32
60
|
schema,
|
|
33
|
-
|
|
34
|
-
authRole,
|
|
61
|
+
resolvedPreset,
|
|
62
|
+
authRole: authRole ?? 'anonymous',
|
|
35
63
|
pgPool,
|
|
64
|
+
pgService,
|
|
36
65
|
conn,
|
|
37
|
-
|
|
66
|
+
query: opts.query,
|
|
67
|
+
variables: opts.variables,
|
|
68
|
+
reqOptions: opts.reqOptions,
|
|
38
69
|
});
|
|
39
70
|
};
|
|
40
71
|
return {
|
|
41
72
|
setup,
|
|
42
73
|
teardown,
|
|
43
|
-
query
|
|
74
|
+
query,
|
|
44
75
|
};
|
|
45
76
|
};
|
|
46
77
|
exports.GraphQLTest = GraphQLTest;
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
1
|
+
export { runGraphQLInContext, setContextOnClient } from './context';
|
|
2
|
+
export { getConnections, getConnectionsObject, getConnectionsObjectUnwrapped, getConnectionsObjectWithLogging, getConnectionsObjectWithTiming, getConnectionsUnwrapped, getConnectionsWithLogging, getConnectionsWithTiming, } from './get-connections';
|
|
3
|
+
export { GraphQLTest } from './graphile-test';
|
|
4
|
+
export type { GetConnectionsInput, GraphQLQueryFn, GraphQLQueryFnObj, GraphQLQueryOptions, GraphQLQueryUnwrappedFn, GraphQLQueryUnwrappedFnObj, GraphQLResponse, GraphQLTestContext, LegacyGraphileOptions, Variables, } from './types';
|
|
5
5
|
export { seed, snapshot } from 'pgsql-test';
|
package/index.js
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.snapshot = exports.seed = void 0;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
exports.snapshot = exports.seed = exports.GraphQLTest = exports.getConnectionsWithTiming = exports.getConnectionsWithLogging = exports.getConnectionsUnwrapped = exports.getConnectionsObjectWithTiming = exports.getConnectionsObjectWithLogging = exports.getConnectionsObjectUnwrapped = exports.getConnectionsObject = exports.getConnections = exports.setContextOnClient = exports.runGraphQLInContext = void 0;
|
|
4
|
+
var context_1 = require("./context");
|
|
5
|
+
Object.defineProperty(exports, "runGraphQLInContext", { enumerable: true, get: function () { return context_1.runGraphQLInContext; } });
|
|
6
|
+
Object.defineProperty(exports, "setContextOnClient", { enumerable: true, get: function () { return context_1.setContextOnClient; } });
|
|
7
|
+
var get_connections_1 = require("./get-connections");
|
|
8
|
+
Object.defineProperty(exports, "getConnections", { enumerable: true, get: function () { return get_connections_1.getConnections; } });
|
|
9
|
+
Object.defineProperty(exports, "getConnectionsObject", { enumerable: true, get: function () { return get_connections_1.getConnectionsObject; } });
|
|
10
|
+
Object.defineProperty(exports, "getConnectionsObjectUnwrapped", { enumerable: true, get: function () { return get_connections_1.getConnectionsObjectUnwrapped; } });
|
|
11
|
+
Object.defineProperty(exports, "getConnectionsObjectWithLogging", { enumerable: true, get: function () { return get_connections_1.getConnectionsObjectWithLogging; } });
|
|
12
|
+
Object.defineProperty(exports, "getConnectionsObjectWithTiming", { enumerable: true, get: function () { return get_connections_1.getConnectionsObjectWithTiming; } });
|
|
13
|
+
Object.defineProperty(exports, "getConnectionsUnwrapped", { enumerable: true, get: function () { return get_connections_1.getConnectionsUnwrapped; } });
|
|
14
|
+
Object.defineProperty(exports, "getConnectionsWithLogging", { enumerable: true, get: function () { return get_connections_1.getConnectionsWithLogging; } });
|
|
15
|
+
Object.defineProperty(exports, "getConnectionsWithTiming", { enumerable: true, get: function () { return get_connections_1.getConnectionsWithTiming; } });
|
|
16
|
+
var graphile_test_1 = require("./graphile-test");
|
|
17
|
+
Object.defineProperty(exports, "GraphQLTest", { enumerable: true, get: function () { return graphile_test_1.GraphQLTest; } });
|
|
22
18
|
var pgsql_test_1 = require("pgsql-test");
|
|
23
19
|
Object.defineProperty(exports, "seed", { enumerable: true, get: function () { return pgsql_test_1.seed; } });
|
|
24
20
|
Object.defineProperty(exports, "snapshot", { enumerable: true, get: function () { return pgsql_test_1.snapshot; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-test",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PostGraphile Testing",
|
|
6
6
|
"main": "index.js",
|
|
@@ -31,17 +31,21 @@
|
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/pg": "^8.16.0",
|
|
33
33
|
"graphql-tag": "2.12.6",
|
|
34
|
-
"makage": "^0.1.
|
|
34
|
+
"makage": "^0.1.10"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@constructive-io/graphql-env": "^
|
|
38
|
-
"@constructive-io/graphql-types": "^
|
|
37
|
+
"@constructive-io/graphql-env": "^3.0.0",
|
|
38
|
+
"@constructive-io/graphql-types": "^3.0.0",
|
|
39
39
|
"@pgpmjs/types": "^2.16.0",
|
|
40
|
-
"
|
|
40
|
+
"grafast": "^1.0.0-rc.4",
|
|
41
|
+
"graphile-build": "^5.0.0-rc.3",
|
|
42
|
+
"graphile-build-pg": "^5.0.0-rc.3",
|
|
43
|
+
"graphile-config": "1.0.0-rc.3",
|
|
44
|
+
"graphql": "^16.9.0",
|
|
41
45
|
"mock-req": "^0.2.0",
|
|
42
46
|
"pg": "^8.17.1",
|
|
43
|
-
"pgsql-test": "^
|
|
44
|
-
"postgraphile": "^
|
|
47
|
+
"pgsql-test": "^4.0.0",
|
|
48
|
+
"postgraphile": "^5.0.0-rc.4"
|
|
45
49
|
},
|
|
46
50
|
"keywords": [
|
|
47
51
|
"testing",
|
|
@@ -51,5 +55,5 @@
|
|
|
51
55
|
"pgpm",
|
|
52
56
|
"test"
|
|
53
57
|
],
|
|
54
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "b2daeefe49cdefb3d01ea63cf778fb9b847ab5fe"
|
|
55
59
|
}
|
package/types.d.ts
CHANGED
|
@@ -1,33 +1,81 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { DocumentNode, GraphQLError } from 'graphql';
|
|
3
|
-
|
|
1
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
2
|
+
import type { DocumentNode, GraphQLError } from 'graphql';
|
|
3
|
+
/**
|
|
4
|
+
* Variables type that accepts plain objects and interfaces without requiring
|
|
5
|
+
* an explicit index signature. Using `Record<string, any>` allows TypeScript
|
|
6
|
+
* to accept typed interfaces like { username: string } since `any` is bi-directionally
|
|
7
|
+
* assignable to all types.
|
|
8
|
+
*/
|
|
9
|
+
export type Variables = Record<string, any>;
|
|
10
|
+
export interface GraphQLQueryOptions<TVariables extends Variables = Variables> {
|
|
4
11
|
query: string | DocumentNode;
|
|
5
12
|
variables?: TVariables;
|
|
6
13
|
commit?: boolean;
|
|
7
|
-
reqOptions?: Record<string,
|
|
14
|
+
reqOptions?: Record<string, unknown>;
|
|
8
15
|
}
|
|
9
16
|
export interface GraphQLTestContext {
|
|
10
17
|
setup: () => Promise<void>;
|
|
11
18
|
teardown: () => Promise<void>;
|
|
12
|
-
query: <TResult =
|
|
19
|
+
query: <TResult = unknown, TVariables extends Variables = Variables>(opts: GraphQLQueryOptions<TVariables>) => Promise<TResult>;
|
|
13
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Legacy v4-style GraphQL options for backward compatibility.
|
|
23
|
+
*
|
|
24
|
+
* @deprecated Use `preset` instead for v5 configuration.
|
|
25
|
+
*/
|
|
26
|
+
export interface LegacyGraphileOptions {
|
|
27
|
+
/**
|
|
28
|
+
* V4-style plugins to append.
|
|
29
|
+
* These plugins use the builder.hook() API which is NOT compatible with v5.
|
|
30
|
+
* For v5, convert these to proper v5 plugins and use the `preset` option instead.
|
|
31
|
+
*
|
|
32
|
+
* @deprecated Use preset.plugins for v5 plugins
|
|
33
|
+
*/
|
|
34
|
+
appendPlugins?: any[];
|
|
35
|
+
/**
|
|
36
|
+
* V4-style graphile build options.
|
|
37
|
+
*
|
|
38
|
+
* @deprecated Use preset.schema for v5 schema options
|
|
39
|
+
*/
|
|
40
|
+
graphileBuildOptions?: Record<string, any>;
|
|
41
|
+
/**
|
|
42
|
+
* V4-style PostGraphile options override.
|
|
43
|
+
*
|
|
44
|
+
* @deprecated Use preset for v5 configuration
|
|
45
|
+
*/
|
|
46
|
+
overrideSettings?: Record<string, any>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Input for GraphQL test connections.
|
|
50
|
+
*
|
|
51
|
+
* Supports both v5 preset-based configuration (recommended) and
|
|
52
|
+
* legacy v4-style configuration (deprecated, for backward compatibility).
|
|
53
|
+
*/
|
|
14
54
|
export interface GetConnectionsInput {
|
|
15
55
|
useRoot?: boolean;
|
|
16
56
|
schemas: string[];
|
|
17
57
|
authRole?: string;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
58
|
+
/**
|
|
59
|
+
* V5 preset configuration (recommended).
|
|
60
|
+
* Can include extends, plugins, schema options, etc.
|
|
61
|
+
*/
|
|
62
|
+
preset?: GraphileConfig.Preset;
|
|
63
|
+
/**
|
|
64
|
+
* Legacy v4-style graphile options for backward compatibility.
|
|
65
|
+
*
|
|
66
|
+
* NOTE: v4-style plugins (using builder.hook()) are NOT compatible with v5.
|
|
67
|
+
* If you use appendPlugins with v4 plugins, they will be ignored.
|
|
68
|
+
* Convert your plugins to v5 format and use the `preset` option instead.
|
|
69
|
+
*
|
|
70
|
+
* @deprecated Use preset for v5 configuration
|
|
71
|
+
*/
|
|
72
|
+
graphile?: LegacyGraphileOptions;
|
|
25
73
|
}
|
|
26
74
|
export interface GraphQLResponse<T> {
|
|
27
75
|
data?: T;
|
|
28
76
|
errors?: readonly GraphQLError[];
|
|
29
77
|
}
|
|
30
|
-
export type GraphQLQueryFnObj = <TResult =
|
|
31
|
-
export type GraphQLQueryFn = <TResult =
|
|
32
|
-
export type GraphQLQueryUnwrappedFnObj = <TResult =
|
|
33
|
-
export type GraphQLQueryUnwrappedFn = <TResult =
|
|
78
|
+
export type GraphQLQueryFnObj = <TResult = unknown, TVariables extends Variables = Variables>(opts: GraphQLQueryOptions<TVariables>) => Promise<GraphQLResponse<TResult>>;
|
|
79
|
+
export type GraphQLQueryFn = <TResult = unknown, TVariables extends Variables = Variables>(query: string | DocumentNode, variables?: TVariables, commit?: boolean, reqOptions?: Record<string, unknown>) => Promise<GraphQLResponse<TResult>>;
|
|
80
|
+
export type GraphQLQueryUnwrappedFnObj = <TResult = unknown, TVariables extends Variables = Variables>(opts: GraphQLQueryOptions<TVariables>) => Promise<TResult>;
|
|
81
|
+
export type GraphQLQueryUnwrappedFn = <TResult = unknown, TVariables extends Variables = Variables>(query: string | DocumentNode, variables?: TVariables, commit?: boolean, reqOptions?: Record<string, unknown>) => Promise<TResult>;
|