@graphoria/server 0.2.0 → 0.2.2

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 (186) hide show
  1. package/dist/src/ai/agent/providers/ollama.d.ts.map +1 -1
  2. package/dist/src/ai/mcp/create-server.d.ts.map +1 -1
  3. package/dist/src/ai/mcp/index.d.ts +5 -2
  4. package/dist/src/ai/mcp/index.d.ts.map +1 -1
  5. package/dist/src/ai/tools/agent.d.ts.map +1 -1
  6. package/dist/src/ai/tools/core.d.ts.map +1 -1
  7. package/dist/src/ai/tools/query-data.d.ts +2 -2
  8. package/dist/src/ai/tools/query-data.d.ts.map +1 -1
  9. package/dist/src/analyzeQuery/analyzers/fragmentAnalyzer.d.ts.map +1 -1
  10. package/dist/src/analyzeQuery/analyzers/operationAnalyzer.d.ts.map +1 -1
  11. package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
  12. package/dist/src/analyzeQuery/analyzers/variableAnalyzer.d.ts.map +1 -1
  13. package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
  14. package/dist/src/analyzeQuery/directiveUtils.d.ts.map +1 -1
  15. package/dist/src/analyzeQuery/resolveVariables.d.ts +17 -2
  16. package/dist/src/analyzeQuery/resolveVariables.d.ts.map +1 -1
  17. package/dist/src/analyzeQuery/typeUtils.d.ts.map +1 -1
  18. package/dist/src/analyzeQuery/valueExtractors.d.ts.map +1 -1
  19. package/dist/src/authentication/duration.d.ts.map +1 -1
  20. package/dist/src/authentication/index.d.ts.map +1 -1
  21. package/dist/src/authentication/jwt.d.ts.map +1 -1
  22. package/dist/src/authentication/paseto.d.ts.map +1 -1
  23. package/dist/src/authentication/tokenRepository.d.ts +2 -1
  24. package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
  25. package/dist/src/cli/seedAuth.d.ts.map +1 -1
  26. package/dist/src/cli/seedAuthArgs.d.ts.map +1 -1
  27. package/dist/src/config/helpers/cronHelper.d.ts.map +1 -1
  28. package/dist/src/config/helpers/operationHelper.d.ts +1 -1
  29. package/dist/src/config/helpers/operationHelper.d.ts.map +1 -1
  30. package/dist/src/config/helpers/queueHelper.d.ts.map +1 -1
  31. package/dist/src/config/index.d.ts.map +1 -1
  32. package/dist/src/config/types/auth.d.ts +11 -5
  33. package/dist/src/config/types/auth.d.ts.map +1 -1
  34. package/dist/src/config/types/auth.test.d.ts +2 -0
  35. package/dist/src/config/types/auth.test.d.ts.map +1 -0
  36. package/dist/src/config/types/db.d.ts +36 -20
  37. package/dist/src/config/types/db.d.ts.map +1 -1
  38. package/dist/src/config/types/operation.d.ts +6 -6
  39. package/dist/src/config/types/queue.d.ts +4 -4
  40. package/dist/src/configuration/getSchemas/index.d.ts +109 -103
  41. package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
  42. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +59 -50
  43. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
  44. package/dist/src/configuration/getSchemas/type-definition-generator/index.d.ts.map +1 -1
  45. package/dist/src/configuration/gql/buildExecute.d.ts.map +1 -1
  46. package/dist/src/configuration/gql/gqlAuthOperations.d.ts.map +1 -1
  47. package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
  48. package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
  49. package/dist/src/configuration/index.d.ts +100 -95
  50. package/dist/src/configuration/index.d.ts.map +1 -1
  51. package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
  52. package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
  53. package/dist/src/configuration/rest/index.d.ts.map +1 -1
  54. package/dist/src/configuration/rest/openApiErrors.d.ts.map +1 -1
  55. package/dist/src/configuration/rest/parseStringParams.d.ts +15 -0
  56. package/dist/src/configuration/rest/parseStringParams.d.ts.map +1 -0
  57. package/dist/src/configuration/rest/parseStringParams.test.d.ts +2 -0
  58. package/dist/src/configuration/rest/parseStringParams.test.d.ts.map +1 -0
  59. package/dist/src/console/api.d.ts.map +1 -1
  60. package/dist/src/cron/index.d.ts.map +1 -1
  61. package/dist/src/databases/auth/auth-operations.d.ts.map +1 -1
  62. package/dist/src/databases/auth/password.d.ts.map +1 -1
  63. package/dist/src/databases/common.d.ts +7 -3
  64. package/dist/src/databases/common.d.ts.map +1 -1
  65. package/dist/src/databases/core/executor.d.ts.map +1 -1
  66. package/dist/src/databases/core/identifier.d.ts.map +1 -1
  67. package/dist/src/databases/core/query-builder.d.ts +4 -3
  68. package/dist/src/databases/core/query-builder.d.ts.map +1 -1
  69. package/dist/src/databases/directives.d.ts.map +1 -1
  70. package/dist/src/databases/engines/mssql/auth.d.ts.map +1 -1
  71. package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
  72. package/dist/src/databases/engines/mssql/format.d.ts.map +1 -1
  73. package/dist/src/databases/engines/mssql/getStructure.d.ts +13 -12
  74. package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
  75. package/dist/src/databases/engines/mssql/getViews.d.ts.map +1 -1
  76. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  77. package/dist/src/databases/engines/mysql/auth.d.ts.map +1 -1
  78. package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
  79. package/dist/src/databases/engines/mysql/format.d.ts.map +1 -1
  80. package/dist/src/databases/engines/mysql/getStructure.d.ts +13 -12
  81. package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
  82. package/dist/src/databases/engines/mysql/getViews.d.ts.map +1 -1
  83. package/dist/src/databases/engines/mysql/placeholders.d.ts.map +1 -1
  84. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  85. package/dist/src/databases/engines/postgresql/auth.d.ts.map +1 -1
  86. package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
  87. package/dist/src/databases/engines/postgresql/format.d.ts.map +1 -1
  88. package/dist/src/databases/engines/postgresql/getStructure.d.ts +13 -12
  89. package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
  90. package/dist/src/databases/engines/postgresql/getViews.d.ts.map +1 -1
  91. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  92. package/dist/src/databases/engines/shared/claims.d.ts.map +1 -1
  93. package/dist/src/databases/high-level-operations.d.ts +11 -59
  94. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  95. package/dist/src/databases/identifierMapping.test.d.ts +2 -0
  96. package/dist/src/databases/identifierMapping.test.d.ts.map +1 -0
  97. package/dist/src/databases/metadata/structure.d.ts +13 -12
  98. package/dist/src/databases/metadata/structure.d.ts.map +1 -1
  99. package/dist/src/databases/schemaBuilder/generateCreateTable/index.d.ts.map +1 -1
  100. package/dist/src/databases/schemaBuilder/generateCreateView/index.d.ts.map +1 -1
  101. package/dist/src/databases/schemaBuilder/generateInsert/index.d.ts.map +1 -1
  102. package/dist/src/databases/schemaBuilder/generateSchemaCreation/index.d.ts.map +1 -1
  103. package/dist/src/databases/schemaBuilder/mssqlToPostgres/convertView.d.ts.map +1 -1
  104. package/dist/src/databases/schemaBuilder/utils.d.ts +2 -2
  105. package/dist/src/databases/schemaBuilder/utils.d.ts.map +1 -1
  106. package/dist/src/databases/sqlTypeUtils.d.ts.map +1 -1
  107. package/dist/src/databases/transformers/data-transformers.d.ts +37 -34
  108. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  109. package/dist/src/databases/transformers/genResolverName.d.ts.map +1 -1
  110. package/dist/src/databases/transformers/graphqlName.d.ts +23 -0
  111. package/dist/src/databases/transformers/graphqlName.d.ts.map +1 -0
  112. package/dist/src/databases/transformers/graphqlName.test.d.ts +2 -0
  113. package/dist/src/databases/transformers/graphqlName.test.d.ts.map +1 -0
  114. package/dist/src/index.d.ts +6 -5
  115. package/dist/src/index.d.ts.map +1 -1
  116. package/dist/src/logging/index.d.ts.map +1 -1
  117. package/dist/src/queues/kafka.d.ts.map +1 -1
  118. package/dist/src/queues/rabbitmq.d.ts.map +1 -1
  119. package/dist/src/remoteREST/index.d.ts.map +1 -1
  120. package/dist/src/remoteREST/parse.d.ts.map +1 -1
  121. package/dist/src/remoteREST/proxy.d.ts.map +1 -1
  122. package/dist/src/remoteREST/transform.d.ts.map +1 -1
  123. package/dist/src/remoteSchemas/index.d.ts.map +1 -1
  124. package/dist/src/remoteSchemas/introspect.d.ts.map +1 -1
  125. package/dist/src/remoteSchemas/proxy.d.ts.map +1 -1
  126. package/dist/src/remoteSchemas/transform.d.ts.map +1 -1
  127. package/dist/src/singletons/ai.d.ts.map +1 -1
  128. package/dist/src/singletons/authentication.d.ts.map +1 -1
  129. package/dist/src/singletons/cache/lruCacheStore.d.ts.map +1 -1
  130. package/dist/src/singletons/cache/redisCacheStore.d.ts.map +1 -1
  131. package/dist/src/singletons/cache/registry.d.ts.map +1 -1
  132. package/dist/src/singletons/cron.d.ts.map +1 -1
  133. package/dist/src/singletons/databases.d.ts.map +1 -1
  134. package/dist/src/singletons/env.d.ts +191 -188
  135. package/dist/src/singletons/env.d.ts.map +1 -1
  136. package/dist/src/singletons/queues.d.ts.map +1 -1
  137. package/dist/src/subscriptions/strategies/index.d.ts.map +1 -1
  138. package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
  139. package/dist/src/types/db.d.ts.map +1 -1
  140. package/dist/src/types/env.d.ts +647 -636
  141. package/dist/src/types/env.d.ts.map +1 -1
  142. package/dist/src/types/resolver.d.ts.map +1 -1
  143. package/dist/src/types/zod/auth.d.ts +18 -18
  144. package/dist/src/types/zod/configuration.d.ts +300 -295
  145. package/dist/src/types/zod/configuration.d.ts.map +1 -1
  146. package/dist/src/types/zod/db.d.ts +89 -80
  147. package/dist/src/types/zod/db.d.ts.map +1 -1
  148. package/dist/src/types/zod/queue.d.ts +80 -80
  149. package/dist/src/types/zod/queue.d.ts.map +1 -1
  150. package/dist/src/utils/benchmark.d.ts.map +1 -1
  151. package/dist/src/utils/responses.d.ts.map +1 -1
  152. package/dist/src/utils/safeJSONParse.d.ts.map +1 -1
  153. package/dist/src/utils/selection.d.ts.map +1 -1
  154. package/dist/src/utils/sessionVariables.d.ts +5 -1
  155. package/dist/src/utils/sessionVariables.d.ts.map +1 -1
  156. package/dist/src/utils/writeSchema.d.ts.map +1 -1
  157. package/dist/tsconfig.tsbuildinfo +1 -1
  158. package/package.json +6 -6
  159. package/playgrounds/console/index.html +18 -15
  160. package/playgrounds/graphiql/index.html +854 -622
  161. package/playgrounds/scalar/index.html +5 -5
  162. package/src/ai/README.md +257 -0
  163. package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +7 -1
  164. package/src/analyzeQuery/resolveVariables.ts +130 -16
  165. package/src/authentication/jwt.ts +3 -1
  166. package/src/authentication/paseto.ts +3 -1
  167. package/src/authentication/tokenRepository.ts +12 -4
  168. package/src/config/types/auth.ts +7 -1
  169. package/src/config/types/db.ts +8 -0
  170. package/src/configuration/getSchemas/mergeEntities/index.ts +26 -1
  171. package/src/configuration/getSchemas/type-definition-generator/index.ts +11 -6
  172. package/src/configuration/rest/handleRESTRequestFactory.ts +5 -7
  173. package/src/configuration/rest/parseStringParams.ts +92 -0
  174. package/src/databases/common.ts +13 -2
  175. package/src/databases/engines/mssql/query/index.ts +25 -12
  176. package/src/databases/engines/mysql/connection.ts +1 -0
  177. package/src/databases/engines/mysql/query/index.ts +21 -8
  178. package/src/databases/engines/postgresql/query/index.ts +21 -7
  179. package/src/databases/high-level-operations.ts +2 -2
  180. package/src/databases/transformers/genResolverName.ts +3 -3
  181. package/src/databases/transformers/graphqlName.ts +28 -0
  182. package/src/logging/index.ts +3 -1
  183. package/src/types/db.ts +0 -1
  184. package/src/types/zod/db.ts +5 -7
  185. package/src/types/zod/queue.ts +2 -10
  186. package/src/utils/sessionVariables.ts +7 -1
@@ -6,6 +6,7 @@ import type { ResolverRegistry } from "../../../types/resolver";
6
6
  import type { TypedOperation } from "../../../types/zod/operation";
7
7
 
8
8
  import { EntitySource, createResolverEntry } from "../../../types/resolver";
9
+ import { columnFieldName } from "../../../databases/transformers/graphqlName";
9
10
  import { SqlTypeCategory, categorizeSqlType } from "../../../databases/sqlTypeUtils";
10
11
 
11
12
  const dataTypeToOpenApiType = (dataType: string): string => {
@@ -33,7 +34,23 @@ export const mergeEntities = (
33
34
  const resolverRegistry: ResolverRegistry = {};
34
35
 
35
36
  // Register tables (queries)
37
+ // Sanitisation is not injective — `categoría` and `categor_a` both produce
38
+ // `categoria` — so two tables can land on one resolver name. Serving one
39
+ // table's rows under the other's name is worse than not booting.
40
+ const sourceOfResolverName: Record<string, string> = {};
41
+
36
42
  const queriesMap = entityOfRole.tables.reduce<Record<string, TableResolver>>((acc, table) => {
43
+ const source = `${table.schema}.${table.name}`;
44
+ const previousSource = sourceOfResolverName[table.resolverName];
45
+
46
+ if (previousSource) {
47
+ throw new Error(
48
+ `Duplicate GraphQL name "${table.resolverName}": both ${previousSource} and ${source} map to it. Rename one of them, or exclude it with schema.excludedTables.`,
49
+ );
50
+ }
51
+
52
+ sourceOfResolverName[table.resolverName] = source;
53
+
37
54
  acc[table.resolverName] = table;
38
55
  acc[`${table.resolverName}_single`] = table;
39
56
  acc[`${table.resolverName}_aggregate`] = table;
@@ -207,10 +224,18 @@ export const mergeEntities = (
207
224
  },
208
225
  isVirtualColumn: (table: string, column: string): VirtualColumn | undefined => {
209
226
  const col = queriesMap[table]?.columns.find(
210
- (c) => c.name === column && "virtual" in c && c.virtual,
227
+ (c) => columnFieldName(c) === column && "virtual" in c && c.virtual,
211
228
  );
212
229
  return col ? (col as VirtualColumn) : undefined;
213
230
  },
231
+ /**
232
+ * The SQL identifier behind a GraphQL field name. Columns whose name GraphQL
233
+ * cannot spell are exposed under a sanitised field name, so anything that
234
+ * emits SQL for a field the client named has to come back through here.
235
+ * Unknown names pass through: a legal name always sanitises to itself.
236
+ */
237
+ columnSqlName: (table: string, fieldName: string): string =>
238
+ queriesMap[table]?.columns.find((c) => columnFieldName(c) === fieldName)?.name ?? fieldName,
214
239
  getColumnTypeForOpenApi: (table: string, column: string) => {
215
240
  const tableObj = queriesMap[table];
216
241
 
@@ -1,6 +1,7 @@
1
1
  import type { OpenAPIV3_1 } from "openapi-types";
2
2
 
3
3
  import { SqlTypeCategory, categorizeSqlType, isNumericType } from "../../../databases/sqlTypeUtils";
4
+ import { columnFieldName } from "../../../databases/transformers/graphqlName";
4
5
  import { convertFromZod } from "../../rest/openApiErrors";
5
6
  import { type MergedEntities } from "../mergeEntities";
6
7
 
@@ -91,8 +92,8 @@ export const generateTableType = (mergedEntities: MergedEntities) =>
91
92
  ${lj([
92
93
  lj(
93
94
  columns,
94
- ({ name, dataType, isNullable, description }) =>
95
- `${sdlDesc(description)}${name}: ${mapSQLTypeToGraphQLType(dataType)}${isNullable ? "" : "!"}`,
95
+ (column) =>
96
+ `${sdlDesc(column.description)}${columnFieldName(column)}: ${mapSQLTypeToGraphQLType(column.dataType)}${column.isNullable ? "" : "!"}`,
96
97
  ),
97
98
  lj(
98
99
  relationships,
@@ -115,7 +116,11 @@ export const generateWhereInputType = (mergedEntities: MergedEntities) =>
115
116
  ({ resolverName, columns, relationships, relationshipsReversed }) => `
116
117
  input ${resolverName}WhereInput {
117
118
  ${lj([
118
- lj(columns, ({ name, dataType }) => `${name}: ${mapSQLTypeToConditionType(dataType)}`),
119
+ lj(
120
+ columns,
121
+ (column) =>
122
+ `${columnFieldName(column)}: ${mapSQLTypeToConditionType(column.dataType)}`,
123
+ ),
119
124
  lj(
120
125
  relationships,
121
126
  ({ toResolverName, toInternalName }) =>
@@ -138,7 +143,7 @@ export const generateOrderByInputType = (mergedEntities: MergedEntities) =>
138
143
  mergedEntities.tables.map(
139
144
  ({ internalName, columns }) => `
140
145
  input ${internalName}OrderByInput {
141
- ${lj(columns, ({ name }) => `${name}: OrderByEnum`)}
146
+ ${lj(columns, (column) => `${columnFieldName(column)}: OrderByEnum`)}
142
147
  }`,
143
148
  ),
144
149
  );
@@ -165,7 +170,7 @@ export const generateAggregationTypes = (mergedEntities: MergedEntities) => {
165
170
  ({ name: agg, graphQLType }) =>
166
171
  `
167
172
  type ${resolverName}${agg} {
168
- ${lj(numericColumns, ({ name }) => `${name}: ${graphQLType ?? mapSQLTypeToGraphQLType(columns.find((c) => c.name === name)?.dataType || "")}`)}
173
+ ${lj(numericColumns, (column) => `${columnFieldName(column)}: ${graphQLType ?? mapSQLTypeToGraphQLType(column.dataType)}`)}
169
174
  }
170
175
  `,
171
176
  )
@@ -187,7 +192,7 @@ export const generateAggregationTypes = (mergedEntities: MergedEntities) => {
187
192
  }
188
193
 
189
194
  enum ${resolverName}GroupByKeys {
190
- ${columns.map((c) => c.name).join("\n")}
195
+ ${columns.map(columnFieldName).join("\n")}
191
196
  }
192
197
  `;
193
198
  });
@@ -18,6 +18,7 @@ import { databasesConnections, repositoryMap } from "../../singletons/databases"
18
18
  import { queueManager } from "../../singletons/queues";
19
19
  import { S200, S401, S404 } from "../../utils/responses";
20
20
  import { buildApiRoutes } from "../rest";
21
+ import { parseStringParams } from "./parseStringParams";
21
22
  import { logger } from "../../logging";
22
23
 
23
24
  // Build remote REST route matchers
@@ -118,17 +119,14 @@ export const handleRESTRequestFactory = (
118
119
  // left `undefined` when its schema is not configured; that `undefined` is
119
120
  // forwarded to `beforeRequest` so the hook can tell the sources apart,
120
121
  // while `allVariables` stays identical because spreading `undefined` is a
121
- // no-op.
122
- const pathVariables = route.rest!.pathParams?.parse(pathParameters) as
123
- | Record<string, unknown>
124
- | undefined;
122
+ // no-op. Path and query values are strings on the wire, so
123
+ // `parseStringParams` converts the keys declared as `z.boolean()` first.
124
+ const pathVariables = parseStringParams(route.rest!.pathParams, pathParameters);
125
125
 
126
126
  const paramsQuery = new URLSearchParams(url.search);
127
127
  const paramsQueryDictionary = Object.fromEntries(paramsQuery.entries());
128
128
 
129
- const queryVariables = route.rest!.queryParams?.parse(paramsQueryDictionary) as
130
- | Record<string, unknown>
131
- | undefined;
129
+ const queryVariables = parseStringParams(route.rest!.queryParams, paramsQueryDictionary);
132
130
 
133
131
  let bodyVariables: Record<string, unknown> | undefined;
134
132
 
@@ -0,0 +1,92 @@
1
+ import type { z } from "zod";
2
+
3
+ /**
4
+ * The slice of Zod's runtime schema internals this module walks to find which
5
+ * keys of a `z.object()` ultimately validate a boolean.
6
+ */
7
+ type SchemaDef = {
8
+ type: string;
9
+ innerType?: { def: SchemaDef };
10
+ in?: { def: SchemaDef };
11
+ shape?: Record<string, { def: SchemaDef }>;
12
+ };
13
+
14
+ /** Wrappers that keep the validated type of the schema they wrap. */
15
+ const wrappers = new Set([
16
+ "optional",
17
+ "nullable",
18
+ "nonoptional",
19
+ "default",
20
+ "prefault",
21
+ "catch",
22
+ "readonly",
23
+ ]);
24
+
25
+ /** Accepted spellings, matching the ones `z.stringbool()` recognizes. */
26
+ const truthy = new Set(["true", "1", "yes", "on", "y", "enabled"]);
27
+ const falsy = new Set(["false", "0", "no", "off", "n", "disabled"]);
28
+
29
+ const defOf = (schema: z.ZodType) => (schema as unknown as { def: SchemaDef }).def;
30
+
31
+ const resolveType = (def: SchemaDef): string => {
32
+ if (wrappers.has(def.type) && def.innerType) return resolveType(def.innerType.def);
33
+ // A pipe validates its input side, so `z.stringbool()` resolves to a string.
34
+ if (def.type === "pipe" && def.in) return resolveType(def.in.def);
35
+
36
+ return def.type;
37
+ };
38
+
39
+ // Schemas are created once at config time, so the key scan is done once per schema.
40
+ const booleanKeysCache = new WeakMap<z.ZodType, Set<string>>();
41
+
42
+ const booleanKeys = (schema: z.ZodType) => {
43
+ const cached = booleanKeysCache.get(schema);
44
+ if (cached) return cached;
45
+
46
+ const keys = new Set<string>();
47
+ for (const [key, field] of Object.entries(defOf(schema).shape ?? {})) {
48
+ if (resolveType(field.def) === "boolean") keys.add(key);
49
+ }
50
+
51
+ booleanKeysCache.set(schema, keys);
52
+
53
+ return keys;
54
+ };
55
+
56
+ /**
57
+ * Parses path or query parameters, which always arrive as strings.
58
+ *
59
+ * A parameter declared as `z.boolean()` — the declaration that makes the
60
+ * OpenAPI docs render a true/false picker instead of a free-text box — would
61
+ * never parse on its own, so those keys (and only those) are converted before
62
+ * validation. A value that spells neither true nor false is left as the string
63
+ * it was, so Zod still reports it as invalid.
64
+ *
65
+ * Returns `undefined` when no schema is configured for that source, which is
66
+ * what the REST handler forwards to `beforeRequest`.
67
+ */
68
+ export const parseStringParams = (
69
+ schema: z.ZodType | undefined,
70
+ values: Record<string, string | string[]>,
71
+ ): Record<string, unknown> | undefined => {
72
+ if (!schema) return undefined;
73
+
74
+ const keys = booleanKeys(schema);
75
+
76
+ if (keys.size === 0) return schema.parse(values) as Record<string, unknown>;
77
+
78
+ const coerced: Record<string, unknown> = { ...values };
79
+
80
+ for (const key of keys) {
81
+ const value = coerced[key];
82
+
83
+ if (typeof value !== "string") continue;
84
+
85
+ const spelling = value.toLowerCase();
86
+
87
+ if (truthy.has(spelling)) coerced[key] = true;
88
+ else if (falsy.has(spelling)) coerced[key] = false;
89
+ }
90
+
91
+ return schema.parse(coerced) as Record<string, unknown>;
92
+ };
@@ -224,9 +224,20 @@ const columnTargetFp =
224
224
 
225
225
  return virtualColumn
226
226
  ? renderVirtualColumn(virtualColumn as VirtualColumn)
227
- : `${tableAlias}.${wrapIdentifierFp(dbType)(colName)}`;
227
+ : `${tableAlias}.${wrapIdentifierFp(dbType)(sqlColumnName(entities, tableName, colName))}`;
228
228
  };
229
229
 
230
+ /**
231
+ * The SQL identifier a GraphQL field name compiles to. A column GraphQL cannot
232
+ * spell is exposed under a sanitised field name, so the name the client sent is
233
+ * not necessarily the name the database knows.
234
+ */
235
+ export const sqlColumnName = (
236
+ entities: MergedEntities,
237
+ tableName: string | undefined,
238
+ fieldName: string,
239
+ ): string => (tableName ? entities.columnSqlName(tableName, fieldName) : fieldName);
240
+
230
241
  // Helper to resolve variable values
231
242
  const resolveVariable = <T>(value: unknown, variables: Record<string, unknown>): T => {
232
243
  if (isString(value) && value.startsWith("$")) {
@@ -700,7 +711,7 @@ export const processFieldSelectionsFp =
700
711
  selectClauses.push([`${colName}`, `(${vc.expression})`]);
701
712
  }
702
713
  } else {
703
- let querySelector = `${tableAlias}.${wrapIdentifierFp(dbType)(sel.name)}`;
714
+ let querySelector = `${tableAlias}.${wrapIdentifierFp(dbType)(sqlColumnName(entities, tableName, sel.name))}`;
704
715
 
705
716
  // Apply all directives using the handler registry
706
717
  querySelector = applyDirectives(
@@ -16,6 +16,7 @@ import {
16
16
  isAggregationField,
17
17
  isSingleQuery,
18
18
  processFieldSelectionsMSSQL,
19
+ sqlColumnName,
19
20
  wrapIdentifierMSSQL,
20
21
  } from "../../../common";
21
22
 
@@ -26,18 +27,21 @@ import { applyDirectives } from "../../../directives";
26
27
 
27
28
  // Generate CTE for aggregations
28
29
  const buildAggregationCTE = (
30
+ entities: MergedEntities,
31
+ tableName: string,
29
32
  groupByInfo: GroupByInfo,
30
33
  dottedQuotedName: string,
31
34
  tableAlias: string,
32
35
  whereClause: string,
33
36
  ): string => {
34
37
  const { groupByFields, aggregations, cteAlias } = groupByInfo;
38
+ const sqlName = (fieldName: string) => sqlColumnName(entities, tableName, fieldName);
35
39
 
36
40
  const selectClauses: string[] = [];
37
41
 
38
42
  // Add group by fields
39
43
  groupByFields.forEach((field) => {
40
- selectClauses.push(`${tableAlias}.${wrapIdentifierMSSQL(field)}`);
44
+ selectClauses.push(`${tableAlias}.${wrapIdentifierMSSQL(sqlName(field))}`);
41
45
  });
42
46
 
43
47
  // Add aggregations
@@ -50,13 +54,13 @@ const buildAggregationCTE = (
50
54
  // 1, 5, 5, 2 is 3, not 3.25 — so the average has to be taken on a float.
51
55
  const operand =
52
56
  agg.name === "avg"
53
- ? `CAST(${tableAlias}.${wrapIdentifierMSSQL(agg.fieldName)} AS FLOAT)`
54
- : `${tableAlias}.${wrapIdentifierMSSQL(agg.fieldName)}`;
57
+ ? `CAST(${tableAlias}.${wrapIdentifierMSSQL(sqlName(agg.fieldName))} AS FLOAT)`
58
+ : `${tableAlias}.${wrapIdentifierMSSQL(sqlName(agg.fieldName))}`;
55
59
  selectClauses.push(`${func}(${operand}) AS ${agg.alias}`);
56
60
  }
57
61
  });
58
62
 
59
- const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${wrapIdentifierMSSQL(field)}`).join(", ")}`;
63
+ const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${wrapIdentifierMSSQL(sqlName(field))}`).join(", ")}`;
60
64
 
61
65
  return `${cteAlias} AS (
62
66
  SELECT
@@ -80,19 +84,21 @@ const buildGroupedQuery = (
80
84
  const { groupByFields, aggregations, hasItems, keyResolved, hasKey, keys, cteAlias } =
81
85
  groupByInfo;
82
86
 
87
+ const sqlName = (fieldName: string) => sqlColumnName(entities, field.name, fieldName);
88
+
83
89
  const selectClauses: string[] = [];
84
90
 
85
91
  if (hasKey) {
86
92
  // Add key object with group by fields
87
93
  const keyFields = keys
88
- .map((field) => {
94
+ .map((key) => {
89
95
  const selector = applyDirectives(
90
- `${cteAlias}.${wrapIdentifierMSSQL(field.name)}`,
91
- field.directives,
96
+ `${cteAlias}.${wrapIdentifierMSSQL(sqlName(key.name))}`,
97
+ key.directives,
92
98
  "mssql",
93
99
  variablesDefinition,
94
100
  );
95
- return `${selector} AS ${wrapIdentifierMSSQL(field.alias || field.name)}`;
101
+ return `${selector} AS ${wrapIdentifierMSSQL(key.alias || key.name)}`;
96
102
  })
97
103
  .join(", ");
98
104
 
@@ -121,7 +127,7 @@ const buildGroupedQuery = (
121
127
  .filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
122
128
  .map((sel) => {
123
129
  const selector = applyDirectives(
124
- `${tableAlias}.${wrapIdentifierMSSQL(sel.name)}`,
130
+ `${tableAlias}.${wrapIdentifierMSSQL(sqlName(sel.name))}`,
125
131
  sel.directives,
126
132
  "mssql",
127
133
  variablesDefinition,
@@ -132,8 +138,8 @@ const buildGroupedQuery = (
132
138
 
133
139
  if (itemFields) {
134
140
  const joinConditions = groupByFields.map(
135
- (field) =>
136
- `${tableAlias}.${wrapIdentifierMSSQL(field)} = ${cteAlias}.${wrapIdentifierMSSQL(field)}`,
141
+ (groupByField) =>
142
+ `${tableAlias}.${wrapIdentifierMSSQL(sqlName(groupByField))} = ${cteAlias}.${wrapIdentifierMSSQL(sqlName(groupByField))}`,
137
143
  );
138
144
 
139
145
  selectClauses.push(`JSON_QUERY(ISNULL((
@@ -212,7 +218,14 @@ export const generateSQL = (
212
218
  {},
213
219
  );
214
220
 
215
- const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
221
+ const cte = buildAggregationCTE(
222
+ entities,
223
+ field.name,
224
+ groupByInfo,
225
+ dottedQuotedName,
226
+ tableAlias,
227
+ whereClause,
228
+ );
216
229
  ctes.push(cte);
217
230
  }
218
231
 
@@ -22,6 +22,7 @@ export const getPool = async (db: Database) => {
22
22
  connectionTimeout: opts?.connectionTimeout,
23
23
  maxLifetime: opts?.maxLifetime,
24
24
  tls: opts?.tls,
25
+ allowPublicKeyRetrieval: opts?.allowPublicKeyRetrieval,
25
26
  prepare: opts?.prepare,
26
27
  bigint: opts?.bigint,
27
28
  });
@@ -16,6 +16,7 @@ import {
16
16
  isAggregationField,
17
17
  isSingleQuery,
18
18
  processFieldSelectionsMySQL,
19
+ sqlColumnName,
19
20
  wrapIdentifierMySQL,
20
21
  } from "../../../common";
21
22
 
@@ -23,18 +24,21 @@ import { applyDirectives } from "../../../directives";
23
24
 
24
25
  // Generate CTE for aggregations
25
26
  const buildAggregationCTE = (
27
+ entities: MergedEntities,
28
+ tableName: string,
26
29
  groupByInfo: GroupByInfo,
27
30
  dottedQuotedName: string,
28
31
  tableAlias: string,
29
32
  whereClause: string,
30
33
  ): string => {
31
34
  const { groupByFields, aggregations, cteAlias } = groupByInfo;
35
+ const sqlName = (fieldName: string) => sqlColumnName(entities, tableName, fieldName);
32
36
 
33
37
  const selectClauses: string[] = [];
34
38
 
35
39
  // Add group by fields
36
40
  groupByFields.forEach((field) => {
37
- selectClauses.push(`${tableAlias}.${wrapIdentifierMySQL(field)}`);
41
+ selectClauses.push(`${tableAlias}.${wrapIdentifierMySQL(sqlName(field))}`);
38
42
  });
39
43
 
40
44
  // Add aggregations
@@ -44,12 +48,12 @@ const buildAggregationCTE = (
44
48
  } else {
45
49
  const func = agg.name.toUpperCase();
46
50
  selectClauses.push(
47
- `${func}(${tableAlias}.${wrapIdentifierMySQL(agg.fieldName)}) AS ${agg.alias}`,
51
+ `${func}(${tableAlias}.${wrapIdentifierMySQL(sqlName(agg.fieldName))}) AS ${agg.alias}`,
48
52
  );
49
53
  }
50
54
  });
51
55
 
52
- const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${wrapIdentifierMySQL(field)}`).join(", ")}`;
56
+ const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${wrapIdentifierMySQL(sqlName(field))}`).join(", ")}`;
53
57
 
54
58
  return `${cteAlias} AS (
55
59
  SELECT
@@ -73,6 +77,8 @@ const buildGroupedQuery = (
73
77
  const { groupByFields, aggregations, hasItems, keyResolved, hasKey, keys, cteAlias } =
74
78
  groupByInfo;
75
79
 
80
+ const sqlName = (fieldName: string) => sqlColumnName(entities, field.name, fieldName);
81
+
76
82
  const selectClauses: string[] = [];
77
83
 
78
84
  if (hasKey) {
@@ -80,7 +86,7 @@ const buildGroupedQuery = (
80
86
  const keyFields = keys
81
87
  .map((key) => {
82
88
  const selector = applyDirectives(
83
- `${cteAlias}.${wrapIdentifierMySQL(key.name)}`,
89
+ `${cteAlias}.${wrapIdentifierMySQL(sqlName(key.name))}`,
84
90
  key.directives,
85
91
  "mysql",
86
92
  variablesDefinition,
@@ -112,7 +118,7 @@ const buildGroupedQuery = (
112
118
  .filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
113
119
  .map((sel) => {
114
120
  const selector = applyDirectives(
115
- `${tableAlias}.${wrapIdentifierMySQL(sel.name)}`,
121
+ `${tableAlias}.${wrapIdentifierMySQL(sqlName(sel.name))}`,
116
122
  sel.directives,
117
123
  "mysql",
118
124
  variablesDefinition,
@@ -123,8 +129,8 @@ const buildGroupedQuery = (
123
129
 
124
130
  if (itemFields) {
125
131
  const joinConditions = groupByFields.map(
126
- (field) =>
127
- `${tableAlias}.${wrapIdentifierMySQL(field)} = ${cteAlias}.${wrapIdentifierMySQL(field)}`,
132
+ (groupByField) =>
133
+ `${tableAlias}.${wrapIdentifierMySQL(sqlName(groupByField))} = ${cteAlias}.${wrapIdentifierMySQL(sqlName(groupByField))}`,
128
134
  );
129
135
 
130
136
  const whereConditions = whereClause
@@ -194,7 +200,14 @@ export const generateSQL = (
194
200
  {},
195
201
  );
196
202
 
197
- const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
203
+ const cte = buildAggregationCTE(
204
+ entities,
205
+ field.name,
206
+ groupByInfo,
207
+ dottedQuotedName,
208
+ tableAlias,
209
+ whereClause,
210
+ );
198
211
  ctes.push(cte);
199
212
  }
200
213
 
@@ -16,24 +16,28 @@ import {
16
16
  isAggregationField,
17
17
  isSingleQuery,
18
18
  processFieldSelectionsPG,
19
+ sqlColumnName,
19
20
  } from "../../../common";
20
21
 
21
22
  import { applyDirectives } from "../../../directives";
22
23
 
23
24
  // Generate CTE for aggregations
24
25
  const buildAggregationCTE = (
26
+ entities: MergedEntities,
27
+ tableName: string,
25
28
  groupByInfo: GroupByInfo,
26
29
  dottedQuotedName: string,
27
30
  tableAlias: string,
28
31
  whereClause: string,
29
32
  ): string => {
30
33
  const { groupByFields, aggregations, cteAlias } = groupByInfo;
34
+ const sqlName = (fieldName: string) => sqlColumnName(entities, tableName, fieldName);
31
35
 
32
36
  const selectClauses: string[] = [];
33
37
 
34
38
  // Add group by fields
35
39
  groupByFields.forEach((field) => {
36
- selectClauses.push(`${tableAlias}."${field}"`);
40
+ selectClauses.push(`${tableAlias}."${sqlName(field)}"`);
37
41
  });
38
42
 
39
43
  // Add aggregations
@@ -42,11 +46,11 @@ const buildAggregationCTE = (
42
46
  selectClauses.push(`COUNT(*) AS ${agg.alias}`);
43
47
  } else {
44
48
  const func = agg.name.toUpperCase();
45
- selectClauses.push(`${func}(${tableAlias}."${agg.fieldName}") AS ${agg.alias}`);
49
+ selectClauses.push(`${func}(${tableAlias}."${sqlName(agg.fieldName)}") AS ${agg.alias}`);
46
50
  }
47
51
  });
48
52
 
49
- const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}."${field}"`).join(", ")}`;
53
+ const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}."${sqlName(field)}"`).join(", ")}`;
50
54
 
51
55
  return `${cteAlias} AS (
52
56
  SELECT
@@ -70,6 +74,8 @@ const buildGroupedQuery = (
70
74
  const { groupByFields, aggregations, hasItems, keyResolved, hasKey, keys, cteAlias } =
71
75
  groupByInfo;
72
76
 
77
+ const sqlName = (fieldName: string) => sqlColumnName(entities, field.name, fieldName);
78
+
73
79
  const selectClauses: string[] = [];
74
80
 
75
81
  if (hasKey) {
@@ -77,7 +83,7 @@ const buildGroupedQuery = (
77
83
  const keyFields = keys
78
84
  .map((key) => {
79
85
  const selector = applyDirectives(
80
- `${cteAlias}."${key.name}"`,
86
+ `${cteAlias}."${sqlName(key.name)}"`,
81
87
  key.directives,
82
88
  "pg",
83
89
  variablesDefinition,
@@ -109,7 +115,7 @@ const buildGroupedQuery = (
109
115
  .filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
110
116
  .map((sel) => {
111
117
  const selector = applyDirectives(
112
- `${tableAlias}."${sel.name}"`,
118
+ `${tableAlias}."${sqlName(sel.name)}"`,
113
119
  sel.directives,
114
120
  "pg",
115
121
  variablesDefinition,
@@ -120,7 +126,8 @@ const buildGroupedQuery = (
120
126
 
121
127
  if (itemFields) {
122
128
  const joinConditions = groupByFields.map(
123
- (field) => `${tableAlias}."${field}" = ${cteAlias}."${field}"`,
129
+ (groupByField) =>
130
+ `${tableAlias}."${sqlName(groupByField)}" = ${cteAlias}."${sqlName(groupByField)}"`,
124
131
  );
125
132
 
126
133
  const whereConditions = whereClause
@@ -190,7 +197,14 @@ export const generateSQL = (
190
197
  {},
191
198
  );
192
199
 
193
- const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
200
+ const cte = buildAggregationCTE(
201
+ entities,
202
+ field.name,
203
+ groupByInfo,
204
+ dottedQuotedName,
205
+ tableAlias,
206
+ whereClause,
207
+ );
194
208
  ctes.push(cte);
195
209
  }
196
210
 
@@ -11,7 +11,7 @@ import type {
11
11
  TableFilter,
12
12
  VirtualColumns,
13
13
  } from "../types/configuration";
14
- import type { Column, ProcedureResolver, TableResolver, Tables } from "../types/db";
14
+ import type { ProcedureResolver, TableColumn, TableResolver, Tables } from "../types/db";
15
15
 
16
16
  import { getDatabaseStructure } from "./metadata/structure";
17
17
  import { buildProcedureResolver, buildTableResolver } from "./transformers/data-transformers";
@@ -63,7 +63,7 @@ export const getDatabasesStructure = async (
63
63
 
64
64
  if (tableOverride.columns.length) {
65
65
  // Mix database columns with virtual columns from configuration
66
- t.columns = [...t.columns, ...tableOverride.columns] as Column[];
66
+ t.columns = [...t.columns, ...tableOverride.columns] as TableColumn[];
67
67
  }
68
68
 
69
69
  if (tableOverride.relationships.length) {
@@ -1,3 +1,5 @@
1
+ import { sanitizeGraphQLName } from "./graphqlName";
2
+
1
3
  export const genResolverName = (
2
4
  schema: string,
3
5
  name: string,
@@ -12,7 +14,5 @@ export const genResolverName = (
12
14
  .replace("{schema}", schema)
13
15
  .replace("{name}", name);
14
16
 
15
- if (suffix) return `${baseName}_${suffix}`;
16
-
17
- return baseName;
17
+ return sanitizeGraphQLName(suffix ? `${baseName}_${suffix}` : baseName);
18
18
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Maps a SQL identifier onto a legal GraphQL name (`/^[_A-Za-z][_0-9A-Za-z]*$/`).
3
+ *
4
+ * PostgreSQL, MySQL and SQL Server all accept identifiers GraphQL cannot spell —
5
+ * spaces, accents, punctuation — and an unmapped one produces an SDL document
6
+ * that fails to parse, taking the whole server down at boot.
7
+ *
8
+ * A name that is already legal is returned unchanged, so this cannot rename a
9
+ * field in any schema that boots today. Runs of illegal characters are not
10
+ * collapsed, and diacritics fold to their base letter rather than being dropped,
11
+ * both so that distinct identifiers stay distinct as often as possible.
12
+ */
13
+ export const sanitizeGraphQLName = (name: string): string => {
14
+ const folded = name.normalize("NFD").replace(/[̀-ͯ]/g, "");
15
+ const replaced = folded.replace(/[^_0-9A-Za-z]/g, "_");
16
+
17
+ if (replaced.length === 0) return "_";
18
+
19
+ return /^[0-9]/.test(replaced) ? `_${replaced}` : replaced;
20
+ };
21
+
22
+ /**
23
+ * The GraphQL field a column is exposed as. Introspected columns carry a
24
+ * `fieldName` computed once at parse time; virtual columns come from
25
+ * configuration and carry only a name, so they are sanitised on the spot.
26
+ */
27
+ export const columnFieldName = (column: { name: string; fieldName?: string }): string =>
28
+ column.fieldName ?? sanitizeGraphQLName(column.name);
@@ -3,7 +3,9 @@ import pino from "pino";
3
3
  const DEFAULT_LEVEL =
4
4
  process.env.LOG_LEVEL || (process.env.NODE_ENV === "production" ? "info" : "debug");
5
5
 
6
- const isDev = process.env.NODE_ENV !== "production";
6
+ // `bun test` sets NODE_ENV=test: pino-pretty runs on a worker thread, and
7
+ // Bun's test runner cannot construct one (thread-stream crashes on import).
8
+ const isDev = process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test";
7
9
 
8
10
  const DEFAULT_OPTIONS: pino.LoggerOptions = {
9
11
  level: DEFAULT_LEVEL,
package/src/types/db.ts CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  TableColumnZod,
16
16
  TableZod,
17
17
  ViewZod,
18
- VirtualColumnZod,
19
18
  } from "./zod/db";
20
19
 
21
20
  // Re-export VirtualColumnType from the config module