@graphoria/server 0.1.5 → 0.2.1

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 (135) hide show
  1. package/dist/src/analyzeQuery/resolveVariables.d.ts.map +1 -1
  2. package/dist/src/authentication/jwt.d.ts.map +1 -1
  3. package/dist/src/authentication/paseto.d.ts.map +1 -1
  4. package/dist/src/authentication/tokenRepository.d.ts +2 -1
  5. package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
  6. package/dist/src/config/helpers/operationHelper.d.ts +60 -6
  7. package/dist/src/config/helpers/operationHelper.d.ts.map +1 -1
  8. package/dist/src/config/helpers/operationHelper.test.d.ts +2 -0
  9. package/dist/src/config/helpers/operationHelper.test.d.ts.map +1 -0
  10. package/dist/src/config/index.d.ts +65 -60
  11. package/dist/src/config/index.d.ts.map +1 -1
  12. package/dist/src/config/types/ai.d.ts +9 -3
  13. package/dist/src/config/types/ai.d.ts.map +1 -1
  14. package/dist/src/config/types/auth.d.ts +55 -15
  15. package/dist/src/config/types/auth.d.ts.map +1 -1
  16. package/dist/src/config/types/cron.d.ts +96 -2
  17. package/dist/src/config/types/cron.d.ts.map +1 -1
  18. package/dist/src/config/types/db.d.ts +102 -29
  19. package/dist/src/config/types/db.d.ts.map +1 -1
  20. package/dist/src/config/types/index.d.ts +1 -1
  21. package/dist/src/config/types/index.d.ts.map +1 -1
  22. package/dist/src/config/types/operation.d.ts +49 -17
  23. package/dist/src/config/types/operation.d.ts.map +1 -1
  24. package/dist/src/config/types/queue.d.ts +61 -14
  25. package/dist/src/config/types/queue.d.ts.map +1 -1
  26. package/dist/src/config/types/remote-rest.d.ts +10 -1
  27. package/dist/src/config/types/remote-rest.d.ts.map +1 -1
  28. package/dist/src/config/types/remote-schema.d.ts +13 -3
  29. package/dist/src/config/types/remote-schema.d.ts.map +1 -1
  30. package/dist/src/config/types/virtual-columns.d.ts +1 -1
  31. package/dist/src/config/types/virtual-columns.d.ts.map +1 -1
  32. package/dist/src/configuration/getSchemas/index.d.ts +6 -4
  33. package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
  34. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +9 -2
  35. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
  36. package/dist/src/configuration/getSchemas/mergeEntities/index.test.d.ts +2 -0
  37. package/dist/src/configuration/getSchemas/mergeEntities/index.test.d.ts.map +1 -0
  38. package/dist/src/configuration/getSchemas/type-definition-generator/index.d.ts.map +1 -1
  39. package/dist/src/configuration/index.d.ts +5 -4
  40. package/dist/src/configuration/index.d.ts.map +1 -1
  41. package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
  42. package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
  43. package/dist/src/configuration/rest/parseStringParams.d.ts +15 -0
  44. package/dist/src/configuration/rest/parseStringParams.d.ts.map +1 -0
  45. package/dist/src/configuration/rest/parseStringParams.test.d.ts +2 -0
  46. package/dist/src/configuration/rest/parseStringParams.test.d.ts.map +1 -0
  47. package/dist/src/databases/common.d.ts +9 -1
  48. package/dist/src/databases/common.d.ts.map +1 -1
  49. package/dist/src/databases/directives.d.ts.map +1 -1
  50. package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
  51. package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
  52. package/dist/src/databases/engines/mssql/getStructure.d.ts +1 -0
  53. package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
  54. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  55. package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
  56. package/dist/src/databases/engines/mysql/getStructure.d.ts +1 -0
  57. package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
  58. package/dist/src/databases/engines/mysql/placeholders.d.ts +22 -0
  59. package/dist/src/databases/engines/mysql/placeholders.d.ts.map +1 -0
  60. package/dist/src/databases/engines/mysql/placeholders.test.d.ts +2 -0
  61. package/dist/src/databases/engines/mysql/placeholders.test.d.ts.map +1 -0
  62. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  63. package/dist/src/databases/engines/postgresql/getStructure.d.ts +1 -0
  64. package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
  65. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  66. package/dist/src/databases/high-level-operations.d.ts +1 -0
  67. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  68. package/dist/src/databases/identifierMapping.test.d.ts +2 -0
  69. package/dist/src/databases/identifierMapping.test.d.ts.map +1 -0
  70. package/dist/src/databases/metadata/structure.d.ts +1 -0
  71. package/dist/src/databases/metadata/structure.d.ts.map +1 -1
  72. package/dist/src/databases/sqlTypeUtils.d.ts.map +1 -1
  73. package/dist/src/databases/transformers/data-transformers.d.ts +1 -0
  74. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  75. package/dist/src/databases/transformers/genResolverName.d.ts.map +1 -1
  76. package/dist/src/databases/transformers/graphqlName.d.ts +23 -0
  77. package/dist/src/databases/transformers/graphqlName.d.ts.map +1 -0
  78. package/dist/src/databases/transformers/graphqlName.test.d.ts +2 -0
  79. package/dist/src/databases/transformers/graphqlName.test.d.ts.map +1 -0
  80. package/dist/src/singletons/env.d.ts +2 -2
  81. package/dist/src/types/env.d.ts +56 -56
  82. package/dist/src/types/resolver.d.ts +12 -2
  83. package/dist/src/types/resolver.d.ts.map +1 -1
  84. package/dist/src/types/zod/auth.d.ts +6 -6
  85. package/dist/src/types/zod/configuration.d.ts +52 -52
  86. package/dist/src/types/zod/configuration.d.ts.map +1 -1
  87. package/dist/src/types/zod/cron.d.ts +1 -1
  88. package/dist/src/types/zod/cron.d.ts.map +1 -1
  89. package/dist/src/types/zod/db.d.ts +21 -12
  90. package/dist/src/types/zod/db.d.ts.map +1 -1
  91. package/dist/src/types/zod/queue.d.ts +38 -38
  92. package/dist/src/types/zod/strict-keys.test.d.ts +2 -0
  93. package/dist/src/types/zod/strict-keys.test.d.ts.map +1 -0
  94. package/dist/tsconfig.tsbuildinfo +1 -1
  95. package/package.json +2 -2
  96. package/src/analyzeQuery/resolveVariables.ts +37 -7
  97. package/src/authentication/jwt.ts +3 -1
  98. package/src/authentication/paseto.ts +3 -1
  99. package/src/authentication/tokenRepository.ts +12 -4
  100. package/src/config/helpers/operationHelper.ts +146 -5
  101. package/src/config/index.ts +139 -62
  102. package/src/config/types/ai.ts +2 -2
  103. package/src/config/types/auth.ts +37 -6
  104. package/src/config/types/cron.ts +1 -1
  105. package/src/config/types/db.ts +10 -10
  106. package/src/config/types/index.ts +2 -0
  107. package/src/config/types/operation.ts +49 -9
  108. package/src/config/types/queue.ts +8 -8
  109. package/src/config/types/remote-rest.ts +1 -1
  110. package/src/config/types/remote-schema.ts +2 -2
  111. package/src/config/types/virtual-columns.ts +1 -1
  112. package/src/configuration/getSchemas/mergeEntities/index.ts +32 -1
  113. package/src/configuration/getSchemas/type-definition-generator/index.ts +23 -9
  114. package/src/configuration/rest/generateOpenAPI.ts +5 -9
  115. package/src/configuration/rest/handleRESTRequestFactory.ts +38 -14
  116. package/src/configuration/rest/parseStringParams.ts +92 -0
  117. package/src/databases/common.ts +163 -66
  118. package/src/databases/directives.ts +26 -8
  119. package/src/databases/engines/mssql/connection.ts +1 -1
  120. package/src/databases/engines/mssql/query/index.ts +47 -16
  121. package/src/databases/engines/mysql/auth.ts +2 -2
  122. package/src/databases/engines/mysql/connection.ts +5 -4
  123. package/src/databases/engines/mysql/getStructure.ts +61 -30
  124. package/src/databases/engines/mysql/placeholders.ts +84 -0
  125. package/src/databases/engines/mysql/query/index.ts +68 -10
  126. package/src/databases/engines/postgresql/query/index.ts +59 -7
  127. package/src/databases/high-level-operations.ts +2 -2
  128. package/src/databases/sqlTypeUtils.ts +3 -1
  129. package/src/databases/transformers/genResolverName.ts +3 -3
  130. package/src/databases/transformers/graphqlName.ts +28 -0
  131. package/src/subscriptions/strategies/index.ts +1 -1
  132. package/src/subscriptions/strategies/queue.ts +1 -1
  133. package/src/types/resolver.ts +17 -1
  134. package/src/types/zod/configuration.ts +1 -1
  135. package/src/types/zod/db.ts +4 -0
@@ -28,18 +28,49 @@ export type DirectionUnion = z.input<typeof DirectionUnionZod>;
28
28
  /**
29
29
  * Order by clause for role-based default ordering
30
30
  */
31
- export const OrderByClauseZod = z.object({
31
+ export const OrderByClauseZod = z.strictObject({
32
32
  column: z.string(),
33
33
  direction: DirectionUnionZod,
34
34
  });
35
35
 
36
36
  export type OrderByClause = z.input<typeof OrderByClauseZod>;
37
37
 
38
+ /**
39
+ * Every operator the query builder implements. `databases/common.ts` imports this
40
+ * list rather than keeping its own: an operator that exists in one place and not
41
+ * the other is how a role filter ends up matching every row.
42
+ */
43
+ export const FILTER_OPERATORS = [
44
+ "eq",
45
+ "neq",
46
+ "gt",
47
+ "gte",
48
+ "lt",
49
+ "lte",
50
+ "like",
51
+ "in",
52
+ "between",
53
+ "is_null",
54
+ "is_not_null",
55
+ "not_null",
56
+ ] as const;
57
+
58
+ export type FilterOperator = (typeof FILTER_OPERATORS)[number];
59
+
38
60
  /**
39
61
  * Filter condition — matches GraphQL where argument structure.
40
- * Supports operators: eq, neq, gt, gte, lt, lte, like, in, is_null, not_null.
62
+ *
63
+ * A value is either a column filter (`{ eq: … }`) or a nested relation filter
64
+ * holding more of the same. Operators are constrained to the implemented set so
65
+ * a typo fails at boot instead of evaluating to no condition, which would hand
66
+ * the role the whole table.
41
67
  */
42
- export const FilterConditionZod = z.record(z.string(), z.record(z.string(), z.unknown()));
68
+ export const FilterConditionZod: z.ZodType<Record<string, unknown>> = z.lazy(() =>
69
+ z.record(
70
+ z.string(),
71
+ z.union([z.record(z.enum(FILTER_OPERATORS), z.unknown()), FilterConditionZod]),
72
+ ),
73
+ );
43
74
 
44
75
  export type FilterCondition = z.input<typeof FilterConditionZod>;
45
76
 
@@ -50,7 +81,7 @@ export type FilterCondition = z.input<typeof FilterConditionZod>;
50
81
  /**
51
82
  * Table-level permission configuration
52
83
  */
53
- export const TablePermissionZod = z.object({
84
+ export const TablePermissionZod = z.strictObject({
54
85
  /** Allowed columns — "ALL" or array of column names */
55
86
  columns: z.union([z.literal("ALL"), z.array(z.string())]),
56
87
  /** Role-based query filtering (WHERE clause) */
@@ -65,7 +96,7 @@ export type TablePermission = z.input<typeof TablePermissionZod>;
65
96
  * Permission configuration for a role
66
97
  */
67
98
  export const RolePermissionZod = z
68
- .object({
99
+ .strictObject({
69
100
  /** Tables accessible by this role */
70
101
  tables: z
71
102
  .union([
@@ -120,7 +151,7 @@ export type RolePermission = z.input<typeof RolePermissionZod>;
120
151
  * Authentication configuration
121
152
  */
122
153
  export const AuthConfigZod = z
123
- .object({
154
+ .strictObject({
124
155
  /** Whether authentication is enabled */
125
156
  enabled: z.boolean(),
126
157
  /** Database name where auth tables are stored */
@@ -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
  */
@@ -42,7 +42,7 @@ export const RELATIONSHIP_CONDITION_OPERATORS = [
42
42
  * A `value` is required for every operator except `is_null` / `is_not_null`.
43
43
  */
44
44
  export const RelationshipConditionZod = z
45
- .object({
45
+ .strictObject({
46
46
  /** Column on the declaring (source) table */
47
47
  source: z.string().optional(),
48
48
  /** Column on the referenced (target) table */
@@ -67,10 +67,10 @@ export type RelationshipCondition = z.input<typeof RelationshipConditionZod>;
67
67
  // Table Relationship (config shape, no transform)
68
68
  // ============================================================================
69
69
 
70
- export const TableRelationshipZod = z.object({
70
+ export const TableRelationshipZod = z.strictObject({
71
71
  schema: z.string(),
72
72
  name: z.string(),
73
- columns: z.array(z.object({ source: z.string(), target: z.string() })),
73
+ columns: z.array(z.strictObject({ source: z.string(), target: z.string() })),
74
74
  /** Static-value predicates ANDed into the JOIN condition */
75
75
  conditions: z.array(RelationshipConditionZod).optional(),
76
76
  });
@@ -81,7 +81,7 @@ export type TableRelationship = z.input<typeof TableRelationshipZod>;
81
81
  // Table Schema Config
82
82
  // ============================================================================
83
83
 
84
- export const TableSchemaConfigZod = z.object({
84
+ export const TableSchemaConfigZod = z.strictObject({
85
85
  columns: z.array(VirtualColumnZod).optional().default([]),
86
86
  relationships: z.array(TableRelationshipZod).optional().default([]),
87
87
  /** Overrides the table description from the database */
@@ -96,7 +96,7 @@ export type TableSchemaConfig = z.input<typeof TableSchemaConfigZod>;
96
96
  // Database Schema Config
97
97
  // ============================================================================
98
98
 
99
- export const DatabaseSchemaConfigZod = z.object({
99
+ export const DatabaseSchemaConfigZod = z.strictObject({
100
100
  database: z
101
101
  .record(
102
102
  z.string(),
@@ -127,7 +127,7 @@ export type DatabaseSchemaConfig = z.input<typeof DatabaseSchemaConfigZod>;
127
127
  * Connection pool and transport options for PostgreSQL and MySQL (Bun SQL).
128
128
  * All timeout values are in seconds.
129
129
  */
130
- export const BunSQLConnectionOptionsZod = z.object({
130
+ export const BunSQLConnectionOptionsZod = z.strictObject({
131
131
  /** Maximum number of connections in the pool */
132
132
  max: z.number().int().positive().default(10),
133
133
  /** Maximum time in seconds a connection can be idle before being closed */
@@ -150,10 +150,10 @@ export type BunSQLConnectionOptions = z.input<typeof BunSQLConnectionOptionsZod>
150
150
  * Connection pool and transport options for MSSQL.
151
151
  * All timeout values are in seconds (converted to milliseconds internally).
152
152
  */
153
- export const MSSQLConnectionOptionsZod = z.object({
153
+ export const MSSQLConnectionOptionsZod = z.strictObject({
154
154
  /** Connection pool options */
155
155
  pool: z
156
- .object({
156
+ .strictObject({
157
157
  /** Maximum number of connections in the pool */
158
158
  max: z.number().int().positive().default(10),
159
159
  /** Minimum number of connections in the pool */
@@ -228,7 +228,7 @@ export type ConnectionOptionsForType<T extends DatabaseType> = T extends "pg" |
228
228
  // Database Connection Zod Schema
229
229
  // ============================================================================
230
230
 
231
- export const DatabaseConnectionZod = z.object({
231
+ export const DatabaseConnectionZod = z.strictObject({
232
232
  /** Unique name for the database connection */
233
233
  name: z.string(),
234
234
  /** Whether the database is enabled */
@@ -236,7 +236,7 @@ export const DatabaseConnectionZod = z.object({
236
236
  /** Database type */
237
237
  type: z.union([z.literal("mssql"), z.literal("pg"), z.literal("mysql")]),
238
238
  /** Connection configuration */
239
- connection: z.object({
239
+ connection: z.strictObject({
240
240
  host: z.string(),
241
241
  port: z.number(),
242
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,
@@ -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(),
@@ -6,6 +6,7 @@ import type { ResolverRegistry } from "../../../types/resolver";
6
6
  import type { TypedOperation } from "../../../types/zod/operation";
7
7
 
8
8
  import { EntitySource, createResolverEntry } from "../../../types/resolver";
9
+ import { columnFieldName } from "../../../databases/transformers/graphqlName";
9
10
  import { SqlTypeCategory, categorizeSqlType } from "../../../databases/sqlTypeUtils";
10
11
 
11
12
  const dataTypeToOpenApiType = (dataType: string): string => {
@@ -33,7 +34,23 @@ export const mergeEntities = (
33
34
  const resolverRegistry: ResolverRegistry = {};
34
35
 
35
36
  // Register tables (queries)
37
+ // Sanitisation is not injective — `categoría` and `categor_a` both produce
38
+ // `categoria` — so two tables can land on one resolver name. Serving one
39
+ // table's rows under the other's name is worse than not booting.
40
+ const sourceOfResolverName: Record<string, string> = {};
41
+
36
42
  const queriesMap = entityOfRole.tables.reduce<Record<string, TableResolver>>((acc, table) => {
43
+ const source = `${table.schema}.${table.name}`;
44
+ const previousSource = sourceOfResolverName[table.resolverName];
45
+
46
+ if (previousSource) {
47
+ throw new Error(
48
+ `Duplicate GraphQL name "${table.resolverName}": both ${previousSource} and ${source} map to it. Rename one of them, or exclude it with schema.excludedTables.`,
49
+ );
50
+ }
51
+
52
+ sourceOfResolverName[table.resolverName] = source;
53
+
37
54
  acc[table.resolverName] = table;
38
55
  acc[`${table.resolverName}_single`] = table;
39
56
  acc[`${table.resolverName}_aggregate`] = table;
@@ -87,6 +104,12 @@ export const mergeEntities = (
87
104
  acc[publisher.resolverName] = publisher;
88
105
  });
89
106
  });
107
+ // Subscribers become subscriptions (not mutations): register in the resolver
108
+ // registry only — not queuesMap — so getResolverSource routes them to the queue strategy.
109
+ queue.queues.forEach((subscriber) => {
110
+ resolverRegistry[`${queue.name}_${subscriber.name}`] =
111
+ createResolverEntry.queueSubscriber(subscriber);
112
+ });
90
113
  return acc;
91
114
  }, {});
92
115
 
@@ -201,10 +224,18 @@ export const mergeEntities = (
201
224
  },
202
225
  isVirtualColumn: (table: string, column: string): VirtualColumn | undefined => {
203
226
  const col = queriesMap[table]?.columns.find(
204
- (c) => c.name === column && "virtual" in c && c.virtual,
227
+ (c) => columnFieldName(c) === column && "virtual" in c && c.virtual,
205
228
  );
206
229
  return col ? (col as VirtualColumn) : undefined;
207
230
  },
231
+ /**
232
+ * The SQL identifier behind a GraphQL field name. Columns whose name GraphQL
233
+ * cannot spell are exposed under a sanitised field name, so anything that
234
+ * emits SQL for a field the client named has to come back through here.
235
+ * Unknown names pass through: a legal name always sanitises to itself.
236
+ */
237
+ columnSqlName: (table: string, fieldName: string): string =>
238
+ queriesMap[table]?.columns.find((c) => columnFieldName(c) === fieldName)?.name ?? fieldName,
208
239
  getColumnTypeForOpenApi: (table: string, column: string) => {
209
240
  const tableObj = queriesMap[table];
210
241
 
@@ -1,6 +1,7 @@
1
1
  import type { OpenAPIV3_1 } from "openapi-types";
2
2
 
3
3
  import { SqlTypeCategory, categorizeSqlType, isNumericType } from "../../../databases/sqlTypeUtils";
4
+ import { columnFieldName } from "../../../databases/transformers/graphqlName";
4
5
  import { convertFromZod } from "../../rest/openApiErrors";
5
6
  import { type MergedEntities } from "../mergeEntities";
6
7
 
@@ -91,8 +92,8 @@ export const generateTableType = (mergedEntities: MergedEntities) =>
91
92
  ${lj([
92
93
  lj(
93
94
  columns,
94
- ({ name, dataType, isNullable, description }) =>
95
- `${sdlDesc(description)}${name}: ${mapSQLTypeToGraphQLType(dataType)}${isNullable ? "" : "!"}`,
95
+ (column) =>
96
+ `${sdlDesc(column.description)}${columnFieldName(column)}: ${mapSQLTypeToGraphQLType(column.dataType)}${column.isNullable ? "" : "!"}`,
96
97
  ),
97
98
  lj(
98
99
  relationships,
@@ -115,7 +116,11 @@ export const generateWhereInputType = (mergedEntities: MergedEntities) =>
115
116
  ({ resolverName, columns, relationships, relationshipsReversed }) => `
116
117
  input ${resolverName}WhereInput {
117
118
  ${lj([
118
- lj(columns, ({ name, dataType }) => `${name}: ${mapSQLTypeToConditionType(dataType)}`),
119
+ lj(
120
+ columns,
121
+ (column) =>
122
+ `${columnFieldName(column)}: ${mapSQLTypeToConditionType(column.dataType)}`,
123
+ ),
119
124
  lj(
120
125
  relationships,
121
126
  ({ toResolverName, toInternalName }) =>
@@ -138,12 +143,19 @@ export const generateOrderByInputType = (mergedEntities: MergedEntities) =>
138
143
  mergedEntities.tables.map(
139
144
  ({ internalName, columns }) => `
140
145
  input ${internalName}OrderByInput {
141
- ${lj(columns, ({ name }) => `${name}: OrderByEnum`)}
146
+ ${lj(columns, (column) => `${columnFieldName(column)}: OrderByEnum`)}
142
147
  }`,
143
148
  ),
144
149
  );
145
150
 
146
- const aggTypes: string[] = ["Min", "Max", "Sum", "Avg"];
151
+ // An average is fractional whatever the column holds, so it cannot inherit the
152
+ // column's type the way min/max/sum do.
153
+ const aggTypes: { name: string; graphQLType?: string }[] = [
154
+ { name: "Min" },
155
+ { name: "Max" },
156
+ { name: "Sum" },
157
+ { name: "Avg", graphQLType: "Float" },
158
+ ];
147
159
 
148
160
  // Generate aggregation types for each table
149
161
  export const generateAggregationTypes = (mergedEntities: MergedEntities) => {
@@ -155,10 +167,10 @@ export const generateAggregationTypes = (mergedEntities: MergedEntities) => {
155
167
  numericColumns.length
156
168
  ? aggTypes
157
169
  .map(
158
- (agg) =>
170
+ ({ name: agg, graphQLType }) =>
159
171
  `
160
172
  type ${resolverName}${agg} {
161
- ${lj(numericColumns, ({ name }) => `${name}: ${mapSQLTypeToGraphQLType(columns.find((c) => c.name === name)?.dataType || "")}`)}
173
+ ${lj(numericColumns, (column) => `${columnFieldName(column)}: ${graphQLType ?? mapSQLTypeToGraphQLType(column.dataType)}`)}
162
174
  }
163
175
  `,
164
176
  )
@@ -171,14 +183,16 @@ export const generateAggregationTypes = (mergedEntities: MergedEntities) => {
171
183
  count: Int
172
184
  ${
173
185
  numericColumns.length
174
- ? aggTypes.map((agg) => `${agg.toLowerCase()}: ${resolverName}${agg}`).join("\n")
186
+ ? aggTypes
187
+ .map(({ name: agg }) => `${agg.toLowerCase()}: ${resolverName}${agg}`)
188
+ .join("\n")
175
189
  : ""
176
190
  }
177
191
  items: [${resolverName}]
178
192
  }
179
193
 
180
194
  enum ${resolverName}GroupByKeys {
181
- ${columns.map((c) => c.name).join("\n")}
195
+ ${columns.map(columnFieldName).join("\n")}
182
196
  }
183
197
  `;
184
198
  });
@@ -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!)