@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.
- package/dist/src/ai/agent/providers/ollama.d.ts.map +1 -1
- package/dist/src/ai/mcp/create-server.d.ts.map +1 -1
- package/dist/src/ai/mcp/index.d.ts +5 -2
- package/dist/src/ai/mcp/index.d.ts.map +1 -1
- package/dist/src/ai/tools/agent.d.ts.map +1 -1
- package/dist/src/ai/tools/core.d.ts.map +1 -1
- package/dist/src/ai/tools/query-data.d.ts +2 -2
- package/dist/src/ai/tools/query-data.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/fragmentAnalyzer.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/operationAnalyzer.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/variableAnalyzer.d.ts.map +1 -1
- package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
- package/dist/src/analyzeQuery/directiveUtils.d.ts.map +1 -1
- package/dist/src/analyzeQuery/resolveVariables.d.ts +17 -2
- package/dist/src/analyzeQuery/resolveVariables.d.ts.map +1 -1
- package/dist/src/analyzeQuery/typeUtils.d.ts.map +1 -1
- package/dist/src/analyzeQuery/valueExtractors.d.ts.map +1 -1
- package/dist/src/authentication/duration.d.ts.map +1 -1
- package/dist/src/authentication/index.d.ts.map +1 -1
- package/dist/src/authentication/jwt.d.ts.map +1 -1
- package/dist/src/authentication/paseto.d.ts.map +1 -1
- package/dist/src/authentication/tokenRepository.d.ts +2 -1
- package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
- package/dist/src/cli/seedAuth.d.ts.map +1 -1
- package/dist/src/cli/seedAuthArgs.d.ts.map +1 -1
- package/dist/src/config/helpers/cronHelper.d.ts.map +1 -1
- package/dist/src/config/helpers/operationHelper.d.ts +1 -1
- package/dist/src/config/helpers/operationHelper.d.ts.map +1 -1
- package/dist/src/config/helpers/queueHelper.d.ts.map +1 -1
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/types/auth.d.ts +11 -5
- package/dist/src/config/types/auth.d.ts.map +1 -1
- package/dist/src/config/types/auth.test.d.ts +2 -0
- package/dist/src/config/types/auth.test.d.ts.map +1 -0
- package/dist/src/config/types/db.d.ts +36 -20
- package/dist/src/config/types/db.d.ts.map +1 -1
- package/dist/src/config/types/operation.d.ts +6 -6
- package/dist/src/config/types/queue.d.ts +4 -4
- package/dist/src/configuration/getSchemas/index.d.ts +109 -103
- package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +59 -50
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/type-definition-generator/index.d.ts.map +1 -1
- package/dist/src/configuration/gql/buildExecute.d.ts.map +1 -1
- package/dist/src/configuration/gql/gqlAuthOperations.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
- package/dist/src/configuration/index.d.ts +100 -95
- package/dist/src/configuration/index.d.ts.map +1 -1
- package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
- package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
- package/dist/src/configuration/rest/index.d.ts.map +1 -1
- package/dist/src/configuration/rest/openApiErrors.d.ts.map +1 -1
- package/dist/src/configuration/rest/parseStringParams.d.ts +15 -0
- package/dist/src/configuration/rest/parseStringParams.d.ts.map +1 -0
- package/dist/src/configuration/rest/parseStringParams.test.d.ts +2 -0
- package/dist/src/configuration/rest/parseStringParams.test.d.ts.map +1 -0
- package/dist/src/console/api.d.ts.map +1 -1
- package/dist/src/cron/index.d.ts.map +1 -1
- package/dist/src/databases/auth/auth-operations.d.ts.map +1 -1
- package/dist/src/databases/auth/password.d.ts.map +1 -1
- package/dist/src/databases/common.d.ts +7 -3
- package/dist/src/databases/common.d.ts.map +1 -1
- package/dist/src/databases/core/executor.d.ts.map +1 -1
- package/dist/src/databases/core/identifier.d.ts.map +1 -1
- package/dist/src/databases/core/query-builder.d.ts +4 -3
- package/dist/src/databases/core/query-builder.d.ts.map +1 -1
- package/dist/src/databases/directives.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/auth.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/format.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/getStructure.d.ts +13 -12
- package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/getViews.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/auth.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/format.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/getStructure.d.ts +13 -12
- package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/getViews.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/placeholders.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/auth.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/format.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/getStructure.d.ts +13 -12
- package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/getViews.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/shared/claims.d.ts.map +1 -1
- package/dist/src/databases/high-level-operations.d.ts +11 -59
- package/dist/src/databases/high-level-operations.d.ts.map +1 -1
- package/dist/src/databases/identifierMapping.test.d.ts +2 -0
- package/dist/src/databases/identifierMapping.test.d.ts.map +1 -0
- package/dist/src/databases/metadata/structure.d.ts +13 -12
- package/dist/src/databases/metadata/structure.d.ts.map +1 -1
- package/dist/src/databases/schemaBuilder/generateCreateTable/index.d.ts.map +1 -1
- package/dist/src/databases/schemaBuilder/generateCreateView/index.d.ts.map +1 -1
- package/dist/src/databases/schemaBuilder/generateInsert/index.d.ts.map +1 -1
- package/dist/src/databases/schemaBuilder/generateSchemaCreation/index.d.ts.map +1 -1
- package/dist/src/databases/schemaBuilder/mssqlToPostgres/convertView.d.ts.map +1 -1
- package/dist/src/databases/schemaBuilder/utils.d.ts +2 -2
- package/dist/src/databases/schemaBuilder/utils.d.ts.map +1 -1
- package/dist/src/databases/sqlTypeUtils.d.ts.map +1 -1
- package/dist/src/databases/transformers/data-transformers.d.ts +37 -34
- package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
- package/dist/src/databases/transformers/genResolverName.d.ts.map +1 -1
- package/dist/src/databases/transformers/graphqlName.d.ts +23 -0
- package/dist/src/databases/transformers/graphqlName.d.ts.map +1 -0
- package/dist/src/databases/transformers/graphqlName.test.d.ts +2 -0
- package/dist/src/databases/transformers/graphqlName.test.d.ts.map +1 -0
- package/dist/src/index.d.ts +6 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logging/index.d.ts.map +1 -1
- package/dist/src/queues/kafka.d.ts.map +1 -1
- package/dist/src/queues/rabbitmq.d.ts.map +1 -1
- package/dist/src/remoteREST/index.d.ts.map +1 -1
- package/dist/src/remoteREST/parse.d.ts.map +1 -1
- package/dist/src/remoteREST/proxy.d.ts.map +1 -1
- package/dist/src/remoteREST/transform.d.ts.map +1 -1
- package/dist/src/remoteSchemas/index.d.ts.map +1 -1
- package/dist/src/remoteSchemas/introspect.d.ts.map +1 -1
- package/dist/src/remoteSchemas/proxy.d.ts.map +1 -1
- package/dist/src/remoteSchemas/transform.d.ts.map +1 -1
- package/dist/src/singletons/ai.d.ts.map +1 -1
- package/dist/src/singletons/authentication.d.ts.map +1 -1
- package/dist/src/singletons/cache/lruCacheStore.d.ts.map +1 -1
- package/dist/src/singletons/cache/redisCacheStore.d.ts.map +1 -1
- package/dist/src/singletons/cache/registry.d.ts.map +1 -1
- package/dist/src/singletons/cron.d.ts.map +1 -1
- package/dist/src/singletons/databases.d.ts.map +1 -1
- package/dist/src/singletons/env.d.ts +191 -188
- package/dist/src/singletons/env.d.ts.map +1 -1
- package/dist/src/singletons/queues.d.ts.map +1 -1
- package/dist/src/subscriptions/strategies/index.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
- package/dist/src/types/db.d.ts.map +1 -1
- package/dist/src/types/env.d.ts +647 -636
- package/dist/src/types/env.d.ts.map +1 -1
- package/dist/src/types/resolver.d.ts.map +1 -1
- package/dist/src/types/zod/auth.d.ts +18 -18
- package/dist/src/types/zod/configuration.d.ts +300 -295
- package/dist/src/types/zod/configuration.d.ts.map +1 -1
- package/dist/src/types/zod/db.d.ts +89 -80
- package/dist/src/types/zod/db.d.ts.map +1 -1
- package/dist/src/types/zod/queue.d.ts +80 -80
- package/dist/src/types/zod/queue.d.ts.map +1 -1
- package/dist/src/utils/benchmark.d.ts.map +1 -1
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/safeJSONParse.d.ts.map +1 -1
- package/dist/src/utils/selection.d.ts.map +1 -1
- package/dist/src/utils/sessionVariables.d.ts +5 -1
- package/dist/src/utils/sessionVariables.d.ts.map +1 -1
- package/dist/src/utils/writeSchema.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/playgrounds/console/index.html +18 -15
- package/playgrounds/graphiql/index.html +854 -622
- package/playgrounds/scalar/index.html +5 -5
- package/src/ai/README.md +257 -0
- package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +7 -1
- package/src/analyzeQuery/resolveVariables.ts +130 -16
- package/src/authentication/jwt.ts +3 -1
- package/src/authentication/paseto.ts +3 -1
- package/src/authentication/tokenRepository.ts +12 -4
- package/src/config/types/auth.ts +7 -1
- package/src/config/types/db.ts +8 -0
- package/src/configuration/getSchemas/mergeEntities/index.ts +26 -1
- package/src/configuration/getSchemas/type-definition-generator/index.ts +11 -6
- package/src/configuration/rest/handleRESTRequestFactory.ts +5 -7
- package/src/configuration/rest/parseStringParams.ts +92 -0
- package/src/databases/common.ts +13 -2
- package/src/databases/engines/mssql/query/index.ts +25 -12
- package/src/databases/engines/mysql/connection.ts +1 -0
- package/src/databases/engines/mysql/query/index.ts +21 -8
- package/src/databases/engines/postgresql/query/index.ts +21 -7
- package/src/databases/high-level-operations.ts +2 -2
- package/src/databases/transformers/genResolverName.ts +3 -3
- package/src/databases/transformers/graphqlName.ts +28 -0
- package/src/logging/index.ts +3 -1
- package/src/types/db.ts +0 -1
- package/src/types/zod/db.ts +5 -7
- package/src/types/zod/queue.ts +2 -10
- 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
|
|
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
|
-
(
|
|
95
|
-
`${sdlDesc(description)}${
|
|
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(
|
|
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, (
|
|
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, (
|
|
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(
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
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
|
+
};
|
package/src/databases/common.ts
CHANGED
|
@@ -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((
|
|
94
|
+
.map((key) => {
|
|
89
95
|
const selector = applyDirectives(
|
|
90
|
-
`${cteAlias}.${wrapIdentifierMSSQL(
|
|
91
|
-
|
|
96
|
+
`${cteAlias}.${wrapIdentifierMSSQL(sqlName(key.name))}`,
|
|
97
|
+
key.directives,
|
|
92
98
|
"mssql",
|
|
93
99
|
variablesDefinition,
|
|
94
100
|
);
|
|
95
|
-
return `${selector} AS ${wrapIdentifierMSSQL(
|
|
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
|
-
(
|
|
136
|
-
`${tableAlias}.${wrapIdentifierMSSQL(
|
|
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(
|
|
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
|
-
(
|
|
127
|
-
`${tableAlias}.${wrapIdentifierMySQL(
|
|
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(
|
|
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
|
-
(
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/logging/index.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|