@graphoria/server 0.2.3 → 0.4.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 (183) hide show
  1. package/README.md +29 -20
  2. package/dist/src/ai/mcp/index.d.ts +9 -2
  3. package/dist/src/ai/mcp/index.d.ts.map +1 -1
  4. package/dist/src/ai/tools/core.d.ts +6 -1
  5. package/dist/src/ai/tools/core.d.ts.map +1 -1
  6. package/dist/src/ai/tools/core.test.d.ts +2 -0
  7. package/dist/src/ai/tools/core.test.d.ts.map +1 -0
  8. package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
  9. package/dist/src/analyzeQuery/costLimit.d.ts +30 -0
  10. package/dist/src/analyzeQuery/costLimit.d.ts.map +1 -0
  11. package/dist/src/analyzeQuery/costLimit.test.d.ts +2 -0
  12. package/dist/src/analyzeQuery/costLimit.test.d.ts.map +1 -0
  13. package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
  14. package/dist/src/analyzeQuery/fragments.d.ts +6 -0
  15. package/dist/src/analyzeQuery/fragments.d.ts.map +1 -0
  16. package/dist/src/authentication/capabilities.d.ts +18 -0
  17. package/dist/src/authentication/capabilities.d.ts.map +1 -0
  18. package/dist/src/authentication/capabilities.test.d.ts +2 -0
  19. package/dist/src/authentication/capabilities.test.d.ts.map +1 -0
  20. package/dist/src/authentication/jwt.d.ts.map +1 -1
  21. package/dist/src/authentication/paseto.d.ts.map +1 -1
  22. package/dist/src/authentication/secrets.d.ts +9 -0
  23. package/dist/src/authentication/secrets.d.ts.map +1 -0
  24. package/dist/src/authentication/secrets.test.d.ts +2 -0
  25. package/dist/src/authentication/secrets.test.d.ts.map +1 -0
  26. package/dist/src/config/types/auth.d.ts +8 -0
  27. package/dist/src/config/types/auth.d.ts.map +1 -1
  28. package/dist/src/config/types/db.d.ts +2 -0
  29. package/dist/src/config/types/db.d.ts.map +1 -1
  30. package/dist/src/config/types/operation.d.ts +4 -0
  31. package/dist/src/config/types/operation.d.ts.map +1 -1
  32. package/dist/src/config/types/operation.test.d.ts +2 -0
  33. package/dist/src/config/types/operation.test.d.ts.map +1 -0
  34. package/dist/src/configuration/crossReferences.d.ts +27 -0
  35. package/dist/src/configuration/crossReferences.d.ts.map +1 -0
  36. package/dist/src/configuration/crossReferences.test.d.ts +2 -0
  37. package/dist/src/configuration/crossReferences.test.d.ts.map +1 -0
  38. package/dist/src/configuration/getSchemas/index.d.ts +70 -6
  39. package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
  40. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +3 -1
  41. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
  42. package/dist/src/configuration/gql/gqlAuthOperations.d.ts.map +1 -1
  43. package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts +32 -2
  44. package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
  45. package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
  46. package/dist/src/configuration/index.d.ts +41 -3
  47. package/dist/src/configuration/index.d.ts.map +1 -1
  48. package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
  49. package/dist/src/configuration/rest/generateOpenAPI.test.d.ts +2 -0
  50. package/dist/src/configuration/rest/generateOpenAPI.test.d.ts.map +1 -0
  51. package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
  52. package/dist/src/configuration/rest/index.d.ts.map +1 -1
  53. package/dist/src/console/api.d.ts +7 -5
  54. package/dist/src/console/api.d.ts.map +1 -1
  55. package/dist/src/console/session.d.ts +29 -0
  56. package/dist/src/console/session.d.ts.map +1 -0
  57. package/dist/src/console/session.test.d.ts +2 -0
  58. package/dist/src/console/session.test.d.ts.map +1 -0
  59. package/dist/src/databases/common.d.ts +12 -4
  60. package/dist/src/databases/common.d.ts.map +1 -1
  61. package/dist/src/databases/core/executor.d.ts +7 -3
  62. package/dist/src/databases/core/executor.d.ts.map +1 -1
  63. package/dist/src/databases/core/function-mapping.d.ts +5 -4
  64. package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
  65. package/dist/src/databases/core/procedure-arguments.d.ts +17 -0
  66. package/dist/src/databases/core/procedure-arguments.d.ts.map +1 -0
  67. package/dist/src/databases/core/procedure-arguments.test.d.ts +2 -0
  68. package/dist/src/databases/core/procedure-arguments.test.d.ts.map +1 -0
  69. package/dist/src/databases/core/query-builder.d.ts +7 -1
  70. package/dist/src/databases/core/query-builder.d.ts.map +1 -1
  71. package/dist/src/databases/engines/mssql/connection.d.ts +30 -7
  72. package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
  73. package/dist/src/databases/engines/mssql/connection.test.d.ts +2 -0
  74. package/dist/src/databases/engines/mssql/connection.test.d.ts.map +1 -0
  75. package/dist/src/databases/engines/mssql/format.d.ts +2 -1
  76. package/dist/src/databases/engines/mssql/format.d.ts.map +1 -1
  77. package/dist/src/databases/engines/mssql/query/index.d.ts +3 -2
  78. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  79. package/dist/src/databases/engines/mysql/connection.d.ts +17 -1
  80. package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
  81. package/dist/src/databases/engines/mysql/connection.test.d.ts +2 -0
  82. package/dist/src/databases/engines/mysql/connection.test.d.ts.map +1 -0
  83. package/dist/src/databases/engines/mysql/format.d.ts +2 -1
  84. package/dist/src/databases/engines/mysql/format.d.ts.map +1 -1
  85. package/dist/src/databases/engines/mysql/query/index.d.ts +3 -2
  86. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  87. package/dist/src/databases/engines/postgresql/connection.d.ts +26 -7
  88. package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
  89. package/dist/src/databases/engines/postgresql/connection.test.d.ts +2 -0
  90. package/dist/src/databases/engines/postgresql/connection.test.d.ts.map +1 -0
  91. package/dist/src/databases/engines/postgresql/format.d.ts +2 -1
  92. package/dist/src/databases/engines/postgresql/format.d.ts.map +1 -1
  93. package/dist/src/databases/engines/postgresql/query/index.d.ts +3 -2
  94. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  95. package/dist/src/databases/high-level-operations.d.ts +2 -0
  96. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  97. package/dist/src/databases/transformers/data-transformers.d.ts +2 -0
  98. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  99. package/dist/src/index.d.ts.map +1 -1
  100. package/dist/src/logging/audit.d.ts +27 -0
  101. package/dist/src/logging/audit.d.ts.map +1 -0
  102. package/dist/src/logging/audit.test.d.ts +2 -0
  103. package/dist/src/logging/audit.test.d.ts.map +1 -0
  104. package/dist/src/singletons/env.d.ts +27 -4
  105. package/dist/src/singletons/env.d.ts.map +1 -1
  106. package/dist/src/singletons/queryTimeout.d.ts +3 -0
  107. package/dist/src/singletons/queryTimeout.d.ts.map +1 -0
  108. package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
  109. package/dist/src/subscriptions/strategies/database.test.d.ts +2 -0
  110. package/dist/src/subscriptions/strategies/database.test.d.ts.map +1 -0
  111. package/dist/src/subscriptions/types.d.ts +4 -1
  112. package/dist/src/subscriptions/types.d.ts.map +1 -1
  113. package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
  114. package/dist/src/types/env.d.ts +97 -12
  115. package/dist/src/types/env.d.ts.map +1 -1
  116. package/dist/src/types/zod/auth.d.ts +12 -0
  117. package/dist/src/types/zod/auth.d.ts.map +1 -1
  118. package/dist/src/types/zod/configuration.d.ts +28 -0
  119. package/dist/src/types/zod/configuration.d.ts.map +1 -1
  120. package/dist/src/utils/rateLimit.d.ts +65 -0
  121. package/dist/src/utils/rateLimit.d.ts.map +1 -0
  122. package/dist/src/utils/rateLimit.test.d.ts +2 -0
  123. package/dist/src/utils/rateLimit.test.d.ts.map +1 -0
  124. package/dist/src/utils/responses.d.ts +6 -0
  125. package/dist/src/utils/responses.d.ts.map +1 -1
  126. package/dist/src/utils/sessionVariables.d.ts +2 -0
  127. package/dist/src/utils/sessionVariables.d.ts.map +1 -1
  128. package/dist/tsconfig.tsbuildinfo +1 -1
  129. package/package.json +5 -5
  130. package/playgrounds/console/index.html +7 -7
  131. package/playgrounds/graphiql/index.html +548 -685
  132. package/src/ai/mcp/index.ts +19 -14
  133. package/src/ai/tools/core.ts +30 -6
  134. package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +2 -1
  135. package/src/analyzeQuery/costLimit.ts +236 -0
  136. package/src/analyzeQuery/depthLimit.ts +4 -23
  137. package/src/analyzeQuery/fragments.ts +18 -0
  138. package/src/authentication/capabilities.ts +54 -0
  139. package/src/authentication/index.ts +3 -3
  140. package/src/authentication/jwt.ts +30 -25
  141. package/src/authentication/paseto.ts +36 -26
  142. package/src/authentication/secrets.ts +30 -0
  143. package/src/config/types/auth.ts +7 -0
  144. package/src/config/types/db.ts +7 -0
  145. package/src/config/types/operation.ts +4 -0
  146. package/src/configuration/crossReferences.ts +340 -0
  147. package/src/configuration/getSchemas/mergeEntities/index.ts +0 -26
  148. package/src/configuration/getSchemas/type-definition-generator/index.ts +1 -1
  149. package/src/configuration/gql/buildExecute.ts +1 -1
  150. package/src/configuration/gql/gqlAuthOperations.ts +26 -0
  151. package/src/configuration/gql/handleGraphQLRequestFactory.ts +97 -15
  152. package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +31 -2
  153. package/src/configuration/index.ts +24 -6
  154. package/src/configuration/rest/generateOpenAPI.ts +61 -11
  155. package/src/configuration/rest/handleRESTRequestFactory.ts +32 -4
  156. package/src/configuration/rest/index.ts +11 -2
  157. package/src/console/api.ts +102 -16
  158. package/src/console/session.ts +129 -0
  159. package/src/databases/common.ts +55 -10
  160. package/src/databases/core/executor.ts +13 -4
  161. package/src/databases/core/function-mapping.ts +5 -1
  162. package/src/databases/core/procedure-arguments.ts +25 -0
  163. package/src/databases/core/query-builder.ts +7 -0
  164. package/src/databases/engines/mssql/connection.ts +66 -17
  165. package/src/databases/engines/mssql/format.ts +3 -1
  166. package/src/databases/engines/mssql/query/index.ts +12 -2
  167. package/src/databases/engines/mysql/connection.ts +40 -20
  168. package/src/databases/engines/mysql/format.ts +3 -1
  169. package/src/databases/engines/mysql/query/index.ts +25 -4
  170. package/src/databases/engines/postgresql/connection.ts +86 -22
  171. package/src/databases/engines/postgresql/format.ts +3 -1
  172. package/src/databases/engines/postgresql/query/index.ts +12 -3
  173. package/src/databases/high-level-operations.ts +13 -2
  174. package/src/index.ts +127 -19
  175. package/src/logging/audit.ts +86 -0
  176. package/src/singletons/queryTimeout.ts +20 -0
  177. package/src/subscriptions/strategies/database.ts +19 -3
  178. package/src/subscriptions/types.ts +4 -1
  179. package/src/subscriptions/utils/polling.ts +6 -3
  180. package/src/types/env.ts +47 -9
  181. package/src/utils/rateLimit.ts +209 -0
  182. package/src/utils/responses.ts +16 -0
  183. package/src/utils/sessionVariables.ts +2 -0
@@ -6,32 +6,78 @@ import type { VariableDefinition } from "../../../analyzeQuery/types";
6
6
  import type { Database } from "../../../types/configuration";
7
7
  import type { ProcedureResolver } from "../../../types/db";
8
8
 
9
+ import { MSSQLConnectionOptionsZod } from "../../../config/types/db";
9
10
  import { databasesConnections } from "../../../singletons/databases";
11
+ import { getQueryTimeoutMs } from "../../../singletons/queryTimeout";
10
12
  import { logger } from "../../../logging";
11
13
 
12
- export const getPool = async (db: Database) => {
14
+ // Every pool bound comes from the schema, which is the only place they are
15
+ // declared and documented. Repeating them here as `?? n` fallbacks let the
16
+ // documented default and the applied one drift apart, differently per engine.
17
+ //
18
+ // Defaults are overlaid rather than re-parsed: `connectionOptions` is an
19
+ // undiscriminated union of the two engine shapes, and an object carrying
20
+ // nothing MSSQL-specific — `{}` included — validates against the Bun SQL shape
21
+ // even on an MSSQL database. A strict re-parse of one of those would reject the
22
+ // Bun SQL keys and take the server down at boot.
23
+ export const poolOptions = (db: Database, timeoutMs: number = getQueryTimeoutMs()) => {
13
24
  const ci = db.connection;
14
- const opts = db.connectionOptions as MSSQLConnectionOptions | undefined;
15
- const pool = new ConnectionPool({
25
+ const raw = db.connectionOptions as MSSQLConnectionOptions | undefined;
26
+
27
+ const defaults = MSSQLConnectionOptionsZod.parse({});
28
+ const opts = { ...defaults, ...raw, pool: { ...defaults.pool, ...raw?.pool } };
29
+
30
+ return {
16
31
  server: ci.host,
17
32
  port: ci.port,
18
33
  user: ci.user,
19
34
  password: ci.password,
20
35
  database: ci.database,
21
- connectionTimeout: opts?.connectionTimeout ? opts.connectionTimeout * 1000 : undefined,
22
- requestTimeout: opts?.requestTimeout ? opts.requestTimeout * 1000 : undefined,
36
+ connectionTimeout: opts.connectionTimeout * 1000,
37
+ // Only an omitted key falls through to the resolved timeout: reading it off
38
+ // `opts` cannot tell an operator who set 30 from Zod defaulting to 30, and
39
+ // silently overriding the first would break the slow report it was raised
40
+ // for. Seconds there, milliseconds here.
41
+ requestTimeout: raw?.requestTimeout !== undefined ? raw.requestTimeout * 1000 : timeoutMs,
42
+ // These three keep their code fallbacks rather than the schema's. Two of
43
+ // them default the opposite way there, so taking the schema value would
44
+ // turn certificate validation on and trusted connections off for every
45
+ // deployment that omits `connectionOptions` — a change to how a connection
46
+ // is secured, not to how the pool is sized.
23
47
  options: {
24
- encrypt: opts?.encrypt ?? false,
25
- trustServerCertificate: opts?.trustServerCertificate ?? true,
26
- trustedConnection: opts?.trustedConnection ?? true,
48
+ encrypt: raw?.encrypt ?? false,
49
+ trustServerCertificate: raw?.trustServerCertificate ?? true,
50
+ trustedConnection: raw?.trustedConnection ?? true,
27
51
  },
28
52
  pool: {
29
- max: opts?.pool?.max ?? 50,
30
- min: opts?.pool?.min ?? 1,
31
- idleTimeoutMillis: (opts?.pool?.idleTimeout ?? 30) * 1000,
53
+ max: opts.pool.max,
54
+ min: opts.pool.min,
55
+ idleTimeoutMillis: opts.pool.idleTimeout * 1000,
56
+ acquireTimeoutMillis: opts.pool.acquireTimeout * 1000,
32
57
  },
33
- parseJSON: opts?.parseJSON ?? true,
34
- });
58
+ parseJSON: opts.parseJSON,
59
+ };
60
+ };
61
+
62
+ /** The bound the pool already applies, so the executor can tell an override from the default. */
63
+ export const poolRequestTimeoutMs = (db: Database) => poolOptions(db).requestTimeout;
64
+
65
+ // `ConnectionPool.request` takes per-request overrides from mssql 12.7.0, which
66
+ // is what the workspace pins, but `@types/mssql@12.3.0` still declares it with
67
+ // no parameter. Narrowed to the one key used rather than cast wholesale, so the
68
+ // day the types catch up this stops compiling and can be deleted.
69
+ //
70
+ // Not interchangeable with assigning `request.timeout`: that property does not
71
+ // exist, and the pool default is used instead.
72
+ type PoolWithRequestOverrides = {
73
+ request(overrides: { requestTimeout: number }): ReturnType<ConnectionPool["request"]>;
74
+ };
75
+
76
+ const requestWithTimeout = (pool: ConnectionPool, timeoutMs: number) =>
77
+ (pool as unknown as PoolWithRequestOverrides).request({ requestTimeout: timeoutMs });
78
+
79
+ export const getPool = async (db: Database) => {
80
+ const pool = new ConnectionPool(poolOptions(db));
35
81
 
36
82
  await pool.connect(); // Connect to the database
37
83
 
@@ -52,6 +98,7 @@ export const executeQueryFactory =
52
98
  db: Database,
53
99
  variablesDefinition: VariableDefinition[] = [],
54
100
  params: Record<string, unknown> = {},
101
+ timeoutMs?: number,
55
102
  ) => {
56
103
  const pool = singleQuery ? await getPool(db) : await getPoolSingleton(db);
57
104
 
@@ -59,7 +106,10 @@ export const executeQueryFactory =
59
106
  await new Promise((resolve) => setTimeout(resolve, 1000));
60
107
  }
61
108
 
62
- const request = pool!.request();
109
+ const request =
110
+ timeoutMs !== undefined && timeoutMs !== poolRequestTimeoutMs(db)
111
+ ? requestWithTimeout(pool!, timeoutMs)
112
+ : pool!.request();
63
113
 
64
114
  variablesDefinition.forEach((v, i) => {
65
115
  const varName = (i + 1).toString();
@@ -93,12 +143,14 @@ export const executeQueryJSONFactory =
93
143
  db: Database,
94
144
  variablesDefinition: VariableDefinition[] = [],
95
145
  params: Record<string, unknown> = {},
146
+ timeoutMs?: number,
96
147
  ): Promise<T> => {
97
148
  const result = await (singleQuery ? executeQuerySingle : executeQuery)(
98
149
  query,
99
150
  db,
100
151
  variablesDefinition,
101
152
  params,
153
+ timeoutMs,
102
154
  );
103
155
 
104
156
  return result[0] as T;
@@ -109,7 +161,6 @@ export const executeQueryJSONSingle = executeQueryJSONFactory(true);
109
161
 
110
162
  export const callStoredProcedure = async (
111
163
  sp: ProcedureResolver,
112
- variablesDefinition: VariableDefinition[],
113
164
  variables: Record<string, unknown> = {},
114
165
  ) => {
115
166
  try {
@@ -118,8 +169,6 @@ export const callStoredProcedure = async (
118
169
  const request = pool!.request();
119
170
 
120
171
  for (const [key, value] of Object.entries(variables)) {
121
- if (!value) continue;
122
-
123
172
  const paramFound = sp.parameters.find((p) => p.name === `@${key}`);
124
173
 
125
174
  if (paramFound?.dataType === "varchar") {
@@ -2,6 +2,7 @@ import { format as sqlFormatter } from "sql-formatter";
2
2
 
3
3
  import type { AnalysisResult } from "../../../analyzeQuery/types";
4
4
  import type { MergedEntities } from "../../../configuration/getSchemas/mergeEntities";
5
+ import type { PageLimits } from "../../common";
5
6
 
6
7
  import { generateSQL } from "../../core/query-builder";
7
8
 
@@ -16,8 +17,9 @@ export const genSql = (
16
17
  query: AnalysisResult,
17
18
  variables: Record<string, unknown> = {},
18
19
  hash = false,
20
+ pageLimits: PageLimits | null = null,
19
21
  ) => {
20
- const [[, sql]] = generateSQL(entitites, query, variables, hash);
22
+ const [[, sql]] = generateSQL(entitites, query, variables, hash, pageLimits);
21
23
 
22
24
  return format(sql);
23
25
  };
@@ -4,7 +4,7 @@ import type {
4
4
  VariableDefinition,
5
5
  } from "../../../../analyzeQuery/types";
6
6
  import type { MergedEntities } from "../../../../configuration/getSchemas/mergeEntities";
7
- import type { GroupByInfo } from "../../../common";
7
+ import type { GroupByInfo, PageLimits } from "../../../common";
8
8
 
9
9
  import {
10
10
  buildOrderByClauseMSSQL,
@@ -164,6 +164,7 @@ export const generateSQL = (
164
164
  operation: OperationAnalysis,
165
165
  variables: Record<string, unknown> = {},
166
166
  forHashMethod: boolean = false,
167
+ pageLimits: PageLimits | null = null,
167
168
  ): string => {
168
169
  if (forHashMethod) {
169
170
  return `SELECT HASHBYTES('MD5', (
@@ -176,6 +177,7 @@ export const generateSQL = (
176
177
  null,
177
178
  1,
178
179
  {},
180
+ pageLimits,
179
181
  )})
180
182
  ) AS ResultHash`;
181
183
  }
@@ -238,6 +240,7 @@ export const generateSQL = (
238
240
  null,
239
241
  index + 1,
240
242
  {},
243
+ pageLimits,
241
244
  );
242
245
 
243
246
  fieldQueries.push(`(${fieldSQL}) as ${wrapIdentifierMSSQL(field.alias || field.name)}`);
@@ -261,6 +264,7 @@ export const buildSQLForField = (
261
264
  parentTableAlias: string | null,
262
265
  level: number,
263
266
  aliasMap: { [alias: string]: string },
267
+ pageLimits: PageLimits | null,
264
268
  ): string => {
265
269
  const tableAlias = generateTableAlias(level);
266
270
 
@@ -325,6 +329,7 @@ export const buildSQLForField = (
325
329
  tableAlias,
326
330
  level,
327
331
  aliasMap,
332
+ pageLimits,
328
333
  ),
329
334
  ([name, selector]) => `${selector} AS ${wrapIdentifierMSSQL(name)}`,
330
335
  );
@@ -332,7 +337,12 @@ export const buildSQLForField = (
332
337
  const fromClause = `FROM ${dottedQuotedName} ${tableAlias}`;
333
338
 
334
339
  const orderByClause = buildOrderByClauseMSSQL(entities, field, tableAlias);
335
- const paginationClause = buildPaginationClauseMSSQL(field, variablesDefinition);
340
+ const paginationClause = buildPaginationClauseMSSQL(
341
+ field,
342
+ variablesDefinition,
343
+ variables,
344
+ pageLimits,
345
+ );
336
346
 
337
347
  const isArray = field.isArray;
338
348
  const forJson =
@@ -5,27 +5,45 @@ import type { VariableDefinition } from "../../../analyzeQuery/types";
5
5
  import type { Database } from "../../../types/configuration";
6
6
  import type { ProcedureResolver } from "../../../types/db";
7
7
 
8
+ import { BunSQLConnectionOptionsZod } from "../../../config/types/db";
8
9
  import { databasesConnections } from "../../../singletons/databases";
10
+ import { orderProcedureArguments } from "../../core/procedure-arguments";
9
11
  import { toMySQLPlaceholders } from "./placeholders";
10
12
 
11
- export const getPool = async (db: Database) => {
12
- const opts = db.connectionOptions as BunSQLConnectionOptions | undefined;
13
- const pool = new SQL({
13
+ // Every pool bound comes from the schema, which is the only place they are
14
+ // declared and documented. Repeating them here as `?? n` fallbacks let the
15
+ // documented default and the applied one drift apart, differently per engine.
16
+ //
17
+ // Defaults are overlaid rather than re-parsed: `connectionOptions` is an
18
+ // undiscriminated union of the two engine shapes, so an object can arrive
19
+ // already validated against the other one, and a strict re-parse would reject
20
+ // it at boot.
21
+ export const poolOptions = (db: Database) => {
22
+ const opts = {
23
+ ...BunSQLConnectionOptionsZod.parse({}),
24
+ ...(db.connectionOptions as BunSQLConnectionOptions | undefined),
25
+ };
26
+
27
+ return {
14
28
  host: db.connection.host,
15
29
  port: db.connection.port,
16
30
  username: db.connection.user,
17
31
  password: db.connection.password,
18
32
  database: db.connection.database,
19
- adapter: "mysql",
20
- max: opts?.max ?? 50,
21
- idleTimeout: opts?.idleTimeout ?? 30,
22
- connectionTimeout: opts?.connectionTimeout,
23
- maxLifetime: opts?.maxLifetime,
24
- tls: opts?.tls,
25
- allowPublicKeyRetrieval: opts?.allowPublicKeyRetrieval,
26
- prepare: opts?.prepare,
27
- bigint: opts?.bigint,
28
- });
33
+ adapter: "mysql" as const,
34
+ max: opts.max,
35
+ idleTimeout: opts.idleTimeout,
36
+ connectionTimeout: opts.connectionTimeout,
37
+ maxLifetime: opts.maxLifetime,
38
+ tls: opts.tls,
39
+ allowPublicKeyRetrieval: opts.allowPublicKeyRetrieval,
40
+ prepare: opts.prepare,
41
+ bigint: opts.bigint,
42
+ };
43
+ };
44
+
45
+ export const getPool = async (db: Database) => {
46
+ const pool = new SQL(poolOptions(db));
29
47
 
30
48
  await pool.connect(); // Connect to the database
31
49
 
@@ -80,22 +98,24 @@ export const executeQueryJSONFactory =
80
98
  export const executeQueryJSON = executeQueryJSONFactory();
81
99
  export const executeQueryJSONSingle = executeQueryJSONFactory(true);
82
100
 
101
+ // The generated schema types every stored procedure mutation as `Boolean!`, so
102
+ // the call reports whether it ran, not what it selected. Returning the rows here
103
+ // would put an array behind a field the schema promises is a boolean.
83
104
  export const callStoredProcedure = async (
84
105
  sp: ProcedureResolver,
85
- variablesDefinition: VariableDefinition[],
86
106
  variables: Record<string, unknown>,
87
107
  ) => {
88
108
  try {
89
- const data = await executeQuery(
90
- `CALL ${sp.dottedQuotedName}(${Object.keys(variables)
91
- .map((_, i) => `$${i + 1}`)
92
- .join(", ")});`,
109
+ const args = orderProcedureArguments(sp, variables);
110
+
111
+ await executeQuery(
112
+ `CALL ${sp.dottedQuotedName}(${args.map((_, i) => `$${i + 1}`).join(", ")});`,
93
113
  sp.db!,
94
- variablesDefinition,
114
+ args,
95
115
  variables,
96
116
  );
97
117
 
98
- return data;
118
+ return true;
99
119
  } catch {
100
120
  return false;
101
121
  }
@@ -2,6 +2,7 @@ import { format as sqlFormatter } from "sql-formatter";
2
2
 
3
3
  import type { AnalysisResult } from "../../../analyzeQuery/types";
4
4
  import type { MergedEntities } from "../../../configuration/getSchemas/mergeEntities";
5
+ import type { PageLimits } from "../../common";
5
6
 
6
7
  import { generateSQL } from "../../core/query-builder";
7
8
 
@@ -16,8 +17,9 @@ export const genSql = (
16
17
  query: AnalysisResult,
17
18
  variables: Record<string, unknown> = {},
18
19
  hash = false,
20
+ pageLimits: PageLimits | null = null,
19
21
  ) => {
20
- const [[, sql]] = generateSQL(entitites, query, variables, hash);
22
+ const [[, sql]] = generateSQL(entitites, query, variables, hash, pageLimits);
21
23
 
22
24
  return format(sql);
23
25
  };
@@ -4,7 +4,7 @@ import type {
4
4
  VariableDefinition,
5
5
  } from "../../../../analyzeQuery/types";
6
6
  import type { MergedEntities } from "../../../../configuration/getSchemas/mergeEntities";
7
- import type { GroupByInfo } from "../../../common";
7
+ import type { GroupByInfo, PageLimits } from "../../../common";
8
8
 
9
9
  import {
10
10
  buildOrderByClauseMySQL,
@@ -21,6 +21,7 @@ import {
21
21
  } from "../../../common";
22
22
 
23
23
  import { applyDirectives } from "../../../directives";
24
+ import { getQueryTimeoutMs } from "../../../../singletons/queryTimeout";
24
25
 
25
26
  // Generate CTE for aggregations
26
27
  const buildAggregationCTE = (
@@ -157,9 +158,21 @@ export const generateSQL = (
157
158
  operation: OperationAnalysis,
158
159
  variables: Record<string, unknown> = {},
159
160
  forHashMethod: boolean = false,
161
+ pageLimits: PageLimits | null = null,
162
+ timeoutMs?: number,
160
163
  ): string => {
164
+ // Bun's MySQL adapter ignores the `connection` bag and exposes no
165
+ // per-connection init hook, so there is no pool-level route for
166
+ // max_execution_time: the bound has to ride in the SQL text. MySQL honours the
167
+ // hint only on the outermost SELECT — one placed inside a CTE is silently
168
+ // discarded — which is why it is emitted here, at each return, rather than
169
+ // spliced into the finished string by the executor.
170
+ const effectiveTimeoutMs = timeoutMs ?? getQueryTimeoutMs();
171
+ const timeoutHint =
172
+ effectiveTimeoutMs > 0 ? ` /*+ MAX_EXECUTION_TIME(${effectiveTimeoutMs}) */` : "";
173
+
161
174
  if (forHashMethod) {
162
- return `SELECT MD5((${buildSQLForField(entities, operation.variables ?? [], variables, operation.fields[0], null, null, 1, {})})) AS ResultHash`;
175
+ return `SELECT${timeoutHint} MD5((${buildSQLForField(entities, operation.variables ?? [], variables, operation.fields[0], null, null, 1, {}, pageLimits)})) AS ResultHash`;
163
176
  }
164
177
 
165
178
  const variablesWithDefault = {
@@ -220,6 +233,7 @@ export const generateSQL = (
220
233
  null,
221
234
  index + 1,
222
235
  {},
236
+ pageLimits,
223
237
  );
224
238
 
225
239
  fieldQueries.push(`'${field.alias || field.name}', ${fieldSQL}`);
@@ -228,7 +242,7 @@ export const generateSQL = (
228
242
  const cteClause = ctes.length > 0 ? `WITH\n${ctes.join(",\n")}\n` : "";
229
243
 
230
244
  return `
231
- ${cteClause}SELECT JSON_OBJECT(
245
+ ${cteClause}SELECT${timeoutHint} JSON_OBJECT(
232
246
  ${fieldQueries.join(",\n")}
233
247
  ) as json_result`;
234
248
  };
@@ -242,6 +256,7 @@ export const buildSQLForField = (
242
256
  parentTableAlias: string | null,
243
257
  level: number,
244
258
  aliasMap: { [alias: string]: string },
259
+ pageLimits: PageLimits | null,
245
260
  ): string => {
246
261
  const tableAlias = generateTableAlias(level);
247
262
 
@@ -306,6 +321,7 @@ export const buildSQLForField = (
306
321
  tableAlias,
307
322
  level,
308
323
  aliasMap,
324
+ pageLimits,
309
325
  ),
310
326
  ([name, selector]) => `'${name}', ${selector}`,
311
327
  );
@@ -313,7 +329,12 @@ export const buildSQLForField = (
313
329
  const fromClause = `FROM ${dottedQuotedName} ${tableAlias}`;
314
330
 
315
331
  const orderByClause = buildOrderByClauseMySQL(entities, field, tableAlias);
316
- const paginationClause = buildPaginationClauseMySQL(field, variablesDefinition);
332
+ const paginationClause = buildPaginationClauseMySQL(
333
+ field,
334
+ variablesDefinition,
335
+ variables,
336
+ pageLimits,
337
+ );
317
338
 
318
339
  const isArraySelection = !!field.isArray && !withoutArrayWrapper;
319
340
 
@@ -5,24 +5,52 @@ import type { VariableDefinition } from "../../../analyzeQuery/types";
5
5
  import type { Database } from "../../../types/configuration";
6
6
  import type { ProcedureResolver } from "../../../types/db";
7
7
 
8
+ import { BunSQLConnectionOptionsZod } from "../../../config/types/db";
8
9
  import { databasesConnections } from "../../../singletons/databases";
10
+ import { orderProcedureArguments } from "../../core/procedure-arguments";
11
+ import { getQueryTimeoutMs } from "../../../singletons/queryTimeout";
12
+
13
+ // Every pool bound comes from the schema, which is the only place they are
14
+ // declared and documented. Repeating them here as `?? n` fallbacks let the
15
+ // documented default and the applied one drift apart, differently per engine.
16
+ //
17
+ // Defaults are overlaid rather than re-parsed: `connectionOptions` is an
18
+ // undiscriminated union of the two engine shapes, so an object can arrive
19
+ // already validated against the other one, and a strict re-parse would reject
20
+ // it at boot.
21
+ export const poolOptions = (db: Database, timeoutMs: number = getQueryTimeoutMs()) => {
22
+ const opts = {
23
+ ...BunSQLConnectionOptionsZod.parse({}),
24
+ ...(db.connectionOptions as BunSQLConnectionOptions | undefined),
25
+ };
9
26
 
10
- export const getPool = async (db: Database) => {
11
- const opts = db.connectionOptions as BunSQLConnectionOptions | undefined;
12
- const pool = new SQL({
27
+ return {
13
28
  host: db.connection.host,
14
29
  port: db.connection.port,
15
30
  user: db.connection.user,
16
31
  password: db.connection.password,
17
32
  database: db.connection.database,
18
- max: opts?.max ?? 5,
19
- idleTimeout: opts?.idleTimeout ?? 30,
20
- connectionTimeout: opts?.connectionTimeout,
21
- maxLifetime: opts?.maxLifetime,
22
- tls: opts?.tls,
23
- prepare: opts?.prepare,
24
- bigint: opts?.bigint,
25
- });
33
+ max: opts.max,
34
+ idleTimeout: opts.idleTimeout,
35
+ connectionTimeout: opts.connectionTimeout,
36
+ maxLifetime: opts.maxLifetime,
37
+ tls: opts.tls,
38
+ prepare: opts.prepare,
39
+ bigint: opts.bigint,
40
+ // Applied once per pooled connection rather than per query, so it bounds
41
+ // everything that runs on the connection — auth and introspection included
42
+ // — at no per-query cost. Omitted at 0 rather than sent as "0", which
43
+ // Postgres reads as unlimited but still costs a round trip to set.
44
+ ...(timeoutMs > 0 ? { connection: { statement_timeout: String(timeoutMs) } } : {}),
45
+ };
46
+ };
47
+
48
+ /** The bound the pool already applies, so the executor can tell an override from the default. */
49
+ export const poolStatementTimeoutMs = (db: Database) =>
50
+ Number(poolOptions(db).connection?.statement_timeout ?? 0);
51
+
52
+ export const getPool = async (db: Database) => {
53
+ const pool = new SQL(poolOptions(db));
26
54
 
27
55
  await pool.connect(); // Connect to the database
28
56
 
@@ -31,6 +59,35 @@ export const getPool = async (db: Database) => {
31
59
 
32
60
  export const getPoolSingleton = async (db: Database) => databasesConnections[db.name] as SQL;
33
61
 
62
+ // A timeout other than the pool's own has to be set on a connection, which
63
+ // means pinning one for the duration. Only an override pays for that.
64
+ const executeReserved = async <T>(
65
+ pool: SQL,
66
+ timeoutMs: number,
67
+ query: string,
68
+ params: unknown[],
69
+ ) => {
70
+ // SET takes no bind parameters, so the value is interpolated. It is an
71
+ // integer from the config schema; re-checked here so a widened type upstream
72
+ // cannot turn this line into an injection point.
73
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 0) {
74
+ throw new Error(`Invalid statement timeout: ${timeoutMs}`);
75
+ }
76
+
77
+ const reserved = await pool.reserve();
78
+
79
+ try {
80
+ await reserved.unsafe(`SET statement_timeout = ${timeoutMs}`);
81
+
82
+ return await reserved.unsafe<T>(query, params);
83
+ } finally {
84
+ // The connection goes back to the pool carrying the pool's default again,
85
+ // not this query's override.
86
+ await reserved.unsafe("RESET statement_timeout");
87
+ await reserved.release();
88
+ }
89
+ };
90
+
34
91
  export const executeQueryFactory =
35
92
  (singleQuery = false) =>
36
93
  async <T>(
@@ -38,13 +95,16 @@ export const executeQueryFactory =
38
95
  db: Database,
39
96
  variablesDefinition: VariableDefinition[],
40
97
  values: Record<string, unknown> = {},
98
+ timeoutMs?: number,
41
99
  ) => {
42
100
  const pool = singleQuery ? await getPool(db) : await getPoolSingleton(db);
101
+ const params = variablesDefinition.map((v) => values[v.name]);
43
102
 
44
- const result = await pool.unsafe<T>(
45
- query,
46
- variablesDefinition.map((v) => values[v.name]),
47
- );
103
+ const override = timeoutMs !== undefined && timeoutMs !== poolStatementTimeoutMs(db);
104
+
105
+ const result = override
106
+ ? await executeReserved<T>(pool, timeoutMs, query, params)
107
+ : await pool.unsafe<T>(query, params);
48
108
 
49
109
  if (singleQuery) {
50
110
  await pool.close();
@@ -63,12 +123,14 @@ export const executeQueryJSONFactory =
63
123
  db: Database,
64
124
  variablesDefinition: VariableDefinition[] = [],
65
125
  values: Record<string, unknown> = {},
126
+ timeoutMs?: number,
66
127
  ) => {
67
128
  const result = await (singleQuery ? executeQuerySingle : executeQuery)<[{ json_result: T }]>(
68
129
  query,
69
130
  db,
70
131
  variablesDefinition,
71
132
  values,
133
+ timeoutMs,
72
134
  );
73
135
 
74
136
  return result[0].json_result as T;
@@ -77,22 +139,24 @@ export const executeQueryJSONFactory =
77
139
  export const executeQueryJSON = executeQueryJSONFactory();
78
140
  export const executeQueryJSONSingle = executeQueryJSONFactory(true);
79
141
 
142
+ // The generated schema types every stored procedure mutation as `Boolean!`, so
143
+ // the call reports whether it ran, not what it selected. Returning the rows here
144
+ // would put an array behind a field the schema promises is a boolean.
80
145
  export const callStoredProcedure = async (
81
146
  sp: ProcedureResolver,
82
- variablesDefinition: VariableDefinition[],
83
147
  variables: Record<string, unknown>,
84
148
  ) => {
85
149
  try {
86
- const data = await executeQuery(
87
- `SELECT * FROM ${sp.dottedQuotedName}(${Object.keys(variables)
88
- .map((_, i) => `$${i + 1}`)
89
- .join(", ")});`,
150
+ const args = orderProcedureArguments(sp, variables);
151
+
152
+ await executeQuery(
153
+ `SELECT * FROM ${sp.dottedQuotedName}(${args.map((_, i) => `$${i + 1}`).join(", ")});`,
90
154
  sp.db!,
91
- variablesDefinition,
155
+ args,
92
156
  variables,
93
157
  );
94
158
 
95
- return data;
159
+ return true;
96
160
  } catch {
97
161
  return false;
98
162
  }
@@ -2,6 +2,7 @@ import { format as sqlFormatter } from "sql-formatter";
2
2
 
3
3
  import type { AnalysisResult } from "../../../analyzeQuery/types";
4
4
  import type { MergedEntities } from "../../../configuration/getSchemas/mergeEntities";
5
+ import type { PageLimits } from "../../common";
5
6
 
6
7
  import { generateSQL } from "../../core/query-builder";
7
8
 
@@ -16,8 +17,9 @@ export const genSql = (
16
17
  query: AnalysisResult,
17
18
  variables: Record<string, unknown> = {},
18
19
  hash = false,
20
+ pageLimits: PageLimits | null = null,
19
21
  ) => {
20
- const [[, sql]] = generateSQL(entitites, query, variables, hash);
22
+ const [[, sql]] = generateSQL(entitites, query, variables, hash, pageLimits);
21
23
 
22
24
  return format(sql);
23
25
  };
@@ -4,7 +4,7 @@ import type {
4
4
  VariableDefinition,
5
5
  } from "../../../../analyzeQuery/types";
6
6
  import type { MergedEntities } from "../../../../configuration/getSchemas/mergeEntities";
7
- import type { GroupByInfo } from "../../../common";
7
+ import type { GroupByInfo, PageLimits } from "../../../common";
8
8
 
9
9
  import {
10
10
  buildOrderByClausePG,
@@ -154,9 +154,10 @@ export const generateSQL = (
154
154
  operation: OperationAnalysis,
155
155
  variables: Record<string, unknown> = {},
156
156
  forHashMethod: boolean = false,
157
+ pageLimits: PageLimits | null = null,
157
158
  ): string => {
158
159
  if (forHashMethod) {
159
- return `SELECT MD5((${buildSQLForField(entities, operation.variables ?? [], variables, operation.fields[0], null, null, 1, {})})::text) AS "ResultHash"`;
160
+ return `SELECT MD5((${buildSQLForField(entities, operation.variables ?? [], variables, operation.fields[0], null, null, 1, {}, pageLimits)})::text) AS "ResultHash"`;
160
161
  }
161
162
 
162
163
  const variablesWithDefault = {
@@ -217,6 +218,7 @@ export const generateSQL = (
217
218
  null,
218
219
  index + 1,
219
220
  {},
221
+ pageLimits,
220
222
  );
221
223
 
222
224
  fieldQueries.push(`'${field.alias || field.name}', ${fieldSQL}`);
@@ -239,6 +241,7 @@ export const buildSQLForField = (
239
241
  parentTableAlias: string | null,
240
242
  level: number,
241
243
  aliasMap: { [alias: string]: string },
244
+ pageLimits: PageLimits | null,
242
245
  ): string => {
243
246
  const tableAlias = generateTableAlias(level);
244
247
 
@@ -303,6 +306,7 @@ export const buildSQLForField = (
303
306
  tableAlias,
304
307
  level,
305
308
  aliasMap,
309
+ pageLimits,
306
310
  ),
307
311
  ([name, selector]) => `'${name}', ${selector}`,
308
312
  );
@@ -310,7 +314,12 @@ export const buildSQLForField = (
310
314
  const fromClause = `FROM ${dottedQuotedName} ${tableAlias}`;
311
315
 
312
316
  const orderByClause = buildOrderByClausePG(entities, field, tableAlias);
313
- const paginationClause = buildPaginationClausePG(field, variablesDefinition);
317
+ const paginationClause = buildPaginationClausePG(
318
+ field,
319
+ variablesDefinition,
320
+ variables,
321
+ pageLimits,
322
+ );
314
323
 
315
324
  const isArraySelection = !!field.isArray && !withoutArrayWrapper;
316
325