@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
|
@@ -50,6 +50,11 @@ export const instantiateDatabasesConnections = async (databases: Database[]) =>
|
|
|
50
50
|
return { databasesConnections, repositoryMap };
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
export const pingConnection = (connection: SQL | ConnectionPool, type: string) =>
|
|
54
|
+
type === "mssql"
|
|
55
|
+
? (connection as ConnectionPool).query("SELECT 1")
|
|
56
|
+
: (connection as SQL).unsafe("SELECT 1");
|
|
57
|
+
|
|
53
58
|
/**
|
|
54
59
|
* Close every open database connection and clear the singleton maps. Bun's `SQL`
|
|
55
60
|
* and mssql's `ConnectionPool` both expose `close()`. Used by
|
package/src/singletons/queues.ts
CHANGED
|
@@ -6,7 +6,11 @@ import { startKafkaConnections } from "../queues/kafka";
|
|
|
6
6
|
import { startRabbitMQConnections } from "../queues/rabbitmq";
|
|
7
7
|
import { logger } from "../logging";
|
|
8
8
|
|
|
9
|
-
export type QueueConnectionStatus = {
|
|
9
|
+
export type QueueConnectionStatus = {
|
|
10
|
+
type: "rabbitmq" | "kafka";
|
|
11
|
+
name: string;
|
|
12
|
+
connected: boolean;
|
|
13
|
+
};
|
|
10
14
|
|
|
11
15
|
export type QueueManager = {
|
|
12
16
|
publisherMap: () => Record<string, RabbitMQPublisher | KafkaPublisher>;
|
|
@@ -40,8 +44,9 @@ export const instantiateQueues = async (queues: QueueConfig[]) => {
|
|
|
40
44
|
publisherMap: rabbitMQManager.publisherMap,
|
|
41
45
|
sendMessage: rabbitMQManager.sendMessage,
|
|
42
46
|
connections: () =>
|
|
43
|
-
rabbitMQManager.managers.map((manager) => ({
|
|
47
|
+
rabbitMQManager.managers.map((manager, index) => ({
|
|
44
48
|
type: "rabbitmq" as const,
|
|
49
|
+
name: rabbitMQQueues[index]!.name,
|
|
45
50
|
connected: manager.isConnected(),
|
|
46
51
|
})),
|
|
47
52
|
});
|
|
@@ -55,8 +60,9 @@ export const instantiateQueues = async (queues: QueueConfig[]) => {
|
|
|
55
60
|
publisherMap: kafkaManager.publisherMap,
|
|
56
61
|
sendMessage: kafkaManager.sendMessage,
|
|
57
62
|
connections: () =>
|
|
58
|
-
kafkaManager.managers.map((manager) => ({
|
|
63
|
+
kafkaManager.managers.map((manager, index) => ({
|
|
59
64
|
type: "kafka" as const,
|
|
65
|
+
name: kafkaQueues[index]!.name,
|
|
60
66
|
connected: manager.isConnected(),
|
|
61
67
|
})),
|
|
62
68
|
cleanup: kafkaManager.cleanup,
|
|
@@ -46,6 +46,7 @@ export const createDatabaseSubscriptionStrategy = (): SubscriptionStrategy => ({
|
|
|
46
46
|
variables,
|
|
47
47
|
schemaEntity,
|
|
48
48
|
eventEmitter,
|
|
49
|
+
session,
|
|
49
50
|
} = context;
|
|
50
51
|
|
|
51
52
|
const subscriptionKey = this.getSubscriptionKey(context);
|
|
@@ -69,6 +70,7 @@ export const createDatabaseSubscriptionStrategy = (): SubscriptionStrategy => ({
|
|
|
69
70
|
subscriptionKey,
|
|
70
71
|
eventEmitter,
|
|
71
72
|
pollIntervalMs: 1000,
|
|
73
|
+
role: session.role,
|
|
72
74
|
});
|
|
73
75
|
|
|
74
76
|
// Store poll reference for cleanup by event emitter
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { AnalysisResult, VariableDefinition } from "../../analyzeQuery/types";
|
|
2
2
|
import type { GetSchemaReturn } from "../../configuration/getSchemas";
|
|
3
|
+
import type { QuerySource } from "../../logging/slowQuery";
|
|
3
4
|
import type { Database } from "../../types/configuration";
|
|
4
5
|
import type { DatabasePoller, QueryEventEmitter } from "../types";
|
|
5
6
|
|
|
6
7
|
import { executeQuery, executeQueryJSON, generateSQL } from "../../databases";
|
|
7
8
|
import { logger } from "../../logging";
|
|
9
|
+
import { withSpan } from "../../observability/tracing";
|
|
8
10
|
import { env } from "../../singletons/env";
|
|
9
11
|
|
|
10
12
|
export interface DatabasePollerConfig {
|
|
@@ -15,6 +17,7 @@ export interface DatabasePollerConfig {
|
|
|
15
17
|
subscriptionKey: string;
|
|
16
18
|
eventEmitter: QueryEventEmitter;
|
|
17
19
|
pollIntervalMs?: number;
|
|
20
|
+
role?: string | undefined;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -32,12 +35,15 @@ const getResultHash = async (
|
|
|
32
35
|
db: Database,
|
|
33
36
|
variableDefinitions: VariableDefinition[],
|
|
34
37
|
variables: Record<string, unknown>,
|
|
38
|
+
source: QuerySource,
|
|
35
39
|
): Promise<string> => {
|
|
36
40
|
const hashResult = await executeQuery<{ ResultHash: string }>(
|
|
37
41
|
queryHash,
|
|
38
42
|
db,
|
|
39
43
|
variableDefinitions,
|
|
40
44
|
variables,
|
|
45
|
+
undefined,
|
|
46
|
+
source,
|
|
41
47
|
);
|
|
42
48
|
return Buffer.from(hashResult[0].ResultHash)?.toString("hex");
|
|
43
49
|
};
|
|
@@ -56,6 +62,7 @@ export const createDatabasePoller = async (
|
|
|
56
62
|
subscriptionKey,
|
|
57
63
|
eventEmitter,
|
|
58
64
|
pollIntervalMs = 1000,
|
|
65
|
+
role,
|
|
59
66
|
} = config;
|
|
60
67
|
|
|
61
68
|
const firstFieldName = getFirstFieldName(analysis);
|
|
@@ -67,35 +74,87 @@ export const createDatabasePoller = async (
|
|
|
67
74
|
const [[, queryData]] = generateSQL(schemaEntity, analysis, variables, false, pageLimits);
|
|
68
75
|
const [[, queryHash]] = generateSQL(schemaEntity, analysis, variables, true, pageLimits);
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
const operation = analysis.operations[0];
|
|
78
|
+
const source: QuerySource = {
|
|
79
|
+
operation: {
|
|
80
|
+
type: operation.operation,
|
|
81
|
+
name: operation.name,
|
|
82
|
+
fields: operation.fields.map((field) => field.name),
|
|
83
|
+
},
|
|
84
|
+
role,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// A poll is not inside a request, so each one roots its own trace. The span is
|
|
88
|
+
// entered, which is what puts the statements it runs underneath it. An empty
|
|
89
|
+
// poll still gets a span — an empty poll that is slow is worth seeing.
|
|
90
|
+
const pollSpan = <T>(run: () => Promise<T>) =>
|
|
91
|
+
withSpan(
|
|
92
|
+
"subscription.poll",
|
|
93
|
+
{
|
|
94
|
+
parent: null,
|
|
95
|
+
attributes: {
|
|
96
|
+
"graphql.operation.name": source.operation.name ?? undefined,
|
|
97
|
+
"graphql.operation.type": source.operation.type,
|
|
98
|
+
"graphoria.role": source.role,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
run,
|
|
102
|
+
);
|
|
72
103
|
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
// Get initial hash and send initial data
|
|
105
|
+
let previousHash = "";
|
|
106
|
+
|
|
107
|
+
await pollSpan(async () => {
|
|
108
|
+
previousHash = await getResultHash(queryHash, db, variableDefinitions, variables, source);
|
|
109
|
+
|
|
110
|
+
eventEmitter.sendDataUpdate(subscriptionKey, {
|
|
111
|
+
data: await executeQueryJSON(
|
|
112
|
+
queryData,
|
|
113
|
+
db,
|
|
114
|
+
variableDefinitions,
|
|
115
|
+
variables,
|
|
116
|
+
undefined,
|
|
117
|
+
source,
|
|
118
|
+
),
|
|
119
|
+
});
|
|
75
120
|
});
|
|
76
121
|
|
|
77
122
|
// Poll function that checks for changes
|
|
78
123
|
const log = logger("polling").child({ subscription: subscriptionKey });
|
|
79
|
-
const poll =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
124
|
+
const poll = () =>
|
|
125
|
+
pollSpan(async () => {
|
|
126
|
+
try {
|
|
127
|
+
const currentHash = await getResultHash(
|
|
128
|
+
queryHash,
|
|
129
|
+
db,
|
|
130
|
+
variableDefinitions,
|
|
131
|
+
variables,
|
|
132
|
+
source,
|
|
87
133
|
);
|
|
88
134
|
|
|
89
|
-
previousHash
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
135
|
+
if (currentHash !== previousHash) {
|
|
136
|
+
log.info(
|
|
137
|
+
{ changed: !!previousHash, operation: analysis.operations[0].name },
|
|
138
|
+
previousHash ? "data changed" : "initial fetch",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
previousHash = currentHash;
|
|
142
|
+
|
|
143
|
+
eventEmitter.sendDataUpdate(subscriptionKey, {
|
|
144
|
+
data: await executeQueryJSON(
|
|
145
|
+
queryData,
|
|
146
|
+
db,
|
|
147
|
+
variableDefinitions,
|
|
148
|
+
variables,
|
|
149
|
+
undefined,
|
|
150
|
+
source,
|
|
151
|
+
),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
log.error({ err: error }, "polling failed");
|
|
94
156
|
}
|
|
95
|
-
}
|
|
96
|
-
log.error({ err: error }, "polling failed");
|
|
97
|
-
}
|
|
98
|
-
};
|
|
157
|
+
});
|
|
99
158
|
|
|
100
159
|
// Poller control
|
|
101
160
|
let stopped = false;
|
package/src/types/env.ts
CHANGED
|
@@ -3,19 +3,29 @@ import { z } from "zod";
|
|
|
3
3
|
import { version } from "../../package.json";
|
|
4
4
|
import { ConfigurationZod } from "./zod/configuration";
|
|
5
5
|
|
|
6
|
+
// A secret env var is a comma-separated list: the first entry is the one in
|
|
7
|
+
// use, the rest are still accepted so a rotation can overlap instead of cutting
|
|
8
|
+
// over. Blank entries are dropped, so an unset var is an empty list.
|
|
9
|
+
const splitSecrets = (value: string) =>
|
|
10
|
+
value
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((entry) => entry.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
const secretList = z.string().default("").transform(splitSecrets);
|
|
15
|
+
|
|
6
16
|
export const EnvZod = z
|
|
7
17
|
.object({
|
|
8
18
|
PORT: z.coerce.number().default(3000),
|
|
9
19
|
NODE_ENV: z.string().default("DEVELOPMENT"),
|
|
10
|
-
ADMIN_SECRET: z.string(),
|
|
20
|
+
ADMIN_SECRET: z.string().transform(splitSecrets),
|
|
11
21
|
CONFIGURATION: z.union([z.string(), ConfigurationZod]).optional(),
|
|
12
22
|
ANONYMOUS_ROLE: z.string().default("anonymous"),
|
|
13
|
-
JWT_SECRET:
|
|
23
|
+
JWT_SECRET: secretList,
|
|
14
24
|
JWT_EXPIRES_IN: z.string().default("5m"),
|
|
15
25
|
JWT_RT_EXPIRES_IN: z.string().default("7d"),
|
|
16
|
-
PASETO_LOCAL_KEY:
|
|
26
|
+
PASETO_LOCAL_KEY: secretList,
|
|
17
27
|
PASETO_SECRET_KEY: z.string().optional().default(""),
|
|
18
|
-
PASETO_PUBLIC_KEY:
|
|
28
|
+
PASETO_PUBLIC_KEY: secretList,
|
|
19
29
|
AUTH_STRATEGY: z.enum(["jwt", "paseto_local", "paseto_public"]).optional(),
|
|
20
30
|
REDIS_URL: z.string().default("redis://localhost:6379"),
|
|
21
31
|
CACHE_STORE: z.enum(["memory", "redis"]).default("memory"),
|
|
@@ -31,6 +41,8 @@ export const EnvZod = z
|
|
|
31
41
|
CONSOLE_ENABLED: z.stringbool().default(false),
|
|
32
42
|
CONSOLE_ENDPOINT: z.string().default("/_console"),
|
|
33
43
|
CONSOLE_SESSION_EXPIRES_IN: z.string().default("1h"),
|
|
44
|
+
CONSOLE_READ_SECRET: secretList,
|
|
45
|
+
CONSOLE_WRITE_SECRET: secretList,
|
|
34
46
|
PREFIX: z.string().default(""),
|
|
35
47
|
CORS_ENABLED: z.stringbool().default(true),
|
|
36
48
|
PRINT_SCHEMAS: z.stringbool().default(false),
|
|
@@ -40,6 +52,19 @@ export const EnvZod = z
|
|
|
40
52
|
DEFAULT_PAGE_SIZE: z.coerce.number().int().min(0).default(100),
|
|
41
53
|
MAX_PAGE_SIZE: z.coerce.number().int().min(0).default(1000),
|
|
42
54
|
QUERY_TIMEOUT_MS: z.coerce.number().int().min(0).default(10000),
|
|
55
|
+
SLOW_QUERY_MS: z.coerce.number().int().min(0).default(1000),
|
|
56
|
+
METRICS_ENABLED: z.stringbool().default(false),
|
|
57
|
+
METRICS_ENDPOINT: z.string().default("/metrics"),
|
|
58
|
+
METRICS_SECRET: secretList,
|
|
59
|
+
METRICS_MAX_OPERATION_LABELS: z.coerce.number().int().min(1).default(200),
|
|
60
|
+
// Only the gate is graphoria's own. The rest are the standard OpenTelemetry
|
|
61
|
+
// names, so a cluster that already injects them into its pods needs to set
|
|
62
|
+
// nothing but TRACING_ENABLED.
|
|
63
|
+
TRACING_ENABLED: z.stringbool().default(false),
|
|
64
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().default("http://localhost:4318"),
|
|
65
|
+
OTEL_EXPORTER_OTLP_HEADERS: z.string().default(""),
|
|
66
|
+
OTEL_SERVICE_NAME: z.string().default("graphoria"),
|
|
67
|
+
OTEL_TRACES_SAMPLER_ARG: z.coerce.number().min(0).max(1).default(1),
|
|
43
68
|
MAX_QUERY_COST: z.coerce.number().int().min(0).default(0),
|
|
44
69
|
RATE_LIMIT_MAX: z.coerce.number().int().min(0).default(0),
|
|
45
70
|
RATE_LIMIT_ANONYMOUS_MAX: z.coerce.number().int().min(0).default(0),
|
|
@@ -48,9 +73,11 @@ export const EnvZod = z
|
|
|
48
73
|
AI_ENABLED: z.stringbool().optional(),
|
|
49
74
|
AI_GRAPHQL_ENABLED: z.stringbool().default(true),
|
|
50
75
|
AI_REST_ENABLED: z.stringbool().default(true),
|
|
76
|
+
AI_SECRET: secretList,
|
|
51
77
|
AI_MCP_ENABLED: z.stringbool().optional(),
|
|
52
78
|
AI_MCP_ENDPOINT: z.string().default("/mcp"),
|
|
53
79
|
AI_MCP_REQUIRE_ADMIN_SECRET: z.stringbool().default(false),
|
|
80
|
+
AI_MCP_SECRET: secretList,
|
|
54
81
|
AI_MCP_MAX_QUERY_DEPTH: z.coerce.number().int().min(0).optional(),
|
|
55
82
|
AI_MCP_DISABLED_TOOLS: z.string().default(""),
|
|
56
83
|
AI_MCP_DISABLED_RESOURCES: z.string().default(""),
|
|
@@ -78,20 +105,21 @@ export const EnvZod = z
|
|
|
78
105
|
defaultPageSize: env.DEFAULT_PAGE_SIZE,
|
|
79
106
|
maxPageSize: env.MAX_PAGE_SIZE,
|
|
80
107
|
queryTimeoutMs: env.QUERY_TIMEOUT_MS,
|
|
108
|
+
slowQueryMs: env.SLOW_QUERY_MS,
|
|
81
109
|
maxQueryCost: env.MAX_QUERY_COST,
|
|
82
110
|
admin: {
|
|
83
|
-
|
|
111
|
+
secrets: env.ADMIN_SECRET,
|
|
84
112
|
header: env.ADMIN_SECRET_HEADER,
|
|
85
113
|
},
|
|
86
114
|
jwt: {
|
|
87
|
-
|
|
115
|
+
secrets: env.JWT_SECRET,
|
|
88
116
|
expiresIn: env.JWT_EXPIRES_IN,
|
|
89
117
|
rtExpiresIn: env.JWT_RT_EXPIRES_IN,
|
|
90
118
|
},
|
|
91
119
|
paseto: {
|
|
92
|
-
|
|
120
|
+
localKeys: env.PASETO_LOCAL_KEY,
|
|
93
121
|
secretKey: env.PASETO_SECRET_KEY,
|
|
94
|
-
|
|
122
|
+
publicKeys: env.PASETO_PUBLIC_KEY,
|
|
95
123
|
},
|
|
96
124
|
rateLimit: {
|
|
97
125
|
max: env.RATE_LIMIT_MAX,
|
|
@@ -110,19 +138,36 @@ export const EnvZod = z
|
|
|
110
138
|
superadmin: {
|
|
111
139
|
role: env.SUPERADMIN_ROLE,
|
|
112
140
|
},
|
|
141
|
+
metrics: {
|
|
142
|
+
enabled: env.METRICS_ENABLED,
|
|
143
|
+
endpoint: env.METRICS_ENDPOINT,
|
|
144
|
+
secrets: env.METRICS_SECRET,
|
|
145
|
+
maxOperationLabels: env.METRICS_MAX_OPERATION_LABELS,
|
|
146
|
+
},
|
|
147
|
+
tracing: {
|
|
148
|
+
enabled: env.TRACING_ENABLED,
|
|
149
|
+
endpoint: env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
150
|
+
headers: env.OTEL_EXPORTER_OTLP_HEADERS,
|
|
151
|
+
serviceName: env.OTEL_SERVICE_NAME,
|
|
152
|
+
sampleRatio: env.OTEL_TRACES_SAMPLER_ARG,
|
|
153
|
+
},
|
|
113
154
|
console: {
|
|
114
155
|
enabled: env.CONSOLE_ENABLED,
|
|
115
156
|
endpoint: env.CONSOLE_ENDPOINT,
|
|
116
157
|
sessionExpiresIn: env.CONSOLE_SESSION_EXPIRES_IN,
|
|
158
|
+
readSecrets: env.CONSOLE_READ_SECRET,
|
|
159
|
+
writeSecrets: env.CONSOLE_WRITE_SECRET,
|
|
117
160
|
},
|
|
118
161
|
ai: {
|
|
119
162
|
enabled: env.AI_ENABLED,
|
|
120
163
|
graphqlEnabled: env.AI_GRAPHQL_ENABLED,
|
|
121
164
|
restEnabled: env.AI_REST_ENABLED,
|
|
165
|
+
secrets: env.AI_SECRET,
|
|
122
166
|
mcp: {
|
|
123
167
|
enabled: env.AI_MCP_ENABLED,
|
|
124
168
|
endpoint: env.AI_MCP_ENDPOINT,
|
|
125
169
|
requireAdminSecret: env.AI_MCP_REQUIRE_ADMIN_SECRET,
|
|
170
|
+
secrets: env.AI_MCP_SECRET,
|
|
126
171
|
maxQueryDepth: env.AI_MCP_MAX_QUERY_DEPTH,
|
|
127
172
|
disabledTools: env.AI_MCP_DISABLED_TOOLS
|
|
128
173
|
? env.AI_MCP_DISABLED_TOOLS.split(",")
|
package/src/utils/rateLimit.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LRUCache } from "lru-cache";
|
|
2
2
|
|
|
3
3
|
import { logger } from "../logging";
|
|
4
|
+
import { incMetric } from "../observability/metrics";
|
|
4
5
|
|
|
5
6
|
export type ConsumeResult = { allowed: boolean; retryAfterMs: number };
|
|
6
7
|
|
|
@@ -203,7 +204,11 @@ export const createRateLimiter = ({
|
|
|
203
204
|
const key =
|
|
204
205
|
sub && sub !== "anonymous" ? `rl:${role}:${sub}` : `rl:${role}:ip:${address ?? "unknown"}`;
|
|
205
206
|
|
|
206
|
-
|
|
207
|
+
const verdict = await resolvedStore.consume(key, max, max / windowMs, windowMs);
|
|
208
|
+
|
|
209
|
+
if (!verdict.allowed) incMetric("graphoria_rate_limit_rejections_total", { role });
|
|
210
|
+
|
|
211
|
+
return verdict;
|
|
207
212
|
},
|
|
208
213
|
};
|
|
209
214
|
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { RedisClient as ValkeyClient } from "bun";
|
|
2
|
+
|
|
3
|
+
import { logger } from "../logging";
|
|
4
|
+
|
|
5
|
+
const RECONNECT_INITIAL_DELAY = 1000;
|
|
6
|
+
const RECONNECT_MAX_DELAY = 30000;
|
|
7
|
+
const RECONNECT_MULTIPLIER = 2;
|
|
8
|
+
|
|
9
|
+
export type RevivableRedisClient = {
|
|
10
|
+
onclose: ((error: Error) => void) | null;
|
|
11
|
+
connect(): Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type KeepRedisConnectedOptions = {
|
|
15
|
+
setTimeout?: typeof setTimeout;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Bun's client reconnects on its own only until `maxRetries` runs out; then it
|
|
20
|
+
* fires `onclose` and fails every later command with "Connection has failed",
|
|
21
|
+
* even once the server is back. Only an explicit `connect()` revives it, so
|
|
22
|
+
* that is what `onclose` schedules here, backing off while the server stays down.
|
|
23
|
+
*/
|
|
24
|
+
export const keepRedisConnected = (
|
|
25
|
+
client: RevivableRedisClient,
|
|
26
|
+
options: KeepRedisConnectedOptions = {},
|
|
27
|
+
) => {
|
|
28
|
+
const log = logger("redis");
|
|
29
|
+
const setTimeoutFn = options.setTimeout ?? setTimeout;
|
|
30
|
+
|
|
31
|
+
let attempts = 0;
|
|
32
|
+
let reconnecting = false;
|
|
33
|
+
|
|
34
|
+
const scheduleReconnect = () => {
|
|
35
|
+
if (reconnecting) return;
|
|
36
|
+
reconnecting = true;
|
|
37
|
+
|
|
38
|
+
const delay = Math.min(
|
|
39
|
+
RECONNECT_INITIAL_DELAY * Math.pow(RECONNECT_MULTIPLIER, attempts),
|
|
40
|
+
RECONNECT_MAX_DELAY,
|
|
41
|
+
);
|
|
42
|
+
attempts++;
|
|
43
|
+
|
|
44
|
+
setTimeoutFn(() => {
|
|
45
|
+
client.connect().then(
|
|
46
|
+
() => {
|
|
47
|
+
reconnecting = false;
|
|
48
|
+
attempts = 0;
|
|
49
|
+
log.info("reconnected");
|
|
50
|
+
},
|
|
51
|
+
(error: unknown) => {
|
|
52
|
+
reconnecting = false;
|
|
53
|
+
log.warn({ err: error, attempt: attempts }, "reconnect attempt failed");
|
|
54
|
+
scheduleReconnect();
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
}, delay).unref();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
client.onclose = (error) => {
|
|
61
|
+
if (reconnecting) return;
|
|
62
|
+
log.warn({ err: error }, "connection lost, reconnecting");
|
|
63
|
+
scheduleReconnect();
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const createRedisClient = (url: string) => {
|
|
68
|
+
const client = new ValkeyClient(url);
|
|
69
|
+
keepRedisConnected(client);
|
|
70
|
+
return client;
|
|
71
|
+
};
|
package/src/utils/responses.ts
CHANGED
|
@@ -29,6 +29,12 @@ export class S401 extends ClientResponse {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export class S403 extends ClientResponse {
|
|
33
|
+
constructor(body?: object | null, init?: ResponseInit) {
|
|
34
|
+
super(body, { ...init, status: 403 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
export class S404 extends ClientResponse {
|
|
33
39
|
constructor(body?: object | null, init?: ResponseInit) {
|
|
34
40
|
super(body, { ...init, status: 404 });
|
|
@@ -50,3 +56,9 @@ export class S500 extends ClientResponse {
|
|
|
50
56
|
super(body, { ...init, status: 500 });
|
|
51
57
|
}
|
|
52
58
|
}
|
|
59
|
+
|
|
60
|
+
export class S503 extends ClientResponse {
|
|
61
|
+
constructor(body?: object | null, init?: ResponseInit) {
|
|
62
|
+
super(body, { ...init, status: 503 });
|
|
63
|
+
}
|
|
64
|
+
}
|