@graphoria/server 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -20
- package/dist/src/ai/mcp/index.d.ts +9 -2
- package/dist/src/ai/mcp/index.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
- package/dist/src/analyzeQuery/depthLimit.d.ts +1 -13
- package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
- package/dist/src/authentication/capabilities.d.ts +18 -0
- package/dist/src/authentication/capabilities.d.ts.map +1 -0
- package/dist/src/authentication/capabilities.test.d.ts +2 -0
- package/dist/src/authentication/capabilities.test.d.ts.map +1 -0
- package/dist/src/authentication/jwt.d.ts.map +1 -1
- package/dist/src/authentication/paseto.d.ts.map +1 -1
- package/dist/src/authentication/secrets.d.ts +9 -0
- package/dist/src/authentication/secrets.d.ts.map +1 -0
- package/dist/src/authentication/secrets.test.d.ts +2 -0
- package/dist/src/authentication/secrets.test.d.ts.map +1 -0
- package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
- package/dist/src/config/types/db.d.ts +25 -0
- package/dist/src/config/types/db.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/index.d.ts +40 -2
- package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +20 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.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 +30 -1
- 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/generateOpenAPI.test.d.ts +2 -0
- package/dist/src/configuration/rest/generateOpenAPI.test.d.ts.map +1 -0
- package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
- package/dist/src/console/api.d.ts.map +1 -1
- package/dist/src/console/session.d.ts +12 -4
- package/dist/src/console/session.d.ts.map +1 -1
- package/dist/src/cron/index.d.ts.map +1 -1
- package/dist/src/databases/common.d.ts +20 -6
- package/dist/src/databases/common.d.ts.map +1 -1
- package/dist/src/databases/core/executor.d.ts +4 -6
- package/dist/src/databases/core/executor.d.ts.map +1 -1
- package/dist/src/databases/core/function-mapping.d.ts +2 -3
- package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
- package/dist/src/databases/core/procedure-arguments.d.ts +17 -0
- package/dist/src/databases/core/procedure-arguments.d.ts.map +1 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts +2 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts.map +1 -0
- package/dist/src/databases/core/query-builder.d.ts +5 -0
- package/dist/src/databases/core/query-builder.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
- package/dist/src/databases/high-level-operations.d.ts +5 -0
- package/dist/src/databases/high-level-operations.d.ts.map +1 -1
- package/dist/src/databases/metadata/structure.d.ts +5 -0
- package/dist/src/databases/metadata/structure.d.ts.map +1 -1
- package/dist/src/databases/transformers/data-transformers.d.ts +30 -0
- package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logging/audit.d.ts +27 -0
- package/dist/src/logging/audit.d.ts.map +1 -0
- package/dist/src/logging/audit.test.d.ts +2 -0
- package/dist/src/logging/audit.test.d.ts.map +1 -0
- package/dist/src/logging/slowQuery.d.ts +29 -0
- package/dist/src/logging/slowQuery.d.ts.map +1 -0
- package/dist/src/logging/slowQuery.test.d.ts +2 -0
- package/dist/src/logging/slowQuery.test.d.ts.map +1 -0
- package/dist/src/observability/health.d.ts +23 -0
- package/dist/src/observability/health.d.ts.map +1 -0
- package/dist/src/observability/health.test.d.ts +2 -0
- package/dist/src/observability/health.test.d.ts.map +1 -0
- package/dist/src/observability/httpMetrics.d.ts +7 -0
- package/dist/src/observability/httpMetrics.d.ts.map +1 -0
- package/dist/src/observability/httpMetrics.test.d.ts +2 -0
- package/dist/src/observability/httpMetrics.test.d.ts.map +1 -0
- package/dist/src/observability/httpTracing.d.ts +11 -0
- package/dist/src/observability/httpTracing.d.ts.map +1 -0
- package/dist/src/observability/httpTracing.test.d.ts +2 -0
- package/dist/src/observability/httpTracing.test.d.ts.map +1 -0
- package/dist/src/observability/metrics.d.ts +81 -0
- package/dist/src/observability/metrics.d.ts.map +1 -0
- package/dist/src/observability/metrics.perf.test.d.ts +2 -0
- package/dist/src/observability/metrics.perf.test.d.ts.map +1 -0
- package/dist/src/observability/metrics.test.d.ts +2 -0
- package/dist/src/observability/metrics.test.d.ts.map +1 -0
- package/dist/src/observability/metricsRoute.d.ts +19 -0
- package/dist/src/observability/metricsRoute.d.ts.map +1 -0
- package/dist/src/observability/metricsRoute.test.d.ts +2 -0
- package/dist/src/observability/metricsRoute.test.d.ts.map +1 -0
- package/dist/src/observability/tracing.d.ts +92 -0
- package/dist/src/observability/tracing.d.ts.map +1 -0
- package/dist/src/observability/tracing.perf.test.d.ts +2 -0
- package/dist/src/observability/tracing.perf.test.d.ts.map +1 -0
- package/dist/src/observability/tracing.test.d.ts +2 -0
- package/dist/src/observability/tracing.test.d.ts.map +1 -0
- package/dist/src/queues/kafka.d.ts +6 -0
- package/dist/src/queues/kafka.d.ts.map +1 -1
- package/dist/src/queues/kafka.test.d.ts +2 -0
- package/dist/src/queues/kafka.test.d.ts.map +1 -0
- package/dist/src/queues/rabbitmq.d.ts.map +1 -1
- package/dist/src/remoteREST/proxy.d.ts.map +1 -1
- package/dist/src/remoteSchemas/proxy.d.ts.map +1 -1
- package/dist/src/singletons/cache/redisClient.d.ts +2 -2
- package/dist/src/singletons/cache/redisClient.d.ts.map +1 -1
- package/dist/src/singletons/databases.d.ts +1 -0
- package/dist/src/singletons/databases.d.ts.map +1 -1
- package/dist/src/singletons/env.d.ts +37 -4
- package/dist/src/singletons/env.d.ts.map +1 -1
- package/dist/src/singletons/queues.d.ts +1 -0
- package/dist/src/singletons/queues.d.ts.map +1 -1
- package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.d.ts +1 -0
- package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.test.d.ts +2 -0
- package/dist/src/subscriptions/utils/polling.test.d.ts.map +1 -0
- package/dist/src/types/env.d.ts +113 -12
- package/dist/src/types/env.d.ts.map +1 -1
- package/dist/src/types/zod/configuration.d.ts +25 -0
- package/dist/src/types/zod/configuration.d.ts.map +1 -1
- package/dist/src/types/zod/db.d.ts +85 -0
- package/dist/src/types/zod/db.d.ts.map +1 -1
- package/dist/src/utils/rateLimit.d.ts.map +1 -1
- package/dist/src/utils/redis.d.ts +17 -0
- package/dist/src/utils/redis.d.ts.map +1 -0
- package/dist/src/utils/redis.test.d.ts +2 -0
- package/dist/src/utils/redis.test.d.ts.map +1 -0
- package/dist/src/utils/responses.d.ts +6 -0
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/sessionVariables.d.ts +2 -0
- package/dist/src/utils/sessionVariables.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -5
- package/playgrounds/console/index.html +9 -9
- package/playgrounds/graphiql/index.html +549 -686
- package/playgrounds/scalar/index.html +1 -1
- package/src/ai/mcp/index.ts +19 -14
- package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +2 -1
- package/src/analyzeQuery/depthLimit.ts +15 -5
- package/src/authentication/capabilities.ts +56 -0
- package/src/authentication/index.ts +3 -3
- package/src/authentication/jwt.ts +30 -25
- package/src/authentication/paseto.ts +36 -26
- package/src/authentication/secrets.ts +30 -0
- package/src/authentication/tokenRepository.ts +2 -3
- package/src/config/types/db.ts +16 -2
- package/src/configuration/getSchemas/mergeEntities/index.ts +0 -26
- package/src/configuration/gql/gqlAuthOperations.ts +26 -0
- package/src/configuration/gql/handleGraphQLRequestFactory.ts +241 -148
- package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +9 -0
- package/src/configuration/rest/generateOpenAPI.ts +61 -11
- package/src/configuration/rest/handleRESTRequestFactory.ts +26 -0
- package/src/console/api.ts +64 -24
- package/src/console/session.ts +30 -26
- package/src/cron/index.ts +59 -36
- package/src/databases/common.ts +109 -10
- package/src/databases/core/executor.ts +97 -20
- package/src/databases/core/function-mapping.ts +1 -2
- package/src/databases/core/procedure-arguments.ts +25 -0
- package/src/databases/engines/mssql/connection.ts +0 -3
- package/src/databases/engines/mssql/query/index.ts +4 -1
- package/src/databases/engines/mysql/connection.ts +10 -7
- package/src/databases/engines/mysql/query/index.ts +5 -2
- package/src/databases/engines/postgresql/connection.ts +10 -7
- package/src/databases/engines/postgresql/query/index.ts +5 -2
- package/src/databases/high-level-operations.ts +44 -2
- package/src/index.ts +200 -76
- package/src/logging/audit.ts +86 -0
- package/src/logging/slowQuery.ts +59 -0
- package/src/observability/health.ts +67 -0
- package/src/observability/httpMetrics.ts +37 -0
- package/src/observability/httpTracing.ts +48 -0
- package/src/observability/metrics.ts +243 -0
- package/src/observability/metricsRoute.ts +33 -0
- package/src/observability/tracing.ts +421 -0
- package/src/queues/kafka.ts +45 -2
- package/src/queues/rabbitmq.ts +37 -1
- package/src/remoteREST/proxy.ts +17 -0
- package/src/remoteSchemas/proxy.ts +17 -0
- package/src/singletons/cache/redisClient.ts +4 -5
- package/src/singletons/databases.ts +5 -0
- package/src/singletons/queues.ts +9 -3
- package/src/subscriptions/strategies/database.ts +2 -0
- package/src/subscriptions/utils/polling.ts +80 -21
- package/src/types/env.ts +53 -8
- package/src/utils/rateLimit.ts +6 -1
- package/src/utils/redis.ts +71 -0
- package/src/utils/responses.ts +12 -0
- package/src/utils/sessionVariables.ts +2 -0
|
@@ -12,11 +12,12 @@ import type { SessionContext } from "../../utils/sessionVariables";
|
|
|
12
12
|
|
|
13
13
|
import { analyzeQuery } from "../../analyzeQuery";
|
|
14
14
|
import { checkQueryCost } from "../../analyzeQuery/costLimit";
|
|
15
|
-
import { depthLimitRule } from "../../analyzeQuery/depthLimit";
|
|
15
|
+
import { depthLimitRule, isDepthLimitError } from "../../analyzeQuery/depthLimit";
|
|
16
16
|
import { resolveVariableRef, resolveVariables } from "../../analyzeQuery/resolveVariables";
|
|
17
17
|
import { callStoredProcedure, executeQueryJSON, generateSQL } from "../../databases";
|
|
18
18
|
import { proxyRemoteField } from "../../remoteSchemas/proxy";
|
|
19
19
|
import { getAgent } from "../../singletons/ai";
|
|
20
|
+
import { actorFromSession, audit } from "../../logging/audit";
|
|
20
21
|
import { databasesConnections, repositoryMap } from "../../singletons/databases";
|
|
21
22
|
import { env } from "../../singletons/env";
|
|
22
23
|
import { queueManager } from "../../singletons/queues";
|
|
@@ -24,6 +25,8 @@ import { EntitySource } from "../../types/resolver";
|
|
|
24
25
|
import { filterResultBySelection } from "../../utils/selection";
|
|
25
26
|
import { handleAuthMeQuery, handleAuthMutation } from "./gqlAuthOperations";
|
|
26
27
|
import { logger } from "../../logging";
|
|
28
|
+
import { incMetric, isMetricsEnabled, observeMetric } from "../../observability/metrics";
|
|
29
|
+
import { startSpan, withActiveSpan } from "../../observability/tracing";
|
|
27
30
|
|
|
28
31
|
// Handle GraphQL query
|
|
29
32
|
export const handleGraphQLRequestFactory = (
|
|
@@ -44,7 +47,7 @@ export const handleGraphQLRequestFactory = (
|
|
|
44
47
|
) => Promise<{ data: object }>
|
|
45
48
|
>
|
|
46
49
|
> = {
|
|
47
|
-
[EntitySource.QUEUE_PUBLISHER]: async (field, variables) => {
|
|
50
|
+
[EntitySource.QUEUE_PUBLISHER]: async (field, variables, _queryAnalysis, _req, session) => {
|
|
48
51
|
const data = resolveVariableRef(variables, field.arguments?.data);
|
|
49
52
|
|
|
50
53
|
const publisher = entities.queuesMap[field.name];
|
|
@@ -53,6 +56,12 @@ export const handleGraphQLRequestFactory = (
|
|
|
53
56
|
throw new Error(`Queue publisher not found: ${field.name}`);
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
audit().emit({
|
|
60
|
+
action: "queue.publish",
|
|
61
|
+
actor: actorFromSession(session),
|
|
62
|
+
target: { kind: "publisher", name: publisher.resolverName },
|
|
63
|
+
});
|
|
64
|
+
|
|
56
65
|
return {
|
|
57
66
|
data: {
|
|
58
67
|
[field.alias || field.name]: await queueManager?.sendMessage(
|
|
@@ -100,7 +109,7 @@ export const handleGraphQLRequestFactory = (
|
|
|
100
109
|
};
|
|
101
110
|
},
|
|
102
111
|
|
|
103
|
-
[EntitySource.STORED_PROCEDURE]: async (field, variables, queryAnalysis) => {
|
|
112
|
+
[EntitySource.STORED_PROCEDURE]: async (field, variables, queryAnalysis, _req, session) => {
|
|
104
113
|
const sp = entities.mutationsMap[field.name];
|
|
105
114
|
|
|
106
115
|
if (!sp) {
|
|
@@ -116,8 +125,15 @@ export const handleGraphQLRequestFactory = (
|
|
|
116
125
|
|
|
117
126
|
const result = await callStoredProcedure(
|
|
118
127
|
sp,
|
|
119
|
-
queryAnalysis.operations[0].variables ?? [],
|
|
120
128
|
argumentsReplaced as Record<string, string | number | boolean | null>,
|
|
129
|
+
{
|
|
130
|
+
operation: {
|
|
131
|
+
type: "mutation",
|
|
132
|
+
name: queryAnalysis.operations[0]?.name ?? null,
|
|
133
|
+
fields: [field.name],
|
|
134
|
+
},
|
|
135
|
+
role: session?.role,
|
|
136
|
+
},
|
|
121
137
|
);
|
|
122
138
|
|
|
123
139
|
return {
|
|
@@ -230,6 +246,7 @@ export const handleGraphQLRequestFactory = (
|
|
|
230
246
|
) => {
|
|
231
247
|
const enforceDepthLimit = options?.enforceDepthLimit ?? true;
|
|
232
248
|
const enforceCostLimit = options?.enforceCostLimit ?? true;
|
|
249
|
+
let costRejected = false;
|
|
233
250
|
const entry = getCacheEntry(query);
|
|
234
251
|
// Unparseable queries aren't cached; let parse surface the syntax error
|
|
235
252
|
const document = entry?.document ?? parse(query);
|
|
@@ -257,7 +274,20 @@ export const handleGraphQLRequestFactory = (
|
|
|
257
274
|
env.maxQueryCost,
|
|
258
275
|
);
|
|
259
276
|
|
|
260
|
-
if (costError)
|
|
277
|
+
if (costError) {
|
|
278
|
+
validationErrors = [costError];
|
|
279
|
+
costRejected = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (validationErrors.length > 0) {
|
|
284
|
+
incMetric("graphoria_graphql_rejections_total", {
|
|
285
|
+
reason: costRejected
|
|
286
|
+
? "cost"
|
|
287
|
+
: validationErrors.some(isDepthLimitError)
|
|
288
|
+
? "depth"
|
|
289
|
+
: "validation",
|
|
290
|
+
});
|
|
261
291
|
}
|
|
262
292
|
|
|
263
293
|
return {
|
|
@@ -289,180 +319,243 @@ export const handleGraphQLRequestFactory = (
|
|
|
289
319
|
const startTime = Bun.nanoseconds();
|
|
290
320
|
|
|
291
321
|
// Reuse cached analysis on repeated identical queries
|
|
322
|
+
const analyzeSpan = startSpan("graphoria.analyze");
|
|
292
323
|
const entry = isString(query) ? getCacheEntry(query) : undefined;
|
|
293
324
|
let queryAnalysis: AnalysisResult;
|
|
294
325
|
if (entry?.analysis) {
|
|
295
326
|
queryAnalysis = entry.analysis;
|
|
327
|
+
analyzeSpan?.setAttribute("graphoria.analysis.cached", true);
|
|
296
328
|
} else {
|
|
297
329
|
queryAnalysis = isString(query) ? analyzeQuery(query, entities, gqlEntities.schema) : query;
|
|
298
330
|
if (entry) entry.analysis = queryAnalysis;
|
|
331
|
+
analyzeSpan?.setAttribute("graphoria.analysis.cached", false);
|
|
299
332
|
}
|
|
333
|
+
analyzeSpan?.end();
|
|
300
334
|
|
|
301
335
|
if (queryAnalysis.operations.length === 0) {
|
|
302
336
|
return { data: {} };
|
|
303
337
|
}
|
|
304
338
|
|
|
305
339
|
const operation = queryAnalysis.operations[0];
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
(field) => field.source === EntitySource.REMOTE_SCHEMA,
|
|
325
|
-
);
|
|
326
|
-
const aiFields = resolved.fields.filter((field) => field.source === EntitySource.AI);
|
|
327
|
-
const tableFields = resolved.fields.filter(
|
|
328
|
-
(field) =>
|
|
329
|
-
field.source !== EntitySource.AUTH &&
|
|
330
|
-
field.source !== EntitySource.REMOTE_SCHEMA &&
|
|
331
|
-
field.source !== EntitySource.AI,
|
|
340
|
+
// An anonymous operation is named by its root fields, exactly as the slow
|
|
341
|
+
// query log names one: the document itself would carry inline literals,
|
|
342
|
+
// which are caller data.
|
|
343
|
+
const operationName =
|
|
344
|
+
operation.name ?? (operation.fields ?? []).map((field) => field.name).join(",");
|
|
345
|
+
const role = session?.role ?? "anonymous";
|
|
346
|
+
const recordOperation = (outcome: "success" | "error") => {
|
|
347
|
+
if (!isMetricsEnabled()) return;
|
|
348
|
+
const labels = {
|
|
349
|
+
operation: operationName,
|
|
350
|
+
type: operation.operation,
|
|
351
|
+
role,
|
|
352
|
+
};
|
|
353
|
+
incMetric("graphoria_graphql_operations_total", { ...labels, outcome });
|
|
354
|
+
observeMetric(
|
|
355
|
+
"graphoria_graphql_operation_duration_seconds",
|
|
356
|
+
labels,
|
|
357
|
+
(Bun.nanoseconds() - startTime) / 1e9,
|
|
332
358
|
);
|
|
359
|
+
};
|
|
360
|
+
let outcome: "success" | "error" = "success";
|
|
361
|
+
const span = startSpan(`${operation.operation} ${operationName}`, {
|
|
362
|
+
attributes: {
|
|
363
|
+
"graphql.operation.name": operationName,
|
|
364
|
+
"graphql.operation.type": operation.operation,
|
|
365
|
+
"graphoria.role": role,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
333
368
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// Handle remote schema queries in parallel
|
|
341
|
-
let remoteData: Record<string, unknown> = {};
|
|
342
|
-
if (remoteFields.length > 0) {
|
|
343
|
-
const remoteResults = await Promise.all(
|
|
344
|
-
remoteFields.map(async (field) => {
|
|
345
|
-
const entry = entities.remoteQueriesMap[field.name];
|
|
346
|
-
if (!entry) {
|
|
347
|
-
throw new Error(`Remote schema query not found: ${field.name}`);
|
|
348
|
-
}
|
|
349
|
-
const result = await proxyRemoteField(
|
|
350
|
-
field,
|
|
351
|
-
entry.remoteSchema,
|
|
352
|
-
entry.originalFieldName,
|
|
353
|
-
resolved.allVariables,
|
|
354
|
-
"query",
|
|
355
|
-
req,
|
|
356
|
-
);
|
|
357
|
-
return { [field.alias || field.name]: result };
|
|
358
|
-
}),
|
|
359
|
-
);
|
|
360
|
-
remoteData = remoteResults.reduce((acc, curr) => Object.assign(acc, curr), {});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Handle AI agent queries (admin-only `ask` field)
|
|
364
|
-
let aiData: Record<string, unknown> = {};
|
|
365
|
-
for (const field of aiFields) {
|
|
366
|
-
const alias = field.alias || field.name;
|
|
367
|
-
const prompt = field.arguments?.prompt;
|
|
368
|
-
aiData[alias] = await getAgent()(String(prompt ?? ""));
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Skip SQL generation if there are no table fields
|
|
372
|
-
if (tableFields.length === 0) {
|
|
369
|
+
// The span is entered rather than passed: the statements this operation
|
|
370
|
+
// runs reach the executor through call chains that carry no span, and the
|
|
371
|
+
// async context is what joins them to this one.
|
|
372
|
+
return withActiveSpan(span, async () => {
|
|
373
|
+
try {
|
|
373
374
|
log.debug(
|
|
374
|
-
{ durationMs: (Bun.nanoseconds() - startTime) / 1e6 },
|
|
375
|
-
"graphql request completed (no table fields)",
|
|
376
|
-
);
|
|
377
|
-
return { data: { ...authData, ...remoteData, ...aiData } };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const tableQueryAnalysis = {
|
|
381
|
-
...queryAnalysis,
|
|
382
|
-
operations: [
|
|
383
375
|
{
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
376
|
+
operation: operation.operation,
|
|
377
|
+
name: operation.name,
|
|
378
|
+
fieldCount: operation.fields?.length,
|
|
379
|
+
queryLength: isString(query) ? query.length : undefined,
|
|
387
380
|
},
|
|
388
|
-
|
|
389
|
-
|
|
381
|
+
"graphql request",
|
|
382
|
+
);
|
|
390
383
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
resolved.allVariables,
|
|
395
|
-
false,
|
|
396
|
-
(options?.enforcePageLimits ?? true) ? pageLimits : null,
|
|
397
|
-
options?.timeoutMs,
|
|
398
|
-
);
|
|
384
|
+
// Single pass: validate, flatten object vars, resolve field args + session vars
|
|
385
|
+
// Returns an immutable ResolvedOperation — original operation is not mutated
|
|
386
|
+
const resolved = resolveVariables(operation, variables, session);
|
|
399
387
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
|
|
388
|
+
if (operation.operation === "query") {
|
|
389
|
+
// Separate auth fields, remote schema fields, and table fields
|
|
390
|
+
const authFields = resolved.fields.filter(
|
|
391
|
+
(field) => field.source === EntitySource.AUTH,
|
|
392
|
+
);
|
|
393
|
+
const remoteFields = resolved.fields.filter(
|
|
394
|
+
(field) => field.source === EntitySource.REMOTE_SCHEMA,
|
|
395
|
+
);
|
|
396
|
+
const aiFields = resolved.fields.filter((field) => field.source === EntitySource.AI);
|
|
397
|
+
const tableFields = resolved.fields.filter(
|
|
398
|
+
(field) =>
|
|
399
|
+
field.source !== EntitySource.AUTH &&
|
|
400
|
+
field.source !== EntitySource.REMOTE_SCHEMA &&
|
|
401
|
+
field.source !== EntitySource.AI,
|
|
402
|
+
);
|
|
411
403
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
404
|
+
// Handle auth queries (e.g. auth_me)
|
|
405
|
+
let authData: Record<string, unknown> = {};
|
|
406
|
+
for (const field of authFields) {
|
|
407
|
+
Object.assign(authData, handleAuthMeQuery(field, session));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Handle remote schema queries in parallel
|
|
411
|
+
let remoteData: Record<string, unknown> = {};
|
|
412
|
+
if (remoteFields.length > 0) {
|
|
413
|
+
const remoteResults = await Promise.all(
|
|
414
|
+
remoteFields.map(async (field) => {
|
|
415
|
+
const entry = entities.remoteQueriesMap[field.name];
|
|
416
|
+
if (!entry) {
|
|
417
|
+
throw new Error(`Remote schema query not found: ${field.name}`);
|
|
418
|
+
}
|
|
419
|
+
const result = await proxyRemoteField(
|
|
420
|
+
field,
|
|
421
|
+
entry.remoteSchema,
|
|
422
|
+
entry.originalFieldName,
|
|
423
|
+
resolved.allVariables,
|
|
424
|
+
"query",
|
|
425
|
+
req,
|
|
426
|
+
);
|
|
427
|
+
return { [field.alias || field.name]: result };
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
remoteData = remoteResults.reduce((acc, curr) => Object.assign(acc, curr), {});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Handle AI agent queries (admin-only `ask` field)
|
|
434
|
+
let aiData: Record<string, unknown> = {};
|
|
435
|
+
for (const field of aiFields) {
|
|
436
|
+
const alias = field.alias || field.name;
|
|
437
|
+
const prompt = String(field.arguments?.prompt ?? "");
|
|
438
|
+
audit().emit({
|
|
439
|
+
action: "ai.ask",
|
|
440
|
+
actor: actorFromSession(session),
|
|
441
|
+
target: { kind: "ai", via: "graphql" },
|
|
442
|
+
prompt,
|
|
443
|
+
});
|
|
444
|
+
aiData[alias] = await getAgent()(prompt);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Skip SQL generation if there are no table fields
|
|
448
|
+
if (tableFields.length === 0) {
|
|
449
|
+
log.debug(
|
|
450
|
+
{ durationMs: (Bun.nanoseconds() - startTime) / 1e6 },
|
|
451
|
+
"graphql request completed (no table fields)",
|
|
452
|
+
);
|
|
453
|
+
return { data: { ...authData, ...remoteData, ...aiData } };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const tableQueryAnalysis = {
|
|
457
|
+
...queryAnalysis,
|
|
458
|
+
operations: [
|
|
459
|
+
{
|
|
460
|
+
...operation,
|
|
461
|
+
fields: tableFields,
|
|
462
|
+
variables: resolved.variables,
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
};
|
|
416
466
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
: {}),
|
|
426
|
-
data: data.reduce((acc, curr) => Object.assign(acc, curr), {
|
|
427
|
-
...authData,
|
|
428
|
-
...remoteData,
|
|
429
|
-
...aiData,
|
|
430
|
-
}),
|
|
431
|
-
};
|
|
432
|
-
} else if (operation.operation === "mutation") {
|
|
433
|
-
// Route mutation to the appropriate handler based on field source
|
|
434
|
-
let results: Record<string, object> = {};
|
|
467
|
+
const sqlQueries = generateSQL(
|
|
468
|
+
entities,
|
|
469
|
+
tableQueryAnalysis,
|
|
470
|
+
resolved.allVariables,
|
|
471
|
+
false,
|
|
472
|
+
(options?.enforcePageLimits ?? true) ? pageLimits : null,
|
|
473
|
+
options?.timeoutMs,
|
|
474
|
+
);
|
|
435
475
|
|
|
436
|
-
|
|
437
|
-
|
|
476
|
+
const data = await Promise.all<object>(
|
|
477
|
+
sqlQueries.map(([db, query]) =>
|
|
478
|
+
executeQueryJSON(
|
|
479
|
+
query,
|
|
480
|
+
db,
|
|
481
|
+
resolved.variables,
|
|
482
|
+
resolved.allVariables as Record<string, string | number | boolean | null>,
|
|
483
|
+
options?.timeoutMs,
|
|
484
|
+
{
|
|
485
|
+
operation: {
|
|
486
|
+
type: operation.operation,
|
|
487
|
+
name: operation.name,
|
|
488
|
+
fields: tableFields.map((field) => field.name),
|
|
489
|
+
},
|
|
490
|
+
role: session?.role,
|
|
491
|
+
},
|
|
492
|
+
),
|
|
493
|
+
),
|
|
494
|
+
);
|
|
438
495
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
resolved.allVariables,
|
|
443
|
-
queryAnalysis,
|
|
444
|
-
req,
|
|
445
|
-
session,
|
|
496
|
+
log.debug(
|
|
497
|
+
{ durationMs: (Bun.nanoseconds() - startTime) / 1e6, dbCount: sqlQueries.length },
|
|
498
|
+
"graphql request completed",
|
|
446
499
|
);
|
|
447
500
|
|
|
448
|
-
|
|
449
|
-
...
|
|
450
|
-
|
|
501
|
+
return {
|
|
502
|
+
...(env.queryOnResponse
|
|
503
|
+
? {
|
|
504
|
+
sqlQueries: sqlQueries.map(([db, query]) => ({
|
|
505
|
+
db: db.name,
|
|
506
|
+
query,
|
|
507
|
+
})),
|
|
508
|
+
}
|
|
509
|
+
: {}),
|
|
510
|
+
data: data.reduce((acc, curr) => Object.assign(acc, curr), {
|
|
511
|
+
...authData,
|
|
512
|
+
...remoteData,
|
|
513
|
+
...aiData,
|
|
514
|
+
}),
|
|
451
515
|
};
|
|
452
|
-
}
|
|
453
|
-
|
|
516
|
+
} else if (operation.operation === "mutation") {
|
|
517
|
+
// Route mutation to the appropriate handler based on field source
|
|
518
|
+
let results: Record<string, object> = {};
|
|
519
|
+
|
|
520
|
+
for await (const field of resolved.fields) {
|
|
521
|
+
const source = field.source;
|
|
522
|
+
|
|
523
|
+
if (source && mutationHandlers[source]) {
|
|
524
|
+
const result = await mutationHandlers[source]!(
|
|
525
|
+
field,
|
|
526
|
+
resolved.allVariables,
|
|
527
|
+
queryAnalysis,
|
|
528
|
+
req,
|
|
529
|
+
session,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
results = {
|
|
533
|
+
...results,
|
|
534
|
+
...result.data,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
454
538
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
539
|
+
log.debug(
|
|
540
|
+
{ durationMs: (Bun.nanoseconds() - startTime) / 1e6 },
|
|
541
|
+
"graphql request completed",
|
|
542
|
+
);
|
|
459
543
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
544
|
+
return {
|
|
545
|
+
data: results,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
464
548
|
|
|
465
|
-
|
|
549
|
+
return { data: {} };
|
|
550
|
+
} catch (error) {
|
|
551
|
+
outcome = "error";
|
|
552
|
+
span?.recordError(error);
|
|
553
|
+
throw error;
|
|
554
|
+
} finally {
|
|
555
|
+
recordOperation(outcome);
|
|
556
|
+
span?.end();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
466
559
|
},
|
|
467
560
|
};
|
|
468
561
|
|
|
@@ -10,6 +10,7 @@ import { getTokenService } from "../../singletons/authentication";
|
|
|
10
10
|
import { createSubscriptionStrategyRegistry, getStrategyForSource } from "../../subscriptions";
|
|
11
11
|
import { createQueryEventEmitter } from "../../utils/event-emitter";
|
|
12
12
|
import { logger } from "../../logging";
|
|
13
|
+
import { audit } from "../../logging/audit";
|
|
13
14
|
|
|
14
15
|
export const queryEventEmitter = createQueryEventEmitter();
|
|
15
16
|
|
|
@@ -60,6 +61,14 @@ const handleGraphQLSubscriptionFactory = (
|
|
|
60
61
|
|
|
61
62
|
subscriptionMapping.set(ws, session);
|
|
62
63
|
|
|
64
|
+
if (session.authMethod === "admin_secret") {
|
|
65
|
+
audit().emit({
|
|
66
|
+
action: "admin_secret.used",
|
|
67
|
+
actor: { type: "admin_secret", ip: ws.remoteAddress },
|
|
68
|
+
target: { kind: "websocket" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
logger("subscriptions").debug({ role: session.role }, "client connected");
|
|
64
73
|
|
|
65
74
|
ws.send(
|
|
@@ -1,38 +1,77 @@
|
|
|
1
|
+
import { GraphQLObjectType } from "graphql";
|
|
2
|
+
|
|
3
|
+
import type { GraphQLType } from "graphql";
|
|
1
4
|
import type { OpenAPIV3_1 } from "openapi-types";
|
|
2
5
|
import type { SelectionAnalysis } from "../../analyzeQuery/types";
|
|
3
6
|
import type { GetSchemaReturn } from "../../configuration/getSchemas";
|
|
4
7
|
import type { RemoteRESTResolved } from "../../remoteREST/types";
|
|
5
8
|
import type { Env } from "../../types/env";
|
|
6
9
|
|
|
10
|
+
import { getFieldType, unwrapType } from "../../analyzeQuery/typeUtils";
|
|
7
11
|
import { convertFromZod, errors } from "./openApiErrors";
|
|
8
12
|
|
|
13
|
+
// Deliberately coarse: Int and Float both map to "number", so a column keeps
|
|
14
|
+
// the OpenAPI type it had when types were read off the SQL data type.
|
|
15
|
+
const openApiTypeOf = (namedType: GraphQLType | undefined) => {
|
|
16
|
+
const name = namedType && "name" in namedType ? namedType.name : undefined;
|
|
17
|
+
|
|
18
|
+
switch (name) {
|
|
19
|
+
case "Int":
|
|
20
|
+
case "Float":
|
|
21
|
+
return "number";
|
|
22
|
+
case "Boolean":
|
|
23
|
+
return "boolean";
|
|
24
|
+
default:
|
|
25
|
+
return "string";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const resolveFieldType = (parentType: GraphQLObjectType | undefined, fieldName: string) => {
|
|
30
|
+
const fieldType = parentType && getFieldType(parentType, fieldName);
|
|
31
|
+
const namedType = fieldType ? unwrapType(fieldType).type : undefined;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
namedType,
|
|
35
|
+
objectType: namedType instanceof GraphQLObjectType ? namedType : undefined,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolves selections against the GraphQL schema rather than against a table
|
|
41
|
+
* name. `<table>_aggregate` returns a GroupBy wrapper whose fields — `key`,
|
|
42
|
+
* `count`, `items`, `min`/`max`/`sum`/`avg` — are not tables, so looking a
|
|
43
|
+
* selection's parent name up in the entity map typed everything under an
|
|
44
|
+
* aggregate as a string. Walking the schema also makes sanitised column names
|
|
45
|
+
* resolve, since the schema is what the client actually queries.
|
|
46
|
+
*/
|
|
9
47
|
const processSelections = (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
selections: SelectionAnalysis[] = [],
|
|
48
|
+
parentType: GraphQLObjectType | undefined,
|
|
49
|
+
fieldParent: SelectionAnalysis,
|
|
13
50
|
) =>
|
|
14
|
-
selections
|
|
15
|
-
const
|
|
51
|
+
fieldParent.selections?.reduce<Record<string, unknown>>((acc, field) => {
|
|
52
|
+
const { namedType, objectType } = resolveFieldType(parentType, field.name);
|
|
16
53
|
|
|
17
54
|
if (field.isArray) {
|
|
18
55
|
acc[field.alias || field.name] = {
|
|
19
56
|
type: "array",
|
|
20
57
|
items: {
|
|
21
58
|
type: "object",
|
|
22
|
-
properties: processSelections(
|
|
59
|
+
properties: processSelections(objectType, field),
|
|
23
60
|
required: (field.selections ?? [])
|
|
24
61
|
.filter((f) => f.isRequired)
|
|
25
62
|
.map((f) => f.alias || f.name),
|
|
63
|
+
nullable: !field.isRequired,
|
|
26
64
|
},
|
|
27
65
|
};
|
|
28
66
|
} else {
|
|
29
67
|
acc[field.alias || field.name] = field.selections
|
|
30
68
|
? {
|
|
31
69
|
type: "object",
|
|
32
|
-
properties: processSelections(
|
|
70
|
+
properties: processSelections(objectType, field),
|
|
33
71
|
required: field.selections.filter((f) => f.isRequired).map((f) => f.alias || f.name),
|
|
72
|
+
nullable: !field.isRequired,
|
|
34
73
|
}
|
|
35
|
-
: { type };
|
|
74
|
+
: { type: openApiTypeOf(namedType), nullable: !field.isRequired };
|
|
36
75
|
}
|
|
37
76
|
|
|
38
77
|
return acc;
|
|
@@ -137,7 +176,12 @@ export const generateOpenAPI = ({
|
|
|
137
176
|
const responseDataSchema = value.output
|
|
138
177
|
? convertFromZod(value.output)
|
|
139
178
|
: (() => {
|
|
140
|
-
const
|
|
179
|
+
const operation = value.queryStructure!.operations[0];
|
|
180
|
+
const fields = operation.fields;
|
|
181
|
+
const rootType =
|
|
182
|
+
(operation.operation === "mutation"
|
|
183
|
+
? schema.schema.getMutationType()
|
|
184
|
+
: schema.schema.getQueryType()) ?? undefined;
|
|
141
185
|
return {
|
|
142
186
|
type: "object" as const,
|
|
143
187
|
properties: fields.reduce<Record<string, unknown>>((acc, field) => {
|
|
@@ -146,7 +190,10 @@ export const generateOpenAPI = ({
|
|
|
146
190
|
type: "array",
|
|
147
191
|
items: {
|
|
148
192
|
type: "object",
|
|
149
|
-
properties: processSelections(
|
|
193
|
+
properties: processSelections(
|
|
194
|
+
resolveFieldType(rootType, field.name).objectType,
|
|
195
|
+
field,
|
|
196
|
+
),
|
|
150
197
|
required: (field.selections ?? [])
|
|
151
198
|
.filter((f) => f.isRequired)
|
|
152
199
|
.map((f) => f.alias || f.name),
|
|
@@ -155,7 +202,10 @@ export const generateOpenAPI = ({
|
|
|
155
202
|
} else {
|
|
156
203
|
acc[field.alias || field.name] = {
|
|
157
204
|
type: "object",
|
|
158
|
-
properties: processSelections(
|
|
205
|
+
properties: processSelections(
|
|
206
|
+
resolveFieldType(rootType, field.name).objectType,
|
|
207
|
+
field,
|
|
208
|
+
),
|
|
159
209
|
required: (field.selections ?? [])
|
|
160
210
|
.filter((f) => f.isRequired)
|
|
161
211
|
.map((f) => f.alias || f.name),
|