@graphoria/server 0.2.2 → 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.
Files changed (160) hide show
  1. package/README.md +25 -20
  2. package/dist/src/ai/tools/core.d.ts +6 -1
  3. package/dist/src/ai/tools/core.d.ts.map +1 -1
  4. package/dist/src/ai/tools/core.test.d.ts +2 -0
  5. package/dist/src/ai/tools/core.test.d.ts.map +1 -0
  6. package/dist/src/analyzeQuery/costLimit.d.ts +30 -0
  7. package/dist/src/analyzeQuery/costLimit.d.ts.map +1 -0
  8. package/dist/src/analyzeQuery/costLimit.test.d.ts +2 -0
  9. package/dist/src/analyzeQuery/costLimit.test.d.ts.map +1 -0
  10. package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
  11. package/dist/src/analyzeQuery/fragments.d.ts +6 -0
  12. package/dist/src/analyzeQuery/fragments.d.ts.map +1 -0
  13. package/dist/src/config/types/ai.d.ts +0 -6
  14. package/dist/src/config/types/ai.d.ts.map +1 -1
  15. package/dist/src/config/types/auth.d.ts +8 -29
  16. package/dist/src/config/types/auth.d.ts.map +1 -1
  17. package/dist/src/config/types/cron.d.ts +0 -94
  18. package/dist/src/config/types/cron.d.ts.map +1 -1
  19. package/dist/src/config/types/db.d.ts +2 -87
  20. package/dist/src/config/types/db.d.ts.map +1 -1
  21. package/dist/src/config/types/operation.d.ts +4 -0
  22. package/dist/src/config/types/operation.d.ts.map +1 -1
  23. package/dist/src/config/types/operation.test.d.ts +2 -0
  24. package/dist/src/config/types/operation.test.d.ts.map +1 -0
  25. package/dist/src/config/types/queue.d.ts +0 -47
  26. package/dist/src/config/types/queue.d.ts.map +1 -1
  27. package/dist/src/config/types/remote-rest.d.ts +0 -9
  28. package/dist/src/config/types/remote-rest.d.ts.map +1 -1
  29. package/dist/src/config/types/remote-schema.d.ts +0 -10
  30. package/dist/src/config/types/remote-schema.d.ts.map +1 -1
  31. package/dist/src/configuration/crossReferences.d.ts +27 -0
  32. package/dist/src/configuration/crossReferences.d.ts.map +1 -0
  33. package/dist/src/configuration/crossReferences.test.d.ts +2 -0
  34. package/dist/src/configuration/crossReferences.test.d.ts.map +1 -0
  35. package/dist/src/configuration/getSchemas/index.d.ts +70 -4
  36. package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
  37. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +3 -0
  38. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
  39. package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts +32 -2
  40. package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
  41. package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
  42. package/dist/src/configuration/index.d.ts +41 -2
  43. package/dist/src/configuration/index.d.ts.map +1 -1
  44. package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
  45. package/dist/src/configuration/rest/index.d.ts.map +1 -1
  46. package/dist/src/console/api.d.ts +7 -5
  47. package/dist/src/console/api.d.ts.map +1 -1
  48. package/dist/src/console/session.d.ts +21 -0
  49. package/dist/src/console/session.d.ts.map +1 -0
  50. package/dist/src/console/session.test.d.ts +2 -0
  51. package/dist/src/console/session.test.d.ts.map +1 -0
  52. package/dist/src/cron/index.d.ts +1 -1
  53. package/dist/src/cron/index.d.ts.map +1 -1
  54. package/dist/src/cron/index.test.d.ts +2 -0
  55. package/dist/src/cron/index.test.d.ts.map +1 -0
  56. package/dist/src/databases/common.d.ts +12 -4
  57. package/dist/src/databases/common.d.ts.map +1 -1
  58. package/dist/src/databases/core/executor.d.ts +6 -2
  59. package/dist/src/databases/core/executor.d.ts.map +1 -1
  60. package/dist/src/databases/core/function-mapping.d.ts +4 -3
  61. package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
  62. package/dist/src/databases/core/query-builder.d.ts +7 -1
  63. package/dist/src/databases/core/query-builder.d.ts.map +1 -1
  64. package/dist/src/databases/engines/mssql/connection.d.ts +29 -6
  65. package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
  66. package/dist/src/databases/engines/mssql/connection.test.d.ts +2 -0
  67. package/dist/src/databases/engines/mssql/connection.test.d.ts.map +1 -0
  68. package/dist/src/databases/engines/mssql/format.d.ts +2 -1
  69. package/dist/src/databases/engines/mssql/format.d.ts.map +1 -1
  70. package/dist/src/databases/engines/mssql/query/index.d.ts +3 -2
  71. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  72. package/dist/src/databases/engines/mysql/connection.d.ts +16 -0
  73. package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
  74. package/dist/src/databases/engines/mysql/connection.test.d.ts +2 -0
  75. package/dist/src/databases/engines/mysql/connection.test.d.ts.map +1 -0
  76. package/dist/src/databases/engines/mysql/format.d.ts +2 -1
  77. package/dist/src/databases/engines/mysql/format.d.ts.map +1 -1
  78. package/dist/src/databases/engines/mysql/query/index.d.ts +3 -2
  79. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  80. package/dist/src/databases/engines/postgresql/connection.d.ts +25 -6
  81. package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
  82. package/dist/src/databases/engines/postgresql/connection.test.d.ts +2 -0
  83. package/dist/src/databases/engines/postgresql/connection.test.d.ts.map +1 -0
  84. package/dist/src/databases/engines/postgresql/format.d.ts +2 -1
  85. package/dist/src/databases/engines/postgresql/format.d.ts.map +1 -1
  86. package/dist/src/databases/engines/postgresql/query/index.d.ts +3 -2
  87. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  88. package/dist/src/databases/high-level-operations.d.ts +2 -0
  89. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  90. package/dist/src/databases/transformers/data-transformers.d.ts +2 -0
  91. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  92. package/dist/src/index.d.ts.map +1 -1
  93. package/dist/src/singletons/env.d.ts +19 -0
  94. package/dist/src/singletons/env.d.ts.map +1 -1
  95. package/dist/src/singletons/queryTimeout.d.ts +3 -0
  96. package/dist/src/singletons/queryTimeout.d.ts.map +1 -0
  97. package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
  98. package/dist/src/subscriptions/strategies/database.test.d.ts +2 -0
  99. package/dist/src/subscriptions/strategies/database.test.d.ts.map +1 -0
  100. package/dist/src/subscriptions/types.d.ts +4 -1
  101. package/dist/src/subscriptions/types.d.ts.map +1 -1
  102. package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
  103. package/dist/src/types/env.d.ts +73 -0
  104. package/dist/src/types/env.d.ts.map +1 -1
  105. package/dist/src/types/zod/auth.d.ts +12 -0
  106. package/dist/src/types/zod/auth.d.ts.map +1 -1
  107. package/dist/src/types/zod/configuration.d.ts +28 -0
  108. package/dist/src/types/zod/configuration.d.ts.map +1 -1
  109. package/dist/src/utils/rateLimit.d.ts +65 -0
  110. package/dist/src/utils/rateLimit.d.ts.map +1 -0
  111. package/dist/src/utils/rateLimit.test.d.ts +2 -0
  112. package/dist/src/utils/rateLimit.test.d.ts.map +1 -0
  113. package/dist/src/utils/responses.d.ts +3 -0
  114. package/dist/src/utils/responses.d.ts.map +1 -1
  115. package/dist/tsconfig.tsbuildinfo +1 -1
  116. package/package.json +13 -13
  117. package/playgrounds/console/index.html +7 -7
  118. package/playgrounds/graphiql/index.html +535 -536
  119. package/playgrounds/scalar/index.html +1 -1
  120. package/src/ai/tools/core.ts +30 -6
  121. package/src/analyzeQuery/costLimit.ts +236 -0
  122. package/src/analyzeQuery/depthLimit.ts +4 -23
  123. package/src/analyzeQuery/fragments.ts +18 -0
  124. package/src/config/types/auth.ts +7 -0
  125. package/src/config/types/db.ts +7 -0
  126. package/src/config/types/operation.ts +4 -0
  127. package/src/configuration/crossReferences.ts +340 -0
  128. package/src/configuration/getSchemas/type-definition-generator/index.ts +1 -1
  129. package/src/configuration/gql/buildExecute.ts +1 -1
  130. package/src/configuration/gql/handleGraphQLRequestFactory.ts +80 -10
  131. package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +22 -2
  132. package/src/configuration/index.ts +24 -6
  133. package/src/configuration/rest/handleRESTRequestFactory.ts +6 -4
  134. package/src/configuration/rest/index.ts +11 -2
  135. package/src/console/api.ts +51 -10
  136. package/src/console/session.ts +125 -0
  137. package/src/cron/index.ts +6 -2
  138. package/src/databases/common.ts +55 -10
  139. package/src/databases/core/executor.ts +12 -2
  140. package/src/databases/core/function-mapping.ts +5 -0
  141. package/src/databases/core/query-builder.ts +7 -0
  142. package/src/databases/engines/mssql/connection.ts +66 -14
  143. package/src/databases/engines/mssql/format.ts +3 -1
  144. package/src/databases/engines/mssql/query/index.ts +12 -2
  145. package/src/databases/engines/mysql/connection.ts +30 -13
  146. package/src/databases/engines/mysql/format.ts +3 -1
  147. package/src/databases/engines/mysql/query/index.ts +25 -4
  148. package/src/databases/engines/postgresql/connection.ts +76 -15
  149. package/src/databases/engines/postgresql/format.ts +3 -1
  150. package/src/databases/engines/postgresql/query/index.ts +12 -3
  151. package/src/databases/high-level-operations.ts +13 -2
  152. package/src/index.ts +82 -13
  153. package/src/singletons/queryTimeout.ts +20 -0
  154. package/src/subscriptions/strategies/database.ts +19 -3
  155. package/src/subscriptions/types.ts +4 -1
  156. package/src/subscriptions/utils/polling.ts +6 -3
  157. package/src/types/env.ts +21 -1
  158. package/src/utils/rateLimit.ts +209 -0
  159. package/src/utils/responses.ts +10 -0
  160. package/src/ai/README.md +0 -257
@@ -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 = (query: string) => {
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 errors = validate(role.schema, parse(query), [
231
+ const document = parse(query);
232
+ const errors = validate(role.schema, document, [
222
233
  ...specifiedRules,
223
234
  depthLimitRule(maxQueryDepth),
224
235
  ]);
225
- return { hasErrors: errors.length > 0, validationErrors: errors };
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
- ASTNode,
5
- ASTVisitor,
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
+ };
@@ -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: [],
@@ -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
  /**