@nexus_js/graphql 0.9.3
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/dist/complexity.d.ts +75 -0
- package/dist/complexity.d.ts.map +1 -0
- package/dist/complexity.js +182 -0
- package/dist/complexity.js.map +1 -0
- package/dist/dataloader.d.ts +96 -0
- package/dist/dataloader.d.ts.map +1 -0
- package/dist/dataloader.js +177 -0
- package/dist/dataloader.js.map +1 -0
- package/dist/handler.d.ts +142 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +420 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +109 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +197 -0
- package/dist/jwt.js.map +1 -0
- package/dist/mask.d.ts +70 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +98 -0
- package/dist/mask.js.map +1 -0
- package/dist/remote-executor.d.ts +126 -0
- package/dist/remote-executor.d.ts.map +1 -0
- package/dist/remote-executor.js +270 -0
- package/dist/remote-executor.js.map +1 -0
- package/dist/stitching.d.ts +145 -0
- package/dist/stitching.d.ts.map +1 -0
- package/dist/stitching.js +111 -0
- package/dist/stitching.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus GraphQL Complexity Shield
|
|
3
|
+
*
|
|
4
|
+
* Analyses the parsed GraphQL AST *before* execution to prevent:
|
|
5
|
+
*
|
|
6
|
+
* 1. Deep-nesting attacks — a query like user { friends { friends { … } } }
|
|
7
|
+
* that reaches N levels can cause O(N) database round-trips.
|
|
8
|
+
*
|
|
9
|
+
* 2. Breadth/cost attacks — selecting 50 high-cost fields at once.
|
|
10
|
+
*
|
|
11
|
+
* 3. Introspection leaks — schema exposure in production.
|
|
12
|
+
*
|
|
13
|
+
* Algorithm
|
|
14
|
+
* ─────────
|
|
15
|
+
* For every FieldNode we add its `fieldCost` to the running total.
|
|
16
|
+
* If the field resolves to a list type we multiply children by `listMultiplier`
|
|
17
|
+
* (default 10) to model the N×M fan-out.
|
|
18
|
+
* Inline fragments and fragment spreads are expanded transparently.
|
|
19
|
+
* The visitor is purely synchronous (no I/O).
|
|
20
|
+
*
|
|
21
|
+
* Usage
|
|
22
|
+
* ─────
|
|
23
|
+
* import { analyseComplexity } from '@nexus_js/graphql';
|
|
24
|
+
*
|
|
25
|
+
* const { cost, depth, errors } = analyseComplexity(document, schema, {
|
|
26
|
+
* maxCost: 1000,
|
|
27
|
+
* maxDepth: 10,
|
|
28
|
+
* fieldCosts: { 'Query.analytics': 50, 'User.posts': 5 },
|
|
29
|
+
* });
|
|
30
|
+
* if (errors.length) return respondWithErrors(errors);
|
|
31
|
+
*/
|
|
32
|
+
import type { DocumentNode, GraphQLSchema } from 'graphql';
|
|
33
|
+
export interface ComplexityConfig {
|
|
34
|
+
/** Hard limit on total query cost. Default: 1000. */
|
|
35
|
+
maxCost?: number;
|
|
36
|
+
/** Hard limit on selection-set nesting depth. Default: 12. */
|
|
37
|
+
maxDepth?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Base cost multiplier for list fields (models N-row fan-out).
|
|
40
|
+
* Default: 10. Set to 1 to disable.
|
|
41
|
+
*/
|
|
42
|
+
listMultiplier?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Per-field cost overrides. Key: "TypeName.fieldName" (e.g. "Query.analytics").
|
|
45
|
+
* Default cost per field is 1.
|
|
46
|
+
*/
|
|
47
|
+
fieldCosts?: Record<string, number>;
|
|
48
|
+
/**
|
|
49
|
+
* Allow introspection queries (__schema, __type, __typename).
|
|
50
|
+
* Default: true (disable in production with false).
|
|
51
|
+
*/
|
|
52
|
+
allowIntrospection?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Per-operation-type limits override maxCost for mutations (which are often
|
|
55
|
+
* more expensive than queries). Set to a lower number to tighten mutations.
|
|
56
|
+
*/
|
|
57
|
+
mutationCostMultiplier?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface ComplexityResult {
|
|
60
|
+
cost: number;
|
|
61
|
+
depth: number;
|
|
62
|
+
/** Non-empty if any limit was exceeded. Pass these to GraphQL `errors` array. */
|
|
63
|
+
errors: Array<{
|
|
64
|
+
message: string;
|
|
65
|
+
extensions: {
|
|
66
|
+
code: string;
|
|
67
|
+
};
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Analyse a parsed GraphQL document against a schema.
|
|
72
|
+
* Returns cost + depth + validation errors without executing anything.
|
|
73
|
+
*/
|
|
74
|
+
export declare function analyseComplexity(document: DocumentNode, schema: GraphQLSchema, config?: ComplexityConfig): ComplexityResult;
|
|
75
|
+
//# sourceMappingURL=complexity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"complexity.d.ts","sourceRoot":"","sources":["../src/complexity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EACV,YAAY,EAKZ,aAAa,EAGd,MAAM,SAAS,CAAC;AAIjB,MAAM,WAAW,gBAAgB;IAC/B,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,MAAM,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CAClE;AAQD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,aAAa,EACrB,MAAM,GAAE,gBAAqB,GAC5B,gBAAgB,CA8DlB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus GraphQL Complexity Shield
|
|
3
|
+
*
|
|
4
|
+
* Analyses the parsed GraphQL AST *before* execution to prevent:
|
|
5
|
+
*
|
|
6
|
+
* 1. Deep-nesting attacks — a query like user { friends { friends { … } } }
|
|
7
|
+
* that reaches N levels can cause O(N) database round-trips.
|
|
8
|
+
*
|
|
9
|
+
* 2. Breadth/cost attacks — selecting 50 high-cost fields at once.
|
|
10
|
+
*
|
|
11
|
+
* 3. Introspection leaks — schema exposure in production.
|
|
12
|
+
*
|
|
13
|
+
* Algorithm
|
|
14
|
+
* ─────────
|
|
15
|
+
* For every FieldNode we add its `fieldCost` to the running total.
|
|
16
|
+
* If the field resolves to a list type we multiply children by `listMultiplier`
|
|
17
|
+
* (default 10) to model the N×M fan-out.
|
|
18
|
+
* Inline fragments and fragment spreads are expanded transparently.
|
|
19
|
+
* The visitor is purely synchronous (no I/O).
|
|
20
|
+
*
|
|
21
|
+
* Usage
|
|
22
|
+
* ─────
|
|
23
|
+
* import { analyseComplexity } from '@nexus_js/graphql';
|
|
24
|
+
*
|
|
25
|
+
* const { cost, depth, errors } = analyseComplexity(document, schema, {
|
|
26
|
+
* maxCost: 1000,
|
|
27
|
+
* maxDepth: 10,
|
|
28
|
+
* fieldCosts: { 'Query.analytics': 50, 'User.posts': 5 },
|
|
29
|
+
* });
|
|
30
|
+
* if (errors.length) return respondWithErrors(errors);
|
|
31
|
+
*/
|
|
32
|
+
// ── Introspection field names ────────────────────────────────────────────────
|
|
33
|
+
const INTROSPECTION_FIELDS = new Set(['__schema', '__type', '__typename', '__typeName']);
|
|
34
|
+
// ── Core visitor ────────────────────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* Analyse a parsed GraphQL document against a schema.
|
|
37
|
+
* Returns cost + depth + validation errors without executing anything.
|
|
38
|
+
*/
|
|
39
|
+
export function analyseComplexity(document, schema, config = {}) {
|
|
40
|
+
const { maxCost = 1000, maxDepth = 12, listMultiplier = 10, fieldCosts = {}, allowIntrospection = true, mutationCostMultiplier = 1, } = config;
|
|
41
|
+
const errors = [];
|
|
42
|
+
// Build named-fragment map for inline expansion
|
|
43
|
+
const fragments = new Map();
|
|
44
|
+
for (const def of document.definitions) {
|
|
45
|
+
if (def.kind === 'FragmentDefinition') {
|
|
46
|
+
fragments.set(def.name.value, def.selectionSet);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let globalMaxDepth = 0;
|
|
50
|
+
let totalCost = 0;
|
|
51
|
+
for (const def of document.definitions) {
|
|
52
|
+
if (def.kind !== 'OperationDefinition')
|
|
53
|
+
continue;
|
|
54
|
+
const isMutation = def.operation === 'mutation';
|
|
55
|
+
const costMul = isMutation ? mutationCostMultiplier : 1;
|
|
56
|
+
const rootType = getOperationRootType(schema, def.operation);
|
|
57
|
+
if (!rootType)
|
|
58
|
+
continue;
|
|
59
|
+
const { cost, depth } = visitSelectionSet(def.selectionSet, rootType, schema, fragments, fieldCosts, listMultiplier, 0, allowIntrospection, errors);
|
|
60
|
+
totalCost += cost * costMul;
|
|
61
|
+
if (depth > globalMaxDepth)
|
|
62
|
+
globalMaxDepth = depth;
|
|
63
|
+
}
|
|
64
|
+
if (totalCost > maxCost) {
|
|
65
|
+
errors.push({
|
|
66
|
+
message: `Query complexity ${totalCost} exceeds maximum allowed cost of ${maxCost}.`,
|
|
67
|
+
extensions: { code: 'COMPLEXITY_LIMIT_EXCEEDED' },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (globalMaxDepth > maxDepth) {
|
|
71
|
+
errors.push({
|
|
72
|
+
message: `Query depth ${globalMaxDepth} exceeds maximum allowed depth of ${maxDepth}.`,
|
|
73
|
+
extensions: { code: 'DEPTH_LIMIT_EXCEEDED' },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return { cost: totalCost, depth: globalMaxDepth, errors };
|
|
77
|
+
}
|
|
78
|
+
function visitSelectionSet(selectionSet, parentType, schema, fragments, fieldCosts, listMultiplier, currentDepth, allowIntrospection, errors) {
|
|
79
|
+
let cost = 0;
|
|
80
|
+
let depth = currentDepth;
|
|
81
|
+
for (const selection of selectionSet.selections) {
|
|
82
|
+
if (selection.kind === 'Field') {
|
|
83
|
+
const result = visitField(selection, parentType, schema, fragments, fieldCosts, listMultiplier, currentDepth, allowIntrospection, errors);
|
|
84
|
+
cost += result.cost;
|
|
85
|
+
if (result.depth > depth)
|
|
86
|
+
depth = result.depth;
|
|
87
|
+
}
|
|
88
|
+
else if (selection.kind === 'InlineFragment') {
|
|
89
|
+
const fragmentType = selection.typeCondition
|
|
90
|
+
? schema.getType(selection.typeCondition.name.value)
|
|
91
|
+
: parentType;
|
|
92
|
+
if (fragmentType && 'getFields' in fragmentType) {
|
|
93
|
+
const result = visitSelectionSet(selection.selectionSet, fragmentType, schema, fragments, fieldCosts, listMultiplier, currentDepth, allowIntrospection, errors);
|
|
94
|
+
cost += result.cost;
|
|
95
|
+
if (result.depth > depth)
|
|
96
|
+
depth = result.depth;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (selection.kind === 'FragmentSpread') {
|
|
100
|
+
const frag = fragments.get(selection.name.value);
|
|
101
|
+
if (frag) {
|
|
102
|
+
const result = visitSelectionSet(frag, parentType, schema, fragments, fieldCosts, listMultiplier, currentDepth, allowIntrospection, errors);
|
|
103
|
+
cost += result.cost;
|
|
104
|
+
if (result.depth > depth)
|
|
105
|
+
depth = result.depth;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { cost, depth };
|
|
110
|
+
}
|
|
111
|
+
function visitField(field, parentType, schema, fragments, fieldCosts, listMultiplier, currentDepth, allowIntrospection, errors) {
|
|
112
|
+
const fieldName = field.name.value;
|
|
113
|
+
// Introspection gate
|
|
114
|
+
if (INTROSPECTION_FIELDS.has(fieldName)) {
|
|
115
|
+
if (!allowIntrospection) {
|
|
116
|
+
errors.push({
|
|
117
|
+
message: `Introspection is disabled in this environment.`,
|
|
118
|
+
extensions: { code: 'INTROSPECTION_DISABLED' },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return { cost: 0, depth: currentDepth };
|
|
122
|
+
}
|
|
123
|
+
const typeName = parentType.name;
|
|
124
|
+
const costKey = `${typeName}.${fieldName}`;
|
|
125
|
+
const fieldCost = fieldCosts[costKey] ?? fieldCosts[fieldName] ?? 1;
|
|
126
|
+
const thisDepth = currentDepth + 1;
|
|
127
|
+
let cost = fieldCost;
|
|
128
|
+
let depth = thisDepth;
|
|
129
|
+
if (field.selectionSet) {
|
|
130
|
+
// Resolve the field's return type to determine multiplier
|
|
131
|
+
const fieldDef = parentType.getFields()[fieldName];
|
|
132
|
+
const isList = fieldDef ? typeIsList(fieldDef.type) : false;
|
|
133
|
+
const mul = isList ? listMultiplier : 1;
|
|
134
|
+
const childType = fieldDef
|
|
135
|
+
? namedObjectType(fieldDef.type, schema)
|
|
136
|
+
: null;
|
|
137
|
+
if (childType) {
|
|
138
|
+
const childResult = visitSelectionSet(field.selectionSet, childType, schema, fragments, fieldCosts, listMultiplier, thisDepth, allowIntrospection, errors);
|
|
139
|
+
cost += childResult.cost * mul;
|
|
140
|
+
if (childResult.depth > depth)
|
|
141
|
+
depth = childResult.depth;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { cost, depth };
|
|
145
|
+
}
|
|
146
|
+
// ── Schema helpers ───────────────────────────────────────────────────────────
|
|
147
|
+
function getOperationRootType(schema, operation) {
|
|
148
|
+
switch (operation) {
|
|
149
|
+
case 'query': return schema.getQueryType() ?? undefined;
|
|
150
|
+
case 'mutation': return schema.getMutationType() ?? undefined;
|
|
151
|
+
case 'subscription': return schema.getSubscriptionType() ?? undefined;
|
|
152
|
+
default: return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Recursively unwrap NonNull/List wrappers to check if a list is present. */
|
|
156
|
+
function typeIsList(type) {
|
|
157
|
+
if ('ofType' in type && type.ofType != null) {
|
|
158
|
+
if (type.kind === 'LIST')
|
|
159
|
+
return true;
|
|
160
|
+
return typeIsList(type.ofType);
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Unwrap NonNull / List wrappers to reach the named ObjectType (if any).
|
|
166
|
+
* Returns null for scalars, enums, unions, interfaces.
|
|
167
|
+
*/
|
|
168
|
+
function namedObjectType(type, schema) {
|
|
169
|
+
// Unwrap wrappers
|
|
170
|
+
let t = type;
|
|
171
|
+
while ('ofType' in t && t.ofType != null) {
|
|
172
|
+
t = t.ofType;
|
|
173
|
+
}
|
|
174
|
+
// Named type lookup
|
|
175
|
+
if ('name' in t) {
|
|
176
|
+
const named = schema.getType(t.name);
|
|
177
|
+
if (named && 'getFields' in named)
|
|
178
|
+
return named;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=complexity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"complexity.js","sourceRoot":"","sources":["../src/complexity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus DataLoader — N+1 Query Prevention
|
|
3
|
+
*
|
|
4
|
+
* Batches individual loads that occur within the same microtask tick into a
|
|
5
|
+
* single call to your `batchFn`. This eliminates the N+1 problem in GraphQL
|
|
6
|
+
* resolvers without requiring a database ORM.
|
|
7
|
+
*
|
|
8
|
+
* Comparison to the popular `dataloader` npm package:
|
|
9
|
+
* - Same API surface: `loader.load(key)` → `Promise<Value>`
|
|
10
|
+
* - Same batching model: deduplication + microtask coalescing
|
|
11
|
+
* - No external dependency — uses only built-in `queueMicrotask`
|
|
12
|
+
* - Typed with generics
|
|
13
|
+
* - `loadMany(keys)` helper for bulk lookups
|
|
14
|
+
*
|
|
15
|
+
* Usage
|
|
16
|
+
* ─────
|
|
17
|
+
* // Create per-request (critical: not a singleton!) inside the GraphQL context factory.
|
|
18
|
+
*
|
|
19
|
+
* const userLoader = createBatchLoader<string, User>(async (ids) => {
|
|
20
|
+
* const rows = await db.users.findMany({ where: { id: { in: ids } } });
|
|
21
|
+
* // IMPORTANT: return values in the SAME ORDER as `ids`.
|
|
22
|
+
* return ids.map(id => rows.find(r => r.id === id) ?? new Error(`User ${id} not found`));
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // In resolver:
|
|
26
|
+
* const user = await ctx.loaders.user.load(userId);
|
|
27
|
+
*
|
|
28
|
+
* Key ordering contract
|
|
29
|
+
* ─────────────────────
|
|
30
|
+
* Your `batchFn` MUST return an array of the same length as `keys`, in the
|
|
31
|
+
* same order. Return an `Error` instance for individual failures without
|
|
32
|
+
* rejecting the whole batch.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Batch function: receives deduplicated keys, returns values in the same order.
|
|
36
|
+
* Individual errors: return an `Error` in place of the value for that position.
|
|
37
|
+
*/
|
|
38
|
+
export type BatchFn<K, V> = (keys: readonly K[]) => Promise<ReadonlyArray<V | Error>>;
|
|
39
|
+
/** Cache key serialiser — defaults to `JSON.stringify`. Override for non-primitive keys. */
|
|
40
|
+
export type CacheKeyFn<K> = (key: K) => string;
|
|
41
|
+
export interface BatchLoaderOptions<K> {
|
|
42
|
+
/** Custom key serialiser for non-primitive key types. Default: JSON.stringify. */
|
|
43
|
+
cacheKeyFn?: CacheKeyFn<K>;
|
|
44
|
+
/** Disable in-flight deduplication (each load always queues a new item). Default: false. */
|
|
45
|
+
disableCache?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Maximum batch size. When exceeded, flush immediately and start a new batch.
|
|
48
|
+
* Default: Infinity (unbounded).
|
|
49
|
+
*/
|
|
50
|
+
maxBatchSize?: number;
|
|
51
|
+
}
|
|
52
|
+
export declare class BatchLoader<K, V> {
|
|
53
|
+
private readonly batchFn;
|
|
54
|
+
private readonly cacheKeyFn;
|
|
55
|
+
private readonly disableCache;
|
|
56
|
+
private readonly maxBatchSize;
|
|
57
|
+
/** In-flight dedup cache: cacheKey → pending promise */
|
|
58
|
+
private readonly cache;
|
|
59
|
+
/** Current batch queue */
|
|
60
|
+
private queue;
|
|
61
|
+
private flushScheduled;
|
|
62
|
+
constructor(batchFn: BatchFn<K, V>, options?: BatchLoaderOptions<K>);
|
|
63
|
+
/** Load a single value by key. Deduplicates concurrent loads for the same key. */
|
|
64
|
+
load(key: K): Promise<V>;
|
|
65
|
+
/** Load multiple keys — convenience wrapper. */
|
|
66
|
+
loadMany(keys: readonly K[]): Promise<Array<V | Error>>;
|
|
67
|
+
/** Remove a specific key from the deduplication cache (force a fresh load). */
|
|
68
|
+
clear(key: K): this;
|
|
69
|
+
/** Wipe the entire dedup cache. */
|
|
70
|
+
clearAll(): this;
|
|
71
|
+
/** Prime the cache with a known value (avoids a future batch call). */
|
|
72
|
+
prime(key: K, value: V): this;
|
|
73
|
+
private scheduleFlush;
|
|
74
|
+
private flush;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Convenience factory — same as `new BatchLoader(batchFn, opts)`.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* const userLoader = createBatchLoader(async (ids) => db.users.findMany({ where: { id: { in: ids } } }));
|
|
81
|
+
*/
|
|
82
|
+
export declare function createBatchLoader<K, V>(batchFn: BatchFn<K, V>, options?: BatchLoaderOptions<K>): BatchLoader<K, V>;
|
|
83
|
+
/**
|
|
84
|
+
* Create a per-request loader registry.
|
|
85
|
+
* Pass the factory as your GraphQL context `loaders` field.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* const loaders = createLoaderRegistry({
|
|
89
|
+
* user: createBatchLoader(ids => db.users.findMany(...)),
|
|
90
|
+
* product: createBatchLoader(ids => db.products.findMany(...)),
|
|
91
|
+
* });
|
|
92
|
+
*
|
|
93
|
+
* // In resolver: ctx.loaders.user.load(userId)
|
|
94
|
+
*/
|
|
95
|
+
export declare function createLoaderRegistry<T extends Record<string, BatchLoader<unknown, unknown>>>(loaders: T): T;
|
|
96
|
+
//# sourceMappingURL=dataloader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataloader.d.ts","sourceRoot":"","sources":["../src/dataloader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH;;;GAGG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;AAEtF,4FAA4F;AAC5F,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;AAE/C,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,kFAAkF;IAClF,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAC3B,4FAA4F;IAC5F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAUD,qBAAa,WAAW,CAAC,CAAC,EAAE,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,wDAAwD;IACxD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IAEvD,0BAA0B;IAC1B,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,cAAc,CAAS;gBAEnB,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,GAAE,kBAAkB,CAAC,CAAC,CAAM;IAOvE,kFAAkF;IAClF,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAoBxB,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;IAUvD,+EAA+E;IAC/E,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI;IAKnB,mCAAmC;IACnC,QAAQ,IAAI,IAAI;IAKhB,uEAAuE;IACvE,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAU7B,OAAO,CAAC,aAAa;YAYP,KAAK;CAoDpB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,CAAC,EACpC,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EACtB,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC9B,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAEnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,EACvD,OAAO,EAAE,CAAC,GAAG,CAAC,CAEf"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nexus DataLoader — N+1 Query Prevention
|
|
3
|
+
*
|
|
4
|
+
* Batches individual loads that occur within the same microtask tick into a
|
|
5
|
+
* single call to your `batchFn`. This eliminates the N+1 problem in GraphQL
|
|
6
|
+
* resolvers without requiring a database ORM.
|
|
7
|
+
*
|
|
8
|
+
* Comparison to the popular `dataloader` npm package:
|
|
9
|
+
* - Same API surface: `loader.load(key)` → `Promise<Value>`
|
|
10
|
+
* - Same batching model: deduplication + microtask coalescing
|
|
11
|
+
* - No external dependency — uses only built-in `queueMicrotask`
|
|
12
|
+
* - Typed with generics
|
|
13
|
+
* - `loadMany(keys)` helper for bulk lookups
|
|
14
|
+
*
|
|
15
|
+
* Usage
|
|
16
|
+
* ─────
|
|
17
|
+
* // Create per-request (critical: not a singleton!) inside the GraphQL context factory.
|
|
18
|
+
*
|
|
19
|
+
* const userLoader = createBatchLoader<string, User>(async (ids) => {
|
|
20
|
+
* const rows = await db.users.findMany({ where: { id: { in: ids } } });
|
|
21
|
+
* // IMPORTANT: return values in the SAME ORDER as `ids`.
|
|
22
|
+
* return ids.map(id => rows.find(r => r.id === id) ?? new Error(`User ${id} not found`));
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // In resolver:
|
|
26
|
+
* const user = await ctx.loaders.user.load(userId);
|
|
27
|
+
*
|
|
28
|
+
* Key ordering contract
|
|
29
|
+
* ─────────────────────
|
|
30
|
+
* Your `batchFn` MUST return an array of the same length as `keys`, in the
|
|
31
|
+
* same order. Return an `Error` instance for individual failures without
|
|
32
|
+
* rejecting the whole batch.
|
|
33
|
+
*/
|
|
34
|
+
export class BatchLoader {
|
|
35
|
+
batchFn;
|
|
36
|
+
cacheKeyFn;
|
|
37
|
+
disableCache;
|
|
38
|
+
maxBatchSize;
|
|
39
|
+
/** In-flight dedup cache: cacheKey → pending promise */
|
|
40
|
+
cache = new Map();
|
|
41
|
+
/** Current batch queue */
|
|
42
|
+
queue = [];
|
|
43
|
+
flushScheduled = false;
|
|
44
|
+
constructor(batchFn, options = {}) {
|
|
45
|
+
this.batchFn = batchFn;
|
|
46
|
+
this.cacheKeyFn = options.cacheKeyFn ?? ((k) => JSON.stringify(k));
|
|
47
|
+
this.disableCache = options.disableCache ?? false;
|
|
48
|
+
this.maxBatchSize = options.maxBatchSize ?? Infinity;
|
|
49
|
+
}
|
|
50
|
+
/** Load a single value by key. Deduplicates concurrent loads for the same key. */
|
|
51
|
+
load(key) {
|
|
52
|
+
const cacheKey = this.cacheKeyFn(key);
|
|
53
|
+
if (!this.disableCache) {
|
|
54
|
+
const cached = this.cache.get(cacheKey);
|
|
55
|
+
if (cached)
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
58
|
+
const promise = new Promise((resolve, reject) => {
|
|
59
|
+
this.queue.push({ key, resolve, reject });
|
|
60
|
+
this.scheduleFlush();
|
|
61
|
+
});
|
|
62
|
+
if (!this.disableCache) {
|
|
63
|
+
this.cache.set(cacheKey, promise);
|
|
64
|
+
}
|
|
65
|
+
return promise;
|
|
66
|
+
}
|
|
67
|
+
/** Load multiple keys — convenience wrapper. */
|
|
68
|
+
loadMany(keys) {
|
|
69
|
+
return Promise.all(keys.map(k => this.load(k).catch((err) => err instanceof Error ? err : new Error(String(err)))));
|
|
70
|
+
}
|
|
71
|
+
/** Remove a specific key from the deduplication cache (force a fresh load). */
|
|
72
|
+
clear(key) {
|
|
73
|
+
this.cache.delete(this.cacheKeyFn(key));
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/** Wipe the entire dedup cache. */
|
|
77
|
+
clearAll() {
|
|
78
|
+
this.cache.clear();
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
/** Prime the cache with a known value (avoids a future batch call). */
|
|
82
|
+
prime(key, value) {
|
|
83
|
+
const cacheKey = this.cacheKeyFn(key);
|
|
84
|
+
if (!this.cache.has(cacheKey)) {
|
|
85
|
+
this.cache.set(cacheKey, Promise.resolve(value));
|
|
86
|
+
}
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
// ── Private ────────────────────────────────────────────────────────────────
|
|
90
|
+
scheduleFlush() {
|
|
91
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
92
|
+
// Flush immediately when the batch limit is reached
|
|
93
|
+
void this.flush();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!this.flushScheduled) {
|
|
97
|
+
this.flushScheduled = true;
|
|
98
|
+
queueMicrotask(() => void this.flush());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async flush() {
|
|
102
|
+
this.flushScheduled = false;
|
|
103
|
+
if (this.queue.length === 0)
|
|
104
|
+
return;
|
|
105
|
+
// Drain the current queue
|
|
106
|
+
const batch = this.queue.splice(0);
|
|
107
|
+
// Deduplicate keys (preserving order for the result map)
|
|
108
|
+
const seen = new Map();
|
|
109
|
+
const keyList = [];
|
|
110
|
+
for (const item of batch) {
|
|
111
|
+
const ck = this.cacheKeyFn(item.key);
|
|
112
|
+
if (!seen.has(ck)) {
|
|
113
|
+
seen.set(ck, item.key);
|
|
114
|
+
keyList.push(item.key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let values;
|
|
118
|
+
try {
|
|
119
|
+
values = await this.batchFn(keyList);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
// Entire batch failed — reject all items
|
|
123
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
124
|
+
for (const item of batch) {
|
|
125
|
+
item.reject(error);
|
|
126
|
+
this.cache.delete(this.cacheKeyFn(item.key));
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Build key → value map
|
|
131
|
+
const resultMap = new Map();
|
|
132
|
+
keyList.forEach((k, i) => {
|
|
133
|
+
resultMap.set(this.cacheKeyFn(k), values[i] ?? new Error(`Missing value for key at index ${i}`));
|
|
134
|
+
});
|
|
135
|
+
// Resolve / reject each original queue item
|
|
136
|
+
for (const item of batch) {
|
|
137
|
+
const result = resultMap.get(this.cacheKeyFn(item.key));
|
|
138
|
+
if (result === undefined) {
|
|
139
|
+
const err = new Error(`[Nexus DataLoader] batchFn returned no value for key: ${this.cacheKeyFn(item.key)}`);
|
|
140
|
+
item.reject(err);
|
|
141
|
+
this.cache.delete(this.cacheKeyFn(item.key));
|
|
142
|
+
}
|
|
143
|
+
else if (result instanceof Error) {
|
|
144
|
+
item.reject(result);
|
|
145
|
+
this.cache.delete(this.cacheKeyFn(item.key));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
item.resolve(result);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Convenience factory — same as `new BatchLoader(batchFn, opts)`.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* const userLoader = createBatchLoader(async (ids) => db.users.findMany({ where: { id: { in: ids } } }));
|
|
158
|
+
*/
|
|
159
|
+
export function createBatchLoader(batchFn, options) {
|
|
160
|
+
return new BatchLoader(batchFn, options);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Create a per-request loader registry.
|
|
164
|
+
* Pass the factory as your GraphQL context `loaders` field.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* const loaders = createLoaderRegistry({
|
|
168
|
+
* user: createBatchLoader(ids => db.users.findMany(...)),
|
|
169
|
+
* product: createBatchLoader(ids => db.products.findMany(...)),
|
|
170
|
+
* });
|
|
171
|
+
*
|
|
172
|
+
* // In resolver: ctx.loaders.user.load(userId)
|
|
173
|
+
*/
|
|
174
|
+
export function createLoaderRegistry(loaders) {
|
|
175
|
+
return loaders;
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=dataloader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataloader.js","sourceRoot":"","sources":["../src/dataloader.ts"],"names":[],"mappings":"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"}
|