Skip to content

Commit 0556fca

Browse files
authored
Merge pull request #6740 from luffy1727/adding-transaction-timeout-option
Add ability to specify transaction config in Neo4jGraphQLContext
2 parents 6cc5a42 + 2562943 commit 0556fca

File tree

5 files changed

+246
-8
lines changed

5 files changed

+246
-8
lines changed

.changeset/slow-plants-beam.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@neo4j/graphql": minor
3+
---
4+
5+
Add the ability to specify transaction configuration. e.g. timeout
6+
7+
```js
8+
const transactionConfig = {
9+
timeout: 60 * 1000,
10+
metadata: {
11+
"my-very-own-metadata": "is very good!"
12+
}
13+
};
14+
15+
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
16+
17+
const server = new ApolloServer({
18+
schema: await neoSchema.getSchema(),
19+
});
20+
21+
await startStandaloneServer(server, {
22+
context: async ({ req }) => ({ req, transaction: transactionConfig }),
23+
});
24+
```

packages/graphql/src/classes/Executor.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Debug from "debug";
2121
import type { GraphQLResolveInfo } from "graphql";
2222
import type {
2323
Driver,
24+
Integer,
2425
ManagedTransaction,
2526
QueryResult,
2627
Result,
@@ -73,6 +74,7 @@ type TransactionConfig = {
7374
// Possible values from https://neo4j.com/docs/operations-manual/current/monitoring/logging/#attach-metadata-tx (will only be user-transpiled for @neo4j/graphql)
7475
type: "system" | "user-direct" | "user-action" | "user-transpiled";
7576
};
77+
timeout?: number | Integer | undefined;
7678
};
7779

7880
export type ExecutionContext = Driver | Session | Transaction;
@@ -82,9 +84,14 @@ export type ExecutorConstructorParam = {
8284
cypherQueryOptions?: CypherQueryOptions;
8385
sessionConfig?: SessionConfig;
8486
cypherParams?: Record<string, unknown>;
85-
transactionMetadata?: Record<string, unknown>;
87+
transaction?: UserTransactionConfig;
8688
};
8789

90+
export type UserTransactionConfig = {
91+
timeout?: number | Integer | undefined;
92+
metadata?: Record<string, unknown> | undefined;
93+
}
94+
8895
export type Neo4jGraphQLSessionConfig = Pick<SessionConfig, "database" | "impersonatedUser" | "auth">;
8996

9097
export class Executor {
@@ -95,20 +102,23 @@ export class Executor {
95102
private sessionConfig: SessionConfig | undefined;
96103

97104
private cypherParams: Record<string, unknown>;
98-
private transactionMetadata: Record<string, unknown>;
105+
private transaction: UserTransactionConfig;
99106

100107
constructor({
101108
executionContext,
102109
cypherQueryOptions,
103110
sessionConfig,
104111
cypherParams = {},
105-
transactionMetadata = {},
112+
transaction = {
113+
timeout: undefined,
114+
metadata: undefined,
115+
},
106116
}: ExecutorConstructorParam) {
107117
this.executionContext = executionContext;
108118
this.cypherQueryOptions = cypherQueryOptions;
109119
this.sessionConfig = sessionConfig;
110120
this.cypherParams = cypherParams;
111-
this.transactionMetadata = transactionMetadata;
121+
this.transaction = transaction;
112122
}
113123

114124
public async execute(
@@ -118,7 +128,6 @@ export class Executor {
118128
info?: GraphQLResolveInfo
119129
): Promise<QueryResult> {
120130
const params = { ...parameters, ...this.cypherParams };
121-
122131
try {
123132
if (isDriverLike(this.executionContext)) {
124133
return await this.driverRun({
@@ -205,9 +214,10 @@ export class Executor {
205214
return {
206215
metadata: {
207216
app: APP_ID,
208-
...this.transactionMetadata,
217+
...this.transaction.metadata,
209218
type: "user-transpiled",
210219
},
220+
timeout: this.transaction.timeout,
211221
};
212222
}
213223

packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ export const wrapQueryAndMutation =
104104
cypherQueryOptions: context.cypherQueryOptions,
105105
sessionConfig: context.sessionConfig,
106106
cypherParams: context.cypherParams,
107-
transactionMetadata: context.transactionMetadata,
107+
transaction: {
108+
...context.transaction,
109+
metadata: {...context.transaction?.metadata, ...context.transactionMetadata},
110+
},
108111
});
109112

110113
if (dbInfo) {

packages/graphql/src/types/neo4j-graphql-context.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
* limitations under the License.
1818
*/
1919

20+
import { Integer } from "neo4j-driver";
2021
import type { CypherQueryOptions } from ".";
21-
import type { ExecutionContext, Neo4jGraphQLSessionConfig } from "../classes/Executor";
22+
import type { ExecutionContext, Neo4jGraphQLSessionConfig, UserTransactionConfig } from "../classes/Executor";
2223
import type { Neo4jGraphQLContextInterface } from "./neo4j-graphql-context-interface";
2324

2425
export interface Neo4jGraphQLContext extends Neo4jGraphQLContextInterface {
@@ -51,6 +52,15 @@ export interface Neo4jGraphQLContext extends Neo4jGraphQLContextInterface {
5152
* Attach metadata to the database transaction.
5253
* This can be used to output information to the query log not related to the query itself.
5354
* Will be ignored if {@link executionContext} is an instance of a transaction.
55+
*
56+
* @deprecated This method will be removed in a future version. Please, use {@link transaction} instead.
57+
*
58+
* @see {@link #transaction}
5459
*/
5560
transactionMetadata?: Record<string, unknown>;
61+
62+
/**
63+
* User transaction object which has both the metadata object and the timeout period inside
64+
*/
65+
transaction?: UserTransactionConfig;
5666
}

packages/graphql/src/utils/execute.test.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import { trimmer } from ".";
2222
import { ContextBuilder } from "../../tests/utils/builders/context-builder";
2323
import { Executor } from "../classes/Executor";
2424
import execute from "./execute";
25+
import {
26+
APP_ID,
27+
} from "../constants";
2528

2629
describe("execute", () => {
2730
test("should execute return records.toObject", async () => {
@@ -271,4 +274,192 @@ describe("execute", () => {
271274
expect(executeResult.records).toEqual([{ title }]);
272275
});
273276
});
277+
278+
describe("should set transaction config", () => {
279+
test("user described transaction config", async () => {
280+
const defaultAccessMode = "READ";
281+
282+
const cypher = trimmer(`
283+
CREATE (u:User {title: $title})
284+
RETURN u { .title } as u
285+
`);
286+
287+
const title = "some title";
288+
const params = { title };
289+
const records = [{ toObject: () => ({ title }) }];
290+
const database = "neo4j";
291+
const bookmarks = ["test"];
292+
let transactionConfig: any; // transaction config that is used in the actual driver class.
293+
294+
// @ts-ignore
295+
const driver: Driver = {
296+
// @ts-ignore
297+
session: (options) => {
298+
expect(options).toMatchObject({ defaultAccessMode, database, bookmarks });
299+
300+
const tx = {
301+
run: (paramCypher: string, paramParams) => {
302+
expect(trimmer(paramCypher)).toBe(`CYPHER 5 ${cypher}`);
303+
expect(paramParams).toEqual(params);
304+
305+
return { records, summary: { counters: { updates: () => ({ test: 1 }) } } };
306+
},
307+
commit: () => true,
308+
};
309+
310+
return {
311+
beginTransaction: () => tx,
312+
readTransaction: (fn, config) => {
313+
transactionConfig = config;
314+
// @ts-ignore
315+
return fn(tx);
316+
},
317+
writeTransaction: (fn, config) => {
318+
transactionConfig = config;
319+
// @ts-ignore
320+
return fn(tx);
321+
},
322+
executeRead: (fn, config) => {
323+
transactionConfig = config;
324+
// @ts-ignore
325+
return fn(tx);
326+
},
327+
executeWrite: (fn, config) => {
328+
transactionConfig = config;
329+
// @ts-ignore
330+
return fn(tx);
331+
},
332+
close: () => true,
333+
lastBookmark: () => [],
334+
lastBookmarks: () => [],
335+
};
336+
},
337+
// @ts-ignore
338+
_config: {},
339+
};
340+
341+
const executeResult = await execute({
342+
cypher,
343+
params,
344+
defaultAccessMode,
345+
context: new ContextBuilder({
346+
executor: new Executor({
347+
executionContext: driver,
348+
sessionConfig: {
349+
database,
350+
bookmarks,
351+
},
352+
transaction: {
353+
timeout: 100,
354+
metadata: {
355+
app: "my-app-name",
356+
type: "system",
357+
someOtherUserDefinedField: "test",
358+
},
359+
},
360+
cypherQueryOptions: {},
361+
}),
362+
}).instance(),
363+
});
364+
365+
expect(executeResult.records).toEqual([{ title }]);
366+
expect(transactionConfig).toEqual({
367+
timeout: 100,
368+
metadata: {
369+
app: "my-app-name",
370+
type: "user-transpiled",
371+
someOtherUserDefinedField: "test",
372+
},
373+
});
374+
});
375+
376+
test("empty transaction config", async () => {
377+
const defaultAccessMode = "READ";
378+
379+
const cypher = trimmer(`
380+
CREATE (u:User {title: $title})
381+
RETURN u { .title } as u
382+
`);
383+
384+
const title = "some title";
385+
const params = { title };
386+
const records = [{ toObject: () => ({ title }) }];
387+
const database = "neo4j";
388+
const bookmarks = ["test"];
389+
let transactionConfig: any; // transaction config that is used in the actual driver class.
390+
391+
// @ts-ignore
392+
const driver: Driver = {
393+
// @ts-ignore
394+
session: (options) => {
395+
expect(options).toMatchObject({ defaultAccessMode, database, bookmarks });
396+
397+
const tx = {
398+
run: (paramCypher: string, paramParams) => {
399+
expect(trimmer(paramCypher)).toBe(`CYPHER 5 ${cypher}`);
400+
expect(paramParams).toEqual(params);
401+
402+
return { records, summary: { counters: { updates: () => ({ test: 1 }) } } };
403+
},
404+
commit: () => true,
405+
};
406+
407+
return {
408+
beginTransaction: () => tx,
409+
readTransaction: (fn, config) => {
410+
transactionConfig = config;
411+
// @ts-ignore
412+
return fn(tx);
413+
},
414+
writeTransaction: (fn, config) => {
415+
transactionConfig = config;
416+
// @ts-ignore
417+
return fn(tx);
418+
},
419+
executeRead: (fn, config) => {
420+
transactionConfig = config;
421+
// @ts-ignore
422+
return fn(tx);
423+
},
424+
executeWrite: (fn, config) => {
425+
transactionConfig = config;
426+
// @ts-ignore
427+
return fn(tx);
428+
},
429+
close: () => true,
430+
lastBookmark: () => [],
431+
lastBookmarks: () => [],
432+
};
433+
},
434+
// @ts-ignore
435+
_config: {},
436+
};
437+
438+
const executeResult = await execute({
439+
cypher,
440+
params,
441+
defaultAccessMode,
442+
context: new ContextBuilder({
443+
executor: new Executor({
444+
executionContext: driver,
445+
sessionConfig: {
446+
database,
447+
bookmarks,
448+
},
449+
cypherQueryOptions: {},
450+
}),
451+
}).instance(),
452+
});
453+
// console.log(executeResult)
454+
455+
expect(executeResult.records).toEqual([{ title }]);
456+
expect(transactionConfig).toEqual({
457+
timeout: undefined,
458+
metadata: {
459+
app: APP_ID,
460+
type: "user-transpiled",
461+
},
462+
});
463+
});
464+
});
274465
});

0 commit comments

Comments
 (0)