@graphoria/server 0.2.3 → 0.3.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/README.md +25 -20
- package/dist/src/ai/tools/core.d.ts +6 -1
- package/dist/src/ai/tools/core.d.ts.map +1 -1
- package/dist/src/ai/tools/core.test.d.ts +2 -0
- package/dist/src/ai/tools/core.test.d.ts.map +1 -0
- package/dist/src/analyzeQuery/costLimit.d.ts +30 -0
- package/dist/src/analyzeQuery/costLimit.d.ts.map +1 -0
- package/dist/src/analyzeQuery/costLimit.test.d.ts +2 -0
- package/dist/src/analyzeQuery/costLimit.test.d.ts.map +1 -0
- package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
- package/dist/src/analyzeQuery/fragments.d.ts +6 -0
- package/dist/src/analyzeQuery/fragments.d.ts.map +1 -0
- package/dist/src/config/types/auth.d.ts +8 -0
- package/dist/src/config/types/auth.d.ts.map +1 -1
- package/dist/src/config/types/db.d.ts +2 -0
- package/dist/src/config/types/db.d.ts.map +1 -1
- package/dist/src/config/types/operation.d.ts +4 -0
- package/dist/src/config/types/operation.d.ts.map +1 -1
- package/dist/src/config/types/operation.test.d.ts +2 -0
- package/dist/src/config/types/operation.test.d.ts.map +1 -0
- package/dist/src/configuration/crossReferences.d.ts +27 -0
- package/dist/src/configuration/crossReferences.d.ts.map +1 -0
- package/dist/src/configuration/crossReferences.test.d.ts +2 -0
- package/dist/src/configuration/crossReferences.test.d.ts.map +1 -0
- package/dist/src/configuration/getSchemas/index.d.ts +70 -4
- package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +3 -0
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts +32 -2
- package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
- package/dist/src/configuration/index.d.ts +41 -2
- package/dist/src/configuration/index.d.ts.map +1 -1
- package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
- package/dist/src/configuration/rest/index.d.ts.map +1 -1
- package/dist/src/console/api.d.ts +7 -5
- package/dist/src/console/api.d.ts.map +1 -1
- package/dist/src/console/session.d.ts +21 -0
- package/dist/src/console/session.d.ts.map +1 -0
- package/dist/src/console/session.test.d.ts +2 -0
- package/dist/src/console/session.test.d.ts.map +1 -0
- package/dist/src/databases/common.d.ts +12 -4
- package/dist/src/databases/common.d.ts.map +1 -1
- package/dist/src/databases/core/executor.d.ts +6 -2
- package/dist/src/databases/core/executor.d.ts.map +1 -1
- package/dist/src/databases/core/function-mapping.d.ts +4 -3
- package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
- package/dist/src/databases/core/query-builder.d.ts +7 -1
- package/dist/src/databases/core/query-builder.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts +29 -6
- package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/connection.test.d.ts +2 -0
- package/dist/src/databases/engines/mssql/connection.test.d.ts.map +1 -0
- package/dist/src/databases/engines/mssql/format.d.ts +2 -1
- package/dist/src/databases/engines/mssql/format.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/query/index.d.ts +3 -2
- package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts +16 -0
- package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/connection.test.d.ts +2 -0
- package/dist/src/databases/engines/mysql/connection.test.d.ts.map +1 -0
- package/dist/src/databases/engines/mysql/format.d.ts +2 -1
- package/dist/src/databases/engines/mysql/format.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/query/index.d.ts +3 -2
- package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts +25 -6
- package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/connection.test.d.ts +2 -0
- package/dist/src/databases/engines/postgresql/connection.test.d.ts.map +1 -0
- package/dist/src/databases/engines/postgresql/format.d.ts +2 -1
- package/dist/src/databases/engines/postgresql/format.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/query/index.d.ts +3 -2
- package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
- package/dist/src/databases/high-level-operations.d.ts +2 -0
- package/dist/src/databases/high-level-operations.d.ts.map +1 -1
- package/dist/src/databases/transformers/data-transformers.d.ts +2 -0
- package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/singletons/env.d.ts +19 -0
- package/dist/src/singletons/env.d.ts.map +1 -1
- package/dist/src/singletons/queryTimeout.d.ts +3 -0
- package/dist/src/singletons/queryTimeout.d.ts.map +1 -0
- package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
- package/dist/src/subscriptions/strategies/database.test.d.ts +2 -0
- package/dist/src/subscriptions/strategies/database.test.d.ts.map +1 -0
- package/dist/src/subscriptions/types.d.ts +4 -1
- package/dist/src/subscriptions/types.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
- package/dist/src/types/env.d.ts +73 -0
- package/dist/src/types/env.d.ts.map +1 -1
- package/dist/src/types/zod/auth.d.ts +12 -0
- package/dist/src/types/zod/auth.d.ts.map +1 -1
- package/dist/src/types/zod/configuration.d.ts +28 -0
- package/dist/src/types/zod/configuration.d.ts.map +1 -1
- package/dist/src/utils/rateLimit.d.ts +65 -0
- package/dist/src/utils/rateLimit.d.ts.map +1 -0
- package/dist/src/utils/rateLimit.test.d.ts +2 -0
- package/dist/src/utils/rateLimit.test.d.ts.map +1 -0
- package/dist/src/utils/responses.d.ts +3 -0
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/playgrounds/console/index.html +6 -6
- package/playgrounds/graphiql/index.html +136 -136
- package/src/ai/tools/core.ts +30 -6
- package/src/analyzeQuery/costLimit.ts +236 -0
- package/src/analyzeQuery/depthLimit.ts +4 -23
- package/src/analyzeQuery/fragments.ts +18 -0
- package/src/config/types/auth.ts +7 -0
- package/src/config/types/db.ts +7 -0
- package/src/config/types/operation.ts +4 -0
- package/src/configuration/crossReferences.ts +340 -0
- package/src/configuration/getSchemas/type-definition-generator/index.ts +1 -1
- package/src/configuration/gql/buildExecute.ts +1 -1
- package/src/configuration/gql/handleGraphQLRequestFactory.ts +80 -10
- package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +22 -2
- package/src/configuration/index.ts +24 -6
- package/src/configuration/rest/handleRESTRequestFactory.ts +6 -4
- package/src/configuration/rest/index.ts +11 -2
- package/src/console/api.ts +51 -10
- package/src/console/session.ts +125 -0
- package/src/databases/common.ts +55 -10
- package/src/databases/core/executor.ts +12 -2
- package/src/databases/core/function-mapping.ts +5 -0
- package/src/databases/core/query-builder.ts +7 -0
- package/src/databases/engines/mssql/connection.ts +66 -14
- package/src/databases/engines/mssql/format.ts +3 -1
- package/src/databases/engines/mssql/query/index.ts +12 -2
- package/src/databases/engines/mysql/connection.ts +30 -13
- package/src/databases/engines/mysql/format.ts +3 -1
- package/src/databases/engines/mysql/query/index.ts +25 -4
- package/src/databases/engines/postgresql/connection.ts +76 -15
- package/src/databases/engines/postgresql/format.ts +3 -1
- package/src/databases/engines/postgresql/query/index.ts +12 -3
- package/src/databases/high-level-operations.ts +13 -2
- package/src/index.ts +82 -13
- package/src/singletons/queryTimeout.ts +20 -0
- package/src/subscriptions/strategies/database.ts +19 -3
- package/src/subscriptions/types.ts +4 -1
- package/src/subscriptions/utils/polling.ts +6 -3
- package/src/types/env.ts +21 -1
- package/src/utils/rateLimit.ts +209 -0
- package/src/utils/responses.ts +10 -0
package/src/ai/tools/core.ts
CHANGED
|
@@ -15,8 +15,10 @@ import type { BunRequest } from "bun";
|
|
|
15
15
|
import type { GraphQLNamedType, GraphQLSchema } from "graphql";
|
|
16
16
|
import type { GetSchemaReturn } from "../../configuration/getSchemas";
|
|
17
17
|
|
|
18
|
+
import { checkQueryCost } from "../../analyzeQuery/costLimit";
|
|
18
19
|
import { depthLimitRule } from "../../analyzeQuery/depthLimit";
|
|
19
20
|
import { categorizeSqlType, isNumericType, SqlTypeCategory } from "../../databases/sqlTypeUtils";
|
|
21
|
+
import { env } from "../../singletons/env";
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* A per-role compiled schema bundle (tables, operations, handlers, …).
|
|
@@ -202,7 +204,10 @@ export type ValidationError = {
|
|
|
202
204
|
locations?: ReadonlyArray<{ line: number; column: number }>;
|
|
203
205
|
};
|
|
204
206
|
|
|
205
|
-
export type ValidateQueryFn = (
|
|
207
|
+
export type ValidateQueryFn = (
|
|
208
|
+
query: string,
|
|
209
|
+
variables?: Record<string, unknown>,
|
|
210
|
+
) => {
|
|
206
211
|
hasErrors: boolean;
|
|
207
212
|
validationErrors: readonly ValidationError[];
|
|
208
213
|
};
|
|
@@ -211,18 +216,37 @@ export type ValidateQueryFn = (query: string) => {
|
|
|
211
216
|
* Build a query validator for a role. Without a positive depth limit it
|
|
212
217
|
* delegates to the role's own `hasErrors`; with one it layers a depth-limit
|
|
213
218
|
* rule on the standard rule set.
|
|
219
|
+
*
|
|
220
|
+
* The cost budget applies on both branches, and there is no MCP-specific
|
|
221
|
+
* override for it. MCP gets its own depth knob because an agent legitimately
|
|
222
|
+
* writes deeper queries than an application does; nothing makes the same
|
|
223
|
+
* argument for asking a database for more rows.
|
|
214
224
|
*/
|
|
215
225
|
export const makeValidateQuery =
|
|
216
226
|
(role: RoleEntities, maxQueryDepth?: number): ValidateQueryFn =>
|
|
217
|
-
(query) => {
|
|
227
|
+
(query, variables) => {
|
|
218
228
|
if (maxQueryDepth === undefined || maxQueryDepth <= 0) {
|
|
219
|
-
return role.handlers.gql.hasErrors(query);
|
|
229
|
+
return role.handlers.gql.hasErrors(query, { variables });
|
|
220
230
|
}
|
|
221
|
-
const
|
|
231
|
+
const document = parse(query);
|
|
232
|
+
const errors = validate(role.schema, document, [
|
|
222
233
|
...specifiedRules,
|
|
223
234
|
depthLimitRule(maxQueryDepth),
|
|
224
235
|
]);
|
|
225
|
-
|
|
236
|
+
if (errors.length > 0) return { hasErrors: true, validationErrors: errors };
|
|
237
|
+
|
|
238
|
+
if (env.maxQueryCost > 0) {
|
|
239
|
+
const costError = checkQueryCost(
|
|
240
|
+
document,
|
|
241
|
+
role.schema,
|
|
242
|
+
variables ?? {},
|
|
243
|
+
{ defaultPageSize: env.defaultPageSize, maxPageSize: env.maxPageSize },
|
|
244
|
+
env.maxQueryCost,
|
|
245
|
+
);
|
|
246
|
+
if (costError) return { hasErrors: true, validationErrors: [costError] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { hasErrors: false, validationErrors: errors };
|
|
226
250
|
};
|
|
227
251
|
|
|
228
252
|
// ---- graphql_execute ----
|
|
@@ -246,7 +270,7 @@ export const executeGraphqlCore = async (
|
|
|
246
270
|
try {
|
|
247
271
|
if (containsNonQueryOperation(query)) return { kind: "non_query" };
|
|
248
272
|
|
|
249
|
-
const { hasErrors, validationErrors } = validateQuery(query);
|
|
273
|
+
const { hasErrors, validationErrors } = validateQuery(query, variables);
|
|
250
274
|
if (hasErrors) {
|
|
251
275
|
return {
|
|
252
276
|
kind: "validation",
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { GraphQLError, GraphQLObjectType, Kind } from "graphql";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
DocumentNode,
|
|
5
|
+
FieldNode,
|
|
6
|
+
FragmentDefinitionNode,
|
|
7
|
+
GraphQLSchema,
|
|
8
|
+
OperationDefinitionNode,
|
|
9
|
+
SelectionSetNode,
|
|
10
|
+
ValueNode,
|
|
11
|
+
} from "graphql";
|
|
12
|
+
|
|
13
|
+
import { collectFragments } from "./fragments";
|
|
14
|
+
import { getFieldType, unwrapType } from "./typeUtils";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Page bounds the estimate is drawn against. `maxPageSize` is carried, not read:
|
|
18
|
+
* a resolved limit above it is rejected at execution, so the walk never has to
|
|
19
|
+
* clamp — but the estimator needs it the day that ordering changes.
|
|
20
|
+
*/
|
|
21
|
+
export type CostPageLimits = {
|
|
22
|
+
defaultPageSize: number;
|
|
23
|
+
maxPageSize: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type WalkContext = {
|
|
27
|
+
schema: GraphQLSchema;
|
|
28
|
+
variables: Record<string, unknown>;
|
|
29
|
+
pageLimits: CostPageLimits;
|
|
30
|
+
fragments: Map<string, FragmentDefinitionNode>;
|
|
31
|
+
// A fragment's cost depends only on its own body and type condition, so one
|
|
32
|
+
// expansion serves every spread of it. Without this a document whose
|
|
33
|
+
// fragments each spread the next one twice costs 2^n to estimate.
|
|
34
|
+
fragmentCosts: Map<string, number>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Resolve an argument to a number, following a variable into the runtime values.
|
|
38
|
+
// Mirrors `resolveLimitValue` in databases/common.ts so the estimate and the
|
|
39
|
+
// page-size cap agree on what "resolves to a number" means.
|
|
40
|
+
const resolveIntValue = (
|
|
41
|
+
value: ValueNode | undefined,
|
|
42
|
+
variables: Record<string, unknown>,
|
|
43
|
+
): number | undefined => {
|
|
44
|
+
if (!value) return undefined;
|
|
45
|
+
|
|
46
|
+
if (value.kind === Kind.INT) return Number.parseInt(value.value, 10);
|
|
47
|
+
|
|
48
|
+
if (value.kind === Kind.VARIABLE) {
|
|
49
|
+
const resolved = variables[value.name.value];
|
|
50
|
+
return typeof resolved === "number" ? resolved : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// How many rows a field's children are paid for. Only a list field multiplies:
|
|
57
|
+
// a to-one relationship, a single-row field and a scalar each cost their subtree
|
|
58
|
+
// once.
|
|
59
|
+
const multiplierFor = (field: FieldNode, isArray: boolean, ctx: WalkContext): number => {
|
|
60
|
+
if (!isArray) return 1;
|
|
61
|
+
|
|
62
|
+
const limit = field.arguments?.find((argument) => argument.name.value === "limit")?.value;
|
|
63
|
+
|
|
64
|
+
return resolveIntValue(limit, ctx.variables) ?? ctx.pageLimits.defaultPageSize;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const objectTypeNamed = (schema: GraphQLSchema, name: string): GraphQLObjectType | undefined => {
|
|
68
|
+
const type = schema.getType(name);
|
|
69
|
+
return type instanceof GraphQLObjectType ? type : undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const costOfFragment = (name: string, ctx: WalkContext, onPath: Set<string>): number => {
|
|
73
|
+
const memoised = ctx.fragmentCosts.get(name);
|
|
74
|
+
if (memoised !== undefined) return memoised;
|
|
75
|
+
|
|
76
|
+
// A fragment already on the path is a cycle. `validate` rejects those before
|
|
77
|
+
// the estimate runs; cutting here only keeps a hand-built document finite.
|
|
78
|
+
if (onPath.has(name)) return 0;
|
|
79
|
+
|
|
80
|
+
const fragment = ctx.fragments.get(name);
|
|
81
|
+
if (!fragment) return 0;
|
|
82
|
+
|
|
83
|
+
onPath.add(name);
|
|
84
|
+
const cost = costOfSelectionSet(
|
|
85
|
+
fragment.selectionSet,
|
|
86
|
+
objectTypeNamed(ctx.schema, fragment.typeCondition.name.value),
|
|
87
|
+
ctx,
|
|
88
|
+
onPath,
|
|
89
|
+
);
|
|
90
|
+
onPath.delete(name);
|
|
91
|
+
|
|
92
|
+
ctx.fragmentCosts.set(name, cost);
|
|
93
|
+
return cost;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `cost(field) = 1 + multiplier(field) × Σ cost(child)`, summed across the
|
|
98
|
+
* selection set. An unresolved parent type costs its children once — the
|
|
99
|
+
* estimate degrades, it does not throw.
|
|
100
|
+
*/
|
|
101
|
+
const costOfSelectionSet = (
|
|
102
|
+
selectionSet: SelectionSetNode | undefined,
|
|
103
|
+
parentType: GraphQLObjectType | undefined,
|
|
104
|
+
ctx: WalkContext,
|
|
105
|
+
onPath: Set<string>,
|
|
106
|
+
): number => {
|
|
107
|
+
if (!selectionSet) return 0;
|
|
108
|
+
|
|
109
|
+
let total = 0;
|
|
110
|
+
|
|
111
|
+
for (const selection of selectionSet.selections) {
|
|
112
|
+
switch (selection.kind) {
|
|
113
|
+
case Kind.FIELD: {
|
|
114
|
+
if (selection.name.value.startsWith("__")) continue;
|
|
115
|
+
|
|
116
|
+
const fieldType = parentType ? getFieldType(parentType, selection.name.value) : undefined;
|
|
117
|
+
const unwrapped = fieldType ? unwrapType(fieldType) : undefined;
|
|
118
|
+
const childType = unwrapped?.type instanceof GraphQLObjectType ? unwrapped.type : undefined;
|
|
119
|
+
|
|
120
|
+
const children = costOfSelectionSet(selection.selectionSet, childType, ctx, onPath);
|
|
121
|
+
|
|
122
|
+
total += 1 + multiplierFor(selection, unwrapped?.isArray ?? false, ctx) * children;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case Kind.FRAGMENT_SPREAD: {
|
|
127
|
+
total += costOfFragment(selection.name.value, ctx, onPath);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case Kind.INLINE_FRAGMENT: {
|
|
132
|
+
const condition = selection.typeCondition
|
|
133
|
+
? objectTypeNamed(ctx.schema, selection.typeCondition.name.value)
|
|
134
|
+
: parentType;
|
|
135
|
+
|
|
136
|
+
total += costOfSelectionSet(selection.selectionSet, condition, ctx, onPath);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return total;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const rootTypeFor = (
|
|
146
|
+
schema: GraphQLSchema,
|
|
147
|
+
operation: OperationDefinitionNode,
|
|
148
|
+
): GraphQLObjectType | undefined => {
|
|
149
|
+
switch (operation.operation) {
|
|
150
|
+
case "mutation":
|
|
151
|
+
return schema.getMutationType() ?? undefined;
|
|
152
|
+
case "subscription":
|
|
153
|
+
return schema.getSubscriptionType() ?? undefined;
|
|
154
|
+
default:
|
|
155
|
+
return schema.getQueryType() ?? undefined;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const costliestOperation = (
|
|
160
|
+
document: DocumentNode,
|
|
161
|
+
schema: GraphQLSchema,
|
|
162
|
+
variables: Record<string, unknown>,
|
|
163
|
+
pageLimits: CostPageLimits,
|
|
164
|
+
): { cost: number; operation: OperationDefinitionNode | undefined } => {
|
|
165
|
+
const ctx: WalkContext = {
|
|
166
|
+
schema,
|
|
167
|
+
variables,
|
|
168
|
+
pageLimits,
|
|
169
|
+
fragments: collectFragments(document),
|
|
170
|
+
fragmentCosts: new Map(),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let worst: { cost: number; operation: OperationDefinitionNode | undefined } = {
|
|
174
|
+
cost: 0,
|
|
175
|
+
operation: undefined,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
for (const definition of document.definitions) {
|
|
179
|
+
if (definition.kind !== Kind.OPERATION_DEFINITION) continue;
|
|
180
|
+
|
|
181
|
+
const cost = costOfSelectionSet(
|
|
182
|
+
definition.selectionSet,
|
|
183
|
+
rootTypeFor(schema, definition),
|
|
184
|
+
ctx,
|
|
185
|
+
new Set<string>(),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (cost > worst.cost || worst.operation === undefined) {
|
|
189
|
+
worst = { cost, operation: definition };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return worst;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Estimate how many rows a document asks a database for, before executing it.
|
|
198
|
+
*
|
|
199
|
+
* Every field costs 1 for itself plus the cost of its children multiplied by the
|
|
200
|
+
* page size it requests — the `limit` argument for a list field, resolved through
|
|
201
|
+
* `variables`, falling back to `defaultPageSize`; 1 for anything that returns a
|
|
202
|
+
* single row. Fragment spreads are expanded, so moving a fan-out into a fragment
|
|
203
|
+
* does not hide it.
|
|
204
|
+
*
|
|
205
|
+
* A document carrying several operations is scored by its most expensive one:
|
|
206
|
+
* a request executes one of them.
|
|
207
|
+
*/
|
|
208
|
+
export const estimateQueryCost = (
|
|
209
|
+
document: DocumentNode,
|
|
210
|
+
schema: GraphQLSchema,
|
|
211
|
+
variables: Record<string, unknown>,
|
|
212
|
+
pageLimits: CostPageLimits,
|
|
213
|
+
): number => costliestOperation(document, schema, variables, pageLimits).cost;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Reject a document whose estimated cost exceeds `maxCost`. Returns `null` when
|
|
217
|
+
* it is within budget.
|
|
218
|
+
*/
|
|
219
|
+
export const checkQueryCost = (
|
|
220
|
+
document: DocumentNode,
|
|
221
|
+
schema: GraphQLSchema,
|
|
222
|
+
variables: Record<string, unknown>,
|
|
223
|
+
pageLimits: CostPageLimits,
|
|
224
|
+
maxCost: number,
|
|
225
|
+
): GraphQLError | null => {
|
|
226
|
+
const { cost, operation } = costliestOperation(document, schema, variables, pageLimits);
|
|
227
|
+
|
|
228
|
+
if (cost <= maxCost) return null;
|
|
229
|
+
|
|
230
|
+
const operationName = operation?.name?.value ?? "anonymous";
|
|
231
|
+
|
|
232
|
+
return new GraphQLError(
|
|
233
|
+
`Estimated query cost of ${cost} exceeds the maximum allowed cost of ${maxCost} (operation: "${operationName}"). Raise MAX_QUERY_COST to allow more expensive queries.`,
|
|
234
|
+
{ nodes: operation ? [operation] : undefined },
|
|
235
|
+
);
|
|
236
|
+
};
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { GraphQLError, Kind } from "graphql";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
DocumentNode,
|
|
7
|
-
FragmentDefinitionNode,
|
|
8
|
-
ValidationContext,
|
|
9
|
-
} from "graphql";
|
|
3
|
+
import type { ASTNode, ASTVisitor, FragmentDefinitionNode, ValidationContext } from "graphql";
|
|
4
|
+
|
|
5
|
+
import { collectFragments } from "./fragments";
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* Compute the depth of a selection set, resolving fragment spreads.
|
|
@@ -68,21 +64,6 @@ function computeDepth(
|
|
|
68
64
|
return max;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
/**
|
|
72
|
-
* Collect all fragment definitions from a document.
|
|
73
|
-
*/
|
|
74
|
-
function collectFragments(document: DocumentNode): Map<string, FragmentDefinitionNode> {
|
|
75
|
-
const fragments = new Map<string, FragmentDefinitionNode>();
|
|
76
|
-
|
|
77
|
-
for (const def of document.definitions) {
|
|
78
|
-
if (def.kind === Kind.FRAGMENT_DEFINITION) {
|
|
79
|
-
fragments.set(def.name.value, def);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return fragments;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
67
|
/**
|
|
87
68
|
* GraphQL validation rule that rejects queries exceeding a maximum nesting depth.
|
|
88
69
|
*
|
|
@@ -113,7 +94,7 @@ export const depthLimitRule =
|
|
|
113
94
|
|
|
114
95
|
context.reportError(
|
|
115
96
|
new GraphQLError(
|
|
116
|
-
`Query depth of ${depth} exceeds the maximum allowed depth of ${maxDepth} (operation: "${operationName}")
|
|
97
|
+
`Query depth of ${depth} exceeds the maximum allowed depth of ${maxDepth} (operation: "${operationName}"). Raise MAX_QUERY_DEPTH to allow deeper queries.`,
|
|
117
98
|
{ nodes: [def] },
|
|
118
99
|
),
|
|
119
100
|
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Kind } from "graphql";
|
|
2
|
+
|
|
3
|
+
import type { DocumentNode, FragmentDefinitionNode } from "graphql";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Collect all fragment definitions from a document.
|
|
7
|
+
*/
|
|
8
|
+
export const collectFragments = (document: DocumentNode): Map<string, FragmentDefinitionNode> => {
|
|
9
|
+
const fragments = new Map<string, FragmentDefinitionNode>();
|
|
10
|
+
|
|
11
|
+
for (const def of document.definitions) {
|
|
12
|
+
if (def.kind === Kind.FRAGMENT_DEFINITION) {
|
|
13
|
+
fragments.set(def.name.value, def);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return fragments;
|
|
18
|
+
};
|
package/src/config/types/auth.ts
CHANGED
|
@@ -137,6 +137,13 @@ export const RolePermissionZod = z
|
|
|
137
137
|
.union([z.array(z.string()), z.literal("ALL")])
|
|
138
138
|
.optional()
|
|
139
139
|
.default([]),
|
|
140
|
+
/** Request ceiling for this role, overriding RATE_LIMIT_MAX. 0 disables it */
|
|
141
|
+
rateLimit: z
|
|
142
|
+
.strictObject({
|
|
143
|
+
max: z.number().int().min(0),
|
|
144
|
+
windowMs: z.number().int().positive().optional(),
|
|
145
|
+
})
|
|
146
|
+
.optional(),
|
|
140
147
|
})
|
|
141
148
|
.default({
|
|
142
149
|
tables: [],
|
package/src/config/types/db.ts
CHANGED
|
@@ -168,11 +168,18 @@ export const MSSQLConnectionOptionsZod = z.strictObject({
|
|
|
168
168
|
min: z.number().int().nonnegative().default(0),
|
|
169
169
|
/** Maximum time in seconds a connection can be idle before being closed */
|
|
170
170
|
idleTimeout: z.number().nonnegative().default(30),
|
|
171
|
+
/**
|
|
172
|
+
* Maximum time in seconds to wait for a free connection when the pool is
|
|
173
|
+
* saturated. Without a bound, a slow-query storm queues callers instead of
|
|
174
|
+
* failing them. Bun's SQL driver exposes no equivalent, so this is MSSQL only.
|
|
175
|
+
*/
|
|
176
|
+
acquireTimeout: z.number().positive().default(30),
|
|
171
177
|
})
|
|
172
178
|
.default({
|
|
173
179
|
max: 10,
|
|
174
180
|
min: 0,
|
|
175
181
|
idleTimeout: 30,
|
|
182
|
+
acquireTimeout: 30,
|
|
176
183
|
}),
|
|
177
184
|
/** Maximum time in seconds to wait when establishing a connection */
|
|
178
185
|
connectionTimeout: z.number().nonnegative().default(30),
|
|
@@ -108,6 +108,8 @@ export const OperationZod = z
|
|
|
108
108
|
rest: OperationRestConfigZod.optional(),
|
|
109
109
|
graphql: OperationGraphQLConfigZod.optional().default({ enabled: true }),
|
|
110
110
|
cache: OperationCacheConfigZod.optional(),
|
|
111
|
+
/** Statement timeout in milliseconds. Overrides QUERY_TIMEOUT_MS for this operation. */
|
|
112
|
+
timeout: z.number().int().positive().optional(),
|
|
111
113
|
})
|
|
112
114
|
.refine(
|
|
113
115
|
(operation) => {
|
|
@@ -249,6 +251,8 @@ export type BaseOperation<TInput, TOutput, TInitData> = {
|
|
|
249
251
|
graphql?: OperationGraphQLConfig;
|
|
250
252
|
/** Cache configuration */
|
|
251
253
|
cache?: OperationCacheConfig;
|
|
254
|
+
/** Statement timeout in milliseconds. Overrides `QUERY_TIMEOUT_MS` for this operation. */
|
|
255
|
+
timeout?: number;
|
|
252
256
|
};
|
|
253
257
|
|
|
254
258
|
/**
|