@graphoria/server 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/src/analyzeQuery/resolveVariables.d.ts.map +1 -1
  2. package/dist/src/config/helpers/operationHelper.d.ts +60 -6
  3. package/dist/src/config/helpers/operationHelper.d.ts.map +1 -1
  4. package/dist/src/config/helpers/operationHelper.test.d.ts +2 -0
  5. package/dist/src/config/helpers/operationHelper.test.d.ts.map +1 -0
  6. package/dist/src/config/index.d.ts +65 -60
  7. package/dist/src/config/index.d.ts.map +1 -1
  8. package/dist/src/config/types/ai.d.ts +9 -3
  9. package/dist/src/config/types/ai.d.ts.map +1 -1
  10. package/dist/src/config/types/auth.d.ts +55 -15
  11. package/dist/src/config/types/auth.d.ts.map +1 -1
  12. package/dist/src/config/types/cron.d.ts +96 -2
  13. package/dist/src/config/types/cron.d.ts.map +1 -1
  14. package/dist/src/config/types/db.d.ts +186 -24
  15. package/dist/src/config/types/db.d.ts.map +1 -1
  16. package/dist/src/config/types/index.d.ts +3 -3
  17. package/dist/src/config/types/index.d.ts.map +1 -1
  18. package/dist/src/config/types/operation.d.ts +49 -17
  19. package/dist/src/config/types/operation.d.ts.map +1 -1
  20. package/dist/src/config/types/queue.d.ts +59 -12
  21. package/dist/src/config/types/queue.d.ts.map +1 -1
  22. package/dist/src/config/types/remote-rest.d.ts +10 -1
  23. package/dist/src/config/types/remote-rest.d.ts.map +1 -1
  24. package/dist/src/config/types/remote-schema.d.ts +13 -3
  25. package/dist/src/config/types/remote-schema.d.ts.map +1 -1
  26. package/dist/src/config/types/virtual-columns.d.ts +1 -1
  27. package/dist/src/config/types/virtual-columns.d.ts.map +1 -1
  28. package/dist/src/configuration/getSchemas/index.d.ts +52 -4
  29. package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
  30. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +26 -2
  31. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
  32. package/dist/src/configuration/getSchemas/mergeEntities/index.test.d.ts +2 -0
  33. package/dist/src/configuration/getSchemas/mergeEntities/index.test.d.ts.map +1 -0
  34. package/dist/src/configuration/getSchemas/type-definition-generator/index.d.ts.map +1 -1
  35. package/dist/src/configuration/index.d.ts +40 -4
  36. package/dist/src/configuration/index.d.ts.map +1 -1
  37. package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
  38. package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
  39. package/dist/src/databases/common.d.ts +3 -1
  40. package/dist/src/databases/common.d.ts.map +1 -1
  41. package/dist/src/databases/core/query-builder.d.ts +7 -1
  42. package/dist/src/databases/core/query-builder.d.ts.map +1 -1
  43. package/dist/src/databases/directives.d.ts.map +1 -1
  44. package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
  45. package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
  46. package/dist/src/databases/engines/mssql/getStructure.d.ts +7 -0
  47. package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
  48. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  49. package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
  50. package/dist/src/databases/engines/mysql/getStructure.d.ts +7 -0
  51. package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
  52. package/dist/src/databases/engines/mysql/placeholders.d.ts +22 -0
  53. package/dist/src/databases/engines/mysql/placeholders.d.ts.map +1 -0
  54. package/dist/src/databases/engines/mysql/placeholders.test.d.ts +2 -0
  55. package/dist/src/databases/engines/mysql/placeholders.test.d.ts.map +1 -0
  56. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  57. package/dist/src/databases/engines/postgresql/getStructure.d.ts +7 -0
  58. package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
  59. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  60. package/dist/src/databases/high-level-operations.d.ts +14 -1
  61. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  62. package/dist/src/databases/metadata/structure.d.ts +7 -0
  63. package/dist/src/databases/metadata/structure.d.ts.map +1 -1
  64. package/dist/src/databases/sqlTypeUtils.d.ts.map +1 -1
  65. package/dist/src/databases/transformers/data-transformers.d.ts +39 -2
  66. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  67. package/dist/src/singletons/env.d.ts +21 -3
  68. package/dist/src/singletons/env.d.ts.map +1 -1
  69. package/dist/src/types/configuration.d.ts +1 -1
  70. package/dist/src/types/configuration.d.ts.map +1 -1
  71. package/dist/src/types/env.d.ts +127 -51
  72. package/dist/src/types/env.d.ts.map +1 -1
  73. package/dist/src/types/resolver.d.ts +12 -2
  74. package/dist/src/types/resolver.d.ts.map +1 -1
  75. package/dist/src/types/zod/auth.d.ts +6 -6
  76. package/dist/src/types/zod/configuration.d.ts +85 -45
  77. package/dist/src/types/zod/configuration.d.ts.map +1 -1
  78. package/dist/src/types/zod/cron.d.ts +1 -1
  79. package/dist/src/types/zod/cron.d.ts.map +1 -1
  80. package/dist/src/types/zod/db.d.ts +167 -8
  81. package/dist/src/types/zod/db.d.ts.map +1 -1
  82. package/dist/src/types/zod/queue.d.ts +26 -26
  83. package/dist/src/types/zod/strict-keys.test.d.ts +2 -0
  84. package/dist/src/types/zod/strict-keys.test.d.ts.map +1 -0
  85. package/dist/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +1 -1
  87. package/src/analyzeQuery/resolveVariables.ts +37 -7
  88. package/src/config/helpers/operationHelper.ts +146 -5
  89. package/src/config/index.ts +139 -62
  90. package/src/config/types/ai.ts +2 -2
  91. package/src/config/types/auth.ts +37 -6
  92. package/src/config/types/cron.ts +1 -1
  93. package/src/config/types/db.ts +56 -9
  94. package/src/config/types/index.ts +5 -0
  95. package/src/config/types/operation.ts +49 -9
  96. package/src/config/types/queue.ts +8 -8
  97. package/src/config/types/remote-rest.ts +1 -1
  98. package/src/config/types/remote-schema.ts +2 -2
  99. package/src/config/types/virtual-columns.ts +1 -1
  100. package/src/configuration/getSchemas/mergeEntities/index.ts +6 -0
  101. package/src/configuration/getSchemas/type-definition-generator/index.ts +13 -4
  102. package/src/configuration/rest/generateOpenAPI.ts +5 -9
  103. package/src/configuration/rest/handleRESTRequestFactory.ts +40 -14
  104. package/src/databases/common.ts +279 -83
  105. package/src/databases/directives.ts +26 -8
  106. package/src/databases/engines/mssql/connection.ts +1 -1
  107. package/src/databases/engines/mssql/getStructure.ts +1 -0
  108. package/src/databases/engines/mssql/query/index.ts +29 -11
  109. package/src/databases/engines/mysql/auth.ts +2 -2
  110. package/src/databases/engines/mysql/connection.ts +5 -4
  111. package/src/databases/engines/mysql/getStructure.ts +61 -29
  112. package/src/databases/engines/mysql/placeholders.ts +84 -0
  113. package/src/databases/engines/mysql/query/index.ts +49 -4
  114. package/src/databases/engines/postgresql/getStructure.ts +1 -0
  115. package/src/databases/engines/postgresql/query/index.ts +40 -2
  116. package/src/databases/high-level-operations.ts +25 -0
  117. package/src/databases/sqlTypeUtils.ts +3 -1
  118. package/src/subscriptions/strategies/index.ts +1 -1
  119. package/src/subscriptions/strategies/queue.ts +1 -1
  120. package/src/types/configuration.ts +7 -1
  121. package/src/types/resolver.ts +17 -1
  122. package/src/types/zod/configuration.ts +1 -1
  123. package/src/types/zod/db.ts +4 -0
@@ -42,7 +42,7 @@ export type CronTickCallback<TVariables = Record<string, unknown>> = (
42
42
  * This allows TypeScript to infer the variables type
43
43
  */
44
44
  export const createTypedCronJobZod = <TVariables extends DefaultVariables>() =>
45
- z.object({
45
+ z.strictObject({
46
46
  /**
47
47
  * Unique name/identifier for the cron job
48
48
  */
@@ -18,14 +18,61 @@ import { VirtualColumnZod } from "./virtual-columns";
18
18
 
19
19
  export type DatabaseType = "mssql" | "pg" | "mysql";
20
20
 
21
+ // ============================================================================
22
+ // Relationship Join Condition (static-value predicate)
23
+ // ============================================================================
24
+
25
+ /** Comparison operators allowed in a static relationship join condition. */
26
+ export const RELATIONSHIP_CONDITION_OPERATORS = [
27
+ "eq",
28
+ "neq",
29
+ "gt",
30
+ "gte",
31
+ "lt",
32
+ "lte",
33
+ "like",
34
+ "is_null",
35
+ "is_not_null",
36
+ ] as const;
37
+
38
+ /**
39
+ * A static-value predicate added to a relationship's JOIN. Targets exactly one
40
+ * side of the relationship — `source` (a column on the declaring table) or
41
+ * `target` (a column on the referenced table) — and compares it to a literal.
42
+ * A `value` is required for every operator except `is_null` / `is_not_null`.
43
+ */
44
+ export const RelationshipConditionZod = z
45
+ .strictObject({
46
+ /** Column on the declaring (source) table */
47
+ source: z.string().optional(),
48
+ /** Column on the referenced (target) table */
49
+ target: z.string().optional(),
50
+ /** Comparison operator (default `eq`) */
51
+ operator: z.enum(RELATIONSHIP_CONDITION_OPERATORS).optional().default("eq"),
52
+ /** Literal compared against the column (omit for `is_null` / `is_not_null`) */
53
+ value: z.union([z.string(), z.number(), z.boolean()]).optional(),
54
+ })
55
+ .refine((cond) => (cond.source === undefined) !== (cond.target === undefined), {
56
+ message: 'Relationship condition must set exactly one of "source" or "target"',
57
+ })
58
+ .refine(
59
+ (cond) =>
60
+ cond.operator === "is_null" || cond.operator === "is_not_null" || cond.value !== undefined,
61
+ { message: 'Relationship condition requires a "value" unless operator is is_null/is_not_null' },
62
+ );
63
+
64
+ export type RelationshipCondition = z.input<typeof RelationshipConditionZod>;
65
+
21
66
  // ============================================================================
22
67
  // Table Relationship (config shape, no transform)
23
68
  // ============================================================================
24
69
 
25
- export const TableRelationshipZod = z.object({
70
+ export const TableRelationshipZod = z.strictObject({
26
71
  schema: z.string(),
27
72
  name: z.string(),
28
- columns: z.array(z.object({ source: z.string(), target: z.string() })),
73
+ columns: z.array(z.strictObject({ source: z.string(), target: z.string() })),
74
+ /** Static-value predicates ANDed into the JOIN condition */
75
+ conditions: z.array(RelationshipConditionZod).optional(),
29
76
  });
30
77
 
31
78
  export type TableRelationship = z.input<typeof TableRelationshipZod>;
@@ -34,7 +81,7 @@ export type TableRelationship = z.input<typeof TableRelationshipZod>;
34
81
  // Table Schema Config
35
82
  // ============================================================================
36
83
 
37
- export const TableSchemaConfigZod = z.object({
84
+ export const TableSchemaConfigZod = z.strictObject({
38
85
  columns: z.array(VirtualColumnZod).optional().default([]),
39
86
  relationships: z.array(TableRelationshipZod).optional().default([]),
40
87
  /** Overrides the table description from the database */
@@ -49,7 +96,7 @@ export type TableSchemaConfig = z.input<typeof TableSchemaConfigZod>;
49
96
  // Database Schema Config
50
97
  // ============================================================================
51
98
 
52
- export const DatabaseSchemaConfigZod = z.object({
99
+ export const DatabaseSchemaConfigZod = z.strictObject({
53
100
  database: z
54
101
  .record(
55
102
  z.string(),
@@ -80,7 +127,7 @@ export type DatabaseSchemaConfig = z.input<typeof DatabaseSchemaConfigZod>;
80
127
  * Connection pool and transport options for PostgreSQL and MySQL (Bun SQL).
81
128
  * All timeout values are in seconds.
82
129
  */
83
- export const BunSQLConnectionOptionsZod = z.object({
130
+ export const BunSQLConnectionOptionsZod = z.strictObject({
84
131
  /** Maximum number of connections in the pool */
85
132
  max: z.number().int().positive().default(10),
86
133
  /** Maximum time in seconds a connection can be idle before being closed */
@@ -103,10 +150,10 @@ export type BunSQLConnectionOptions = z.input<typeof BunSQLConnectionOptionsZod>
103
150
  * Connection pool and transport options for MSSQL.
104
151
  * All timeout values are in seconds (converted to milliseconds internally).
105
152
  */
106
- export const MSSQLConnectionOptionsZod = z.object({
153
+ export const MSSQLConnectionOptionsZod = z.strictObject({
107
154
  /** Connection pool options */
108
155
  pool: z
109
- .object({
156
+ .strictObject({
110
157
  /** Maximum number of connections in the pool */
111
158
  max: z.number().int().positive().default(10),
112
159
  /** Minimum number of connections in the pool */
@@ -181,7 +228,7 @@ export type ConnectionOptionsForType<T extends DatabaseType> = T extends "pg" |
181
228
  // Database Connection Zod Schema
182
229
  // ============================================================================
183
230
 
184
- export const DatabaseConnectionZod = z.object({
231
+ export const DatabaseConnectionZod = z.strictObject({
185
232
  /** Unique name for the database connection */
186
233
  name: z.string(),
187
234
  /** Whether the database is enabled */
@@ -189,7 +236,7 @@ export const DatabaseConnectionZod = z.object({
189
236
  /** Database type */
190
237
  type: z.union([z.literal("mssql"), z.literal("pg"), z.literal("mysql")]),
191
238
  /** Connection configuration */
192
- connection: z.object({
239
+ connection: z.strictObject({
193
240
  host: z.string(),
194
241
  port: z.number(),
195
242
  user: z.string(),
@@ -18,6 +18,8 @@ export {
18
18
  // Operation types and Zod schemas
19
19
  export type {
20
20
  DefaultInput,
21
+ AnyQueryDocument,
22
+ QueryResultOf,
21
23
  OperationRestConfig,
22
24
  OperationGraphQLConfig,
23
25
  OperationCacheConfig,
@@ -104,6 +106,7 @@ export type {
104
106
  DatabaseType,
105
107
  DatabaseConnection,
106
108
  TableRelationship,
109
+ RelationshipCondition,
107
110
  TableSchemaConfig,
108
111
  DatabaseSchemaConfig,
109
112
  CustomRepositoryFactory,
@@ -117,6 +120,8 @@ export type {
117
120
 
118
121
  export {
119
122
  TableRelationshipZod,
123
+ RelationshipConditionZod,
124
+ RELATIONSHIP_CONDITION_OPERATORS,
120
125
  TableSchemaConfigZod,
121
126
  DatabaseSchemaConfigZod,
122
127
  BunSQLConnectionOptionsZod,
@@ -5,6 +5,25 @@ import { z } from "zod";
5
5
  */
6
6
  export type DefaultInput = Record<string, unknown>;
7
7
 
8
+ /**
9
+ * A gql.tada / `TypedDocumentNode` query document. Every GraphQL AST node has a
10
+ * `kind`, which distinguishes a document from a plain string query; documents
11
+ * additionally carry their result type on the `__apiType` phantom property.
12
+ */
13
+ export type AnyQueryDocument = { readonly kind: string };
14
+
15
+ /**
16
+ * Extracts a query document's result type (the GraphQL `data` payload) from its
17
+ * gql.tada / TypedDocumentNode `__apiType` phantom. A plain string query (or a
18
+ * document without embedded types) resolves to `unknown`.
19
+ */
20
+ export type QueryResultOf<TQuery> = TQuery extends string
21
+ ? unknown
22
+ : // oxlint-disable-next-line typescript/no-explicit-any
23
+ TQuery extends { __apiType?: (variables: any) => infer TResult }
24
+ ? TResult
25
+ : unknown;
26
+
8
27
  // ============================================================================
9
28
  // Zod Schemas for Runtime Validation
10
29
  // ============================================================================
@@ -12,7 +31,7 @@ export type DefaultInput = Record<string, unknown>;
12
31
  /**
13
32
  * Zod schema for REST configuration
14
33
  */
15
- export const OperationRestConfigZod = z.object({
34
+ export const OperationRestConfigZod = z.strictObject({
16
35
  path: z.string(),
17
36
  method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
18
37
  pathParams: z.custom<z.ZodType>().optional(),
@@ -23,7 +42,7 @@ export const OperationRestConfigZod = z.object({
23
42
  /**
24
43
  * Zod schema for GraphQL configuration
25
44
  */
26
- export const OperationGraphQLConfigZod = z.object({
45
+ export const OperationGraphQLConfigZod = z.strictObject({
27
46
  enabled: z.boolean().optional().default(true),
28
47
  name: z.string().optional(),
29
48
  });
@@ -31,7 +50,7 @@ export const OperationGraphQLConfigZod = z.object({
31
50
  /**
32
51
  * Zod schema for cache configuration
33
52
  */
34
- export const OperationCacheConfigZod = z.object({
53
+ export const OperationCacheConfigZod = z.strictObject({
35
54
  max: z.number().int().positive().optional(),
36
55
  maxSize: z.number().int().positive().optional(),
37
56
  ttl: z.number().int().positive().optional(),
@@ -65,7 +84,7 @@ export type OperationCacheConfig = z.input<typeof OperationCacheConfigZod>;
65
84
  // ============================================================================
66
85
 
67
86
  export const OperationZod = z
68
- .object({
87
+ .strictObject({
69
88
  description: z.string().optional(),
70
89
  query: z.string().optional(),
71
90
  // oxlint-disable-next-line typescript/no-explicit-any
@@ -75,7 +94,7 @@ export const OperationZod = z
75
94
  // oxlint-disable-next-line typescript/no-explicit-any
76
95
  output: z.custom<z.ZodType<any>>().optional(),
77
96
  hooks: z
78
- .object({
97
+ .strictObject({
79
98
  // oxlint-disable-next-line typescript/no-explicit-any
80
99
  init: z.custom<OperationInitHook<any>>().optional(),
81
100
  beforeRequest: z
@@ -108,14 +127,30 @@ export const OperationsZod = z.record(z.string(), OperationZod);
108
127
  export type Operations = z.input<typeof OperationsZod>;
109
128
 
110
129
  /**
111
- * Context passed to beforeRequest hook
130
+ * Context passed to beforeRequest hook.
131
+ *
132
+ * `input` is the merged view of every REST parameter source. `pathParams`,
133
+ * `queryParams`, and `body` expose each source separately (parsed with its own
134
+ * schema), or `undefined` when that source has no schema configured.
112
135
  */
113
- export type BeforeRequestContext<TInput = DefaultInput> = {
136
+ export type BeforeRequestContext<
137
+ TInput = DefaultInput,
138
+ TPathParams = unknown,
139
+ TQueryParams = unknown,
140
+ TBody = unknown,
141
+ > = {
114
142
  input: TInput;
143
+ pathParams?: TPathParams;
144
+ queryParams?: TQueryParams;
145
+ body?: TBody;
115
146
  };
116
147
 
117
148
  /**
118
- * Context passed to afterRequest hook
149
+ * Context passed to the afterRequest hook.
150
+ *
151
+ * `output` is the operation's output payload — for query-based operations the
152
+ * unwrapped GraphQL `data` (the hook's return replaces `data` in the response
153
+ * envelope); for handler-based operations the value the handler returned.
119
154
  */
120
155
  export type AfterRequestContext<TOutput = unknown> = {
121
156
  output: TOutput;
@@ -173,7 +208,12 @@ export type OperationBeforeRequestHook<
173
208
  ) => TVariables | Promise<TVariables>;
174
209
 
175
210
  /**
176
- * AfterRequest hook - transforms output after execution
211
+ * AfterRequest hook transforms the output after a successful query/handler.
212
+ *
213
+ * Receives the output payload and returns the (possibly transformed) payload.
214
+ * Runs on success only; throw to turn a successful response into an error. For
215
+ * cached query operations it runs on the cache miss and the transformed result
216
+ * is cached, so cache hits reuse it without re-invoking the hook.
177
217
  */
178
218
  export type OperationAfterRequestHook<TOutput = unknown> = (
179
219
  context: AfterRequestContext<TOutput>,
@@ -10,7 +10,7 @@ import { z } from "zod";
10
10
  /**
11
11
  * Reconnection configuration
12
12
  */
13
- export const ReconnectConfigZod = z.object({
13
+ export const ReconnectConfigZod = z.strictObject({
14
14
  /** Initial delay before first reconnect attempt (ms) */
15
15
  initialDelay: z.number().int().positive().default(1000),
16
16
  /** Maximum delay between reconnect attempts (ms) */
@@ -26,7 +26,7 @@ export type ReconnectConfig = z.input<typeof ReconnectConfigZod>;
26
26
  /**
27
27
  * Publisher configuration — defines how to send messages
28
28
  */
29
- export const PublisherConfigZod = z.object({
29
+ export const PublisherConfigZod = z.strictObject({
30
30
  /** Topic/Exchange name to publish to */
31
31
  topic: z.string(),
32
32
  /** Routing key (RabbitMQ) or message key (Kafka) */
@@ -58,7 +58,7 @@ export type SubscriberHandler = (
58
58
  /**
59
59
  * Subscriber configuration — defines how to receive messages
60
60
  */
61
- export const SubscriberConfigZod = z.object({
61
+ export const SubscriberConfigZod = z.strictObject({
62
62
  /** Topic/Exchange name to subscribe to */
63
63
  topic: z.string(),
64
64
  /** Pattern for filtering messages (routing key pattern for RabbitMQ) */
@@ -80,7 +80,7 @@ export type SubscriberConfig = z.input<typeof SubscriberConfigZod>;
80
80
  /**
81
81
  * Topic/Exchange configuration — for auto-setup
82
82
  */
83
- export const TopicConfigZod = z.object({
83
+ export const TopicConfigZod = z.strictObject({
84
84
  /** Exchange type (RabbitMQ only) */
85
85
  type: z.enum(["direct", "fanout", "topic", "headers"]).optional().default("topic"),
86
86
  /** Whether exchange should survive broker restart */
@@ -94,7 +94,7 @@ export type TopicConfig = z.input<typeof TopicConfigZod>;
94
94
  /**
95
95
  * Base queue configuration shared by all providers
96
96
  */
97
- export const BaseQueueConfigZod = z.object({
97
+ export const BaseQueueConfigZod = z.strictObject({
98
98
  /** Unique name for this queue connection */
99
99
  name: z.string(),
100
100
  /** Whether to auto-create topics/exchanges/queues */
@@ -118,7 +118,7 @@ export const BaseQueueConfigZod = z.object({
118
118
  /**
119
119
  * RabbitMQ connection configuration (object form)
120
120
  */
121
- export const RabbitMQConnectionZod = z.object({
121
+ export const RabbitMQConnectionZod = z.strictObject({
122
122
  hostname: z.string().default("localhost"),
123
123
  port: z.number().default(5672),
124
124
  username: z.string().optional(),
@@ -131,7 +131,7 @@ export type RabbitMQConnection = z.input<typeof RabbitMQConnectionZod>;
131
131
  /**
132
132
  * Kafka connection configuration
133
133
  */
134
- export const KafkaConnectionZod = z.object({
134
+ export const KafkaConnectionZod = z.strictObject({
135
135
  /** Broker addresses — "host:port" or ["host1:port1", "host2:port2"] */
136
136
  brokers: z.union([z.string(), z.array(z.string())]),
137
137
  /** Client identifier sent to the broker (default: "datagraph-<queue name>") */
@@ -140,7 +140,7 @@ export const KafkaConnectionZod = z.object({
140
140
  ssl: z.boolean().optional().default(false),
141
141
  /** SASL authentication (optional) */
142
142
  sasl: z
143
- .object({
143
+ .strictObject({
144
144
  mechanism: z.enum(["plain", "scram-sha-256", "scram-sha-512"]).optional().default("plain"),
145
145
  username: z.string(),
146
146
  password: z.string(),
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  /**
4
4
  * Remote REST API configuration (OpenAPI-based)
5
5
  */
6
- export const RemoteRESTConfigZod = z.object({
6
+ export const RemoteRESTConfigZod = z.strictObject({
7
7
  /** Unique identifier for this remote REST API */
8
8
  name: z.string().min(1),
9
9
  /** Base URL of the remote API for proxying requests */
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  * Remote GraphQL schema introspection configuration
5
5
  */
6
6
  export const RemoteSchemaIntrospectionConfigZod = z
7
- .object({
7
+ .strictObject({
8
8
  /** Re-introspect interval in ms (0 = only at startup, default: 0) */
9
9
  interval: z.number().int().min(0).optional().default(0),
10
10
  })
@@ -16,7 +16,7 @@ export type RemoteSchemaIntrospectionConfig = z.input<typeof RemoteSchemaIntrosp
16
16
  /**
17
17
  * Remote GraphQL schema configuration
18
18
  */
19
- export const RemoteSchemaConfigZod = z.object({
19
+ export const RemoteSchemaConfigZod = z.strictObject({
20
20
  /** Unique identifier for this remote schema */
21
21
  name: z.string().min(1),
22
22
  /** Remote GraphQL endpoint URL */
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  /**
4
4
  * Virtual column schema for computed/virtual columns in database schema
5
5
  */
6
- export const VirtualColumnZod = z.object({
6
+ export const VirtualColumnZod = z.strictObject({
7
7
  name: z.string(),
8
8
  dataType: z.string(),
9
9
  isNullable: z.boolean(),
@@ -87,6 +87,12 @@ export const mergeEntities = (
87
87
  acc[publisher.resolverName] = publisher;
88
88
  });
89
89
  });
90
+ // Subscribers become subscriptions (not mutations): register in the resolver
91
+ // registry only — not queuesMap — so getResolverSource routes them to the queue strategy.
92
+ queue.queues.forEach((subscriber) => {
93
+ resolverRegistry[`${queue.name}_${subscriber.name}`] =
94
+ createResolverEntry.queueSubscriber(subscriber);
95
+ });
90
96
  return acc;
91
97
  }, {});
92
98
 
@@ -143,7 +143,14 @@ export const generateOrderByInputType = (mergedEntities: MergedEntities) =>
143
143
  ),
144
144
  );
145
145
 
146
- const aggTypes: string[] = ["Min", "Max", "Sum", "Avg"];
146
+ // An average is fractional whatever the column holds, so it cannot inherit the
147
+ // column's type the way min/max/sum do.
148
+ const aggTypes: { name: string; graphQLType?: string }[] = [
149
+ { name: "Min" },
150
+ { name: "Max" },
151
+ { name: "Sum" },
152
+ { name: "Avg", graphQLType: "Float" },
153
+ ];
147
154
 
148
155
  // Generate aggregation types for each table
149
156
  export const generateAggregationTypes = (mergedEntities: MergedEntities) => {
@@ -155,10 +162,10 @@ export const generateAggregationTypes = (mergedEntities: MergedEntities) => {
155
162
  numericColumns.length
156
163
  ? aggTypes
157
164
  .map(
158
- (agg) =>
165
+ ({ name: agg, graphQLType }) =>
159
166
  `
160
167
  type ${resolverName}${agg} {
161
- ${lj(numericColumns, ({ name }) => `${name}: ${mapSQLTypeToGraphQLType(columns.find((c) => c.name === name)?.dataType || "")}`)}
168
+ ${lj(numericColumns, ({ name }) => `${name}: ${graphQLType ?? mapSQLTypeToGraphQLType(columns.find((c) => c.name === name)?.dataType || "")}`)}
162
169
  }
163
170
  `,
164
171
  )
@@ -171,7 +178,9 @@ export const generateAggregationTypes = (mergedEntities: MergedEntities) => {
171
178
  count: Int
172
179
  ${
173
180
  numericColumns.length
174
- ? aggTypes.map((agg) => `${agg.toLowerCase()}: ${resolverName}${agg}`).join("\n")
181
+ ? aggTypes
182
+ .map(({ name: agg }) => `${agg.toLowerCase()}: ${resolverName}${agg}`)
183
+ .join("\n")
175
184
  : ""
176
185
  }
177
186
  items: [${resolverName}]
@@ -189,23 +189,19 @@ export const generateOpenAPI = ({
189
189
  parameters: [
190
190
  ...(Object.entries(
191
191
  pathParametersPOJO.properties ?? ({} as OpenAPIV3_1.SchemaObject),
192
- ).map(([key, { type }]) => ({
192
+ ).map(([key, paramSchema]) => ({
193
193
  name: key,
194
194
  in: "path",
195
195
  required: true,
196
- schema: {
197
- type,
198
- },
196
+ schema: paramSchema,
199
197
  })) ?? []),
200
198
  ...Object.entries(
201
199
  queryParametersPOJO.properties ?? ({} as OpenAPIV3_1.SchemaObject),
202
- ).map(([key, { type, required }]) => ({
200
+ ).map(([key, paramSchema]) => ({
203
201
  name: key,
204
202
  in: "query",
205
- required,
206
- schema: {
207
- type,
208
- },
203
+ required: queryParametersPOJO.required?.includes(key) ?? false,
204
+ schema: paramSchema,
209
205
  })),
210
206
  ],
211
207
  ...(hasBody && ["POST", "PUT", "PATCH"].includes(value.rest!.method!)
@@ -114,24 +114,28 @@ export const handleRESTRequestFactory = (
114
114
  });
115
115
  }
116
116
 
117
- // Validate and parse path parameters with Zod
118
- const pathVariables = route.rest!.pathParams?.parse(pathParameters) as Record<
119
- string,
120
- unknown
121
- >;
117
+ // Validate and parse each REST parameter source with Zod. A source is
118
+ // left `undefined` when its schema is not configured; that `undefined` is
119
+ // forwarded to `beforeRequest` so the hook can tell the sources apart,
120
+ // while `allVariables` stays identical because spreading `undefined` is a
121
+ // no-op.
122
+ const pathVariables = route.rest!.pathParams?.parse(pathParameters) as
123
+ | Record<string, unknown>
124
+ | undefined;
122
125
 
123
126
  const paramsQuery = new URLSearchParams(url.search);
124
127
  const paramsQueryDictionary = Object.fromEntries(paramsQuery.entries());
125
128
 
126
- const queryVariables = (route.rest!.queryParams?.parse(paramsQueryDictionary) ??
127
- {}) as Record<string, unknown>;
129
+ const queryVariables = route.rest!.queryParams?.parse(paramsQueryDictionary) as
130
+ | Record<string, unknown>
131
+ | undefined;
128
132
 
129
- let bodyVariables: Record<string, unknown> = {};
133
+ let bodyVariables: Record<string, unknown> | undefined;
130
134
 
131
135
  if (req.method === "POST") {
132
136
  const body = req.body ? await req.json() : {};
133
137
 
134
- bodyVariables = (route.rest!.body?.parse(body) ?? {}) as Record<string, unknown>;
138
+ bodyVariables = route.rest!.body?.parse(body) as Record<string, unknown> | undefined;
135
139
  }
136
140
 
137
141
  // Prepare all variables for the request
@@ -150,6 +154,9 @@ export const handleRESTRequestFactory = (
150
154
  (await route.hooks?.beforeRequest?.(
151
155
  {
152
156
  input: allVariables,
157
+ pathParams: pathVariables,
158
+ queryParams: queryVariables,
159
+ body: bodyVariables,
153
160
  },
154
161
  routesInitDataPromises[route.routeKey],
155
162
  )) ?? allVariables;
@@ -286,6 +293,19 @@ export const handleRESTRequestFactory = (
286
293
  }
287
294
  }
288
295
 
296
+ // Apply the operation's afterRequest hook (if configured) to a GraphQL
297
+ // result. The hook receives the unwrapped `data` payload; its return
298
+ // replaces `data`, leaving the rest of the envelope (e.g. `sqlQueries`)
299
+ // intact.
300
+ const applyAfterRequest = async (result: Awaited<ReturnType<typeof gql.handler>>) => {
301
+ if (!route.hooks?.afterRequest) return result;
302
+
303
+ return {
304
+ ...result,
305
+ data: await route.hooks.afterRequest({ output: result.data }),
306
+ };
307
+ };
308
+
289
309
  // Check if this route has caching enabled
290
310
  const cache = route.routeKey ? getCache(route.routeKey) : undefined;
291
311
 
@@ -299,7 +319,8 @@ export const handleRESTRequestFactory = (
299
319
  role: session?.role,
300
320
  });
301
321
 
302
- // Try to get from cache first
322
+ // Try to get from cache first. A cached entry has already been through
323
+ // afterRequest, so serve it directly without re-running the hook.
303
324
  const cachedResult = await cache.get(cacheKey);
304
325
  if (cachedResult) {
305
326
  log.debug({ route: route.routeKey }, "rest cache hit");
@@ -308,10 +329,13 @@ export const handleRESTRequestFactory = (
308
329
  log.debug({ route: route.routeKey }, "rest cache miss");
309
330
 
310
331
  try {
311
- // Execute the GraphQL request
312
- const result = await gql.handler(queryAnalysis!, variables, req, session);
332
+ // Execute the GraphQL request, then transform via afterRequest before
333
+ // caching so hits and misses return the same shape.
334
+ const result = await applyAfterRequest(
335
+ await gql.handler(queryAnalysis!, variables, req, session),
336
+ );
313
337
 
314
- // Cache the result
338
+ // Cache the (already transformed) result
315
339
  await cache.set(cacheKey, result);
316
340
 
317
341
  return new S200(result);
@@ -321,7 +345,9 @@ export const handleRESTRequestFactory = (
321
345
  }
322
346
  } else if (queryAnalysis) {
323
347
  // No caching for this route, execute normally
324
- return new S200(await gql.handler(queryAnalysis, variables, req, session));
348
+ return new S200(
349
+ await applyAfterRequest(await gql.handler(queryAnalysis, variables, req, session)),
350
+ );
325
351
  }
326
352
 
327
353
  // Fallback - should not reach here if endpoint is properly configured