@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphoria/server",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Auto-generated GraphQL and REST APIs from database schema - Connect your database to modern APIs instantly",
5
5
  "keywords": [
6
6
  "auto-generated",
@@ -116,4 +116,4 @@
116
116
  "engines": {
117
117
  "bun": ">=1.3.4"
118
118
  }
119
- }
119
+ }
@@ -226,6 +226,37 @@ const resolveSessionInArguments = (
226
226
  return result;
227
227
  };
228
228
 
229
+ /**
230
+ * Recursively replaces `$varName` references with their resolved (already-flattened) values from
231
+ * `resolvedMap`, walking nested objects/arrays so a reference nested inside an inline argument
232
+ * object (e.g. `where: { rel: { sub: $where } }`) is substituted like a top-level one. The resolved
233
+ * value is inserted as-is — it is not walked further, so its own `$static_N` leaves survive for SQL
234
+ * parameter binding. Refs absent from `resolvedMap` (scalar vars, `$session.*`) pass through.
235
+ */
236
+ const substituteResolvedRefs = (value: unknown, resolvedMap: Map<string, unknown>): unknown => {
237
+ if (isString(value)) {
238
+ if (value.startsWith("$")) {
239
+ const varName = value.substring(1);
240
+ if (resolvedMap.has(varName)) return resolvedMap.get(varName);
241
+ }
242
+ return value;
243
+ }
244
+
245
+ if (Array.isArray(value)) {
246
+ return value.map((item) => substituteResolvedRefs(item, resolvedMap));
247
+ }
248
+
249
+ if (value !== null && typeof value === "object") {
250
+ const result: Record<string, unknown> = {};
251
+ for (const [key, nested] of Object.entries(value)) {
252
+ result[key] = substituteResolvedRefs(nested, resolvedMap);
253
+ }
254
+ return result;
255
+ }
256
+
257
+ return value;
258
+ };
259
+
229
260
  /**
230
261
  * Returns a new field tree with:
231
262
  * 1. `$varName` argument references replaced with their resolved objects from `resolvedMap`
@@ -245,13 +276,12 @@ export const resolveFieldArguments = (
245
276
  // Shallow-copy arguments so we don't mutate originals
246
277
  newArguments = { ...field.arguments };
247
278
 
248
- // Replace object variable references in arguments
249
- for (const [key, value] of Object.entries(newArguments)) {
250
- if (isString(value) && value.startsWith("$")) {
251
- const varName = value.substring(1);
252
- if (resolvedMap.has(varName)) {
253
- newArguments[key] = resolvedMap.get(varName);
254
- }
279
+ // Replace object variable references anywhere in arguments — top-level (`where: $where`) or
280
+ // nested inside an inline object (`where: { rel: { sub: $where } }`). Only walk when there is
281
+ // something to substitute so the session-only path stays a no-op.
282
+ if (resolvedMap.size > 0) {
283
+ for (const [key, value] of Object.entries(newArguments)) {
284
+ newArguments[key] = substituteResolvedRefs(value, resolvedMap);
255
285
  }
256
286
  }
257
287
 
@@ -214,7 +214,9 @@ export const createJWTService = (
214
214
  verifyTokenAndGetSession,
215
215
  createTokenPair,
216
216
  refreshAccessToken,
217
- revoke: tokenRepository.revoke,
217
+ // The refresh lifetime is the longest a revocation has to outlive, so it is
218
+ // the TTL for a JTI the repository has not seen before.
219
+ revoke: (jti: string) => tokenRepository.revoke(jti, env.jwt.rtExpiresIn),
218
220
  isRevoked: tokenRepository.isRevoked,
219
221
  };
220
222
  };
@@ -250,7 +250,9 @@ export const createPASETOService = (
250
250
  verifyTokenAndGetSession,
251
251
  createTokenPair,
252
252
  refreshAccessToken,
253
- revoke: tokenRepository.revoke,
253
+ // The refresh lifetime is the longest a revocation has to outlive, so it is
254
+ // the TTL for a JTI the repository has not seen before.
255
+ revoke: (jti: string) => tokenRepository.revoke(jti, env.jwt.rtExpiresIn),
254
256
  isRevoked: tokenRepository.isRevoked,
255
257
  };
256
258
  };
@@ -6,7 +6,8 @@ import { logger } from "../logging";
6
6
  export type TokenRepository = {
7
7
  saveJti(jti: string, expiresIn: string): Promise<void>;
8
8
  isTokenUsed(jti: string): Promise<boolean>;
9
- revoke(jti: string): Promise<void>;
9
+ /** `expiresIn` only applies to a JTI with no TTL of its own — see `revoke`. */
10
+ revoke(jti: string, expiresIn: string): Promise<void>;
10
11
  isRevoked(jti: string): Promise<boolean>;
11
12
  };
12
13
 
@@ -39,11 +40,18 @@ export const createTokenRepositoryWithClient = (client: TokenRepositoryClient):
39
40
  }
40
41
  };
41
42
 
42
- const revoke = async (jti: string) => {
43
+ const revoke = async (jti: string, expiresIn: string) => {
43
44
  try {
44
- const [isUsed] = await client.hmget(jti, ["isUsed"]);
45
- if (isUsed !== "true") return;
45
+ const [isUsed, isRevoked] = await client.hmget(jti, ["isUsed", "isRevoked"]);
46
+ if (isRevoked === "true") return;
47
+
46
48
  await client.hset(jti, { isRevoked: "true" });
49
+
50
+ // saveJti is the only writer of a TTL, so `isUsed` doubles as "this key
51
+ // already expires on its own". Without one the revocation record would
52
+ // outlive the token it revokes, forever — but overwriting an existing TTL
53
+ // would move a refresh token's expiry, so only a fresh key gets one.
54
+ if (isUsed !== "true") await client.expire(jti, parseDurationToMs(expiresIn) / 1000);
47
55
  } catch (error) {
48
56
  log.error({ err: error }, "failed to revoke JTI");
49
57
  throw new Error("Token revocation failed");
@@ -1,10 +1,15 @@
1
+ import { print } from "graphql";
1
2
  import { z } from "zod";
2
3
 
4
+ import type { DocumentNode } from "graphql";
3
5
  import type {
6
+ AnyQueryDocument,
4
7
  DefaultInput,
5
8
  HandlerOperation,
6
9
  OperationHandler,
10
+ OperationOptions,
7
11
  QueryOperation,
12
+ QueryResultOf,
8
13
  TypedOperation,
9
14
  } from "../types/operation";
10
15
 
@@ -22,6 +27,70 @@ type InferInput<TInputSchema> = TInputSchema extends z.ZodType<infer T> ? T : De
22
27
  */
23
28
  type InferOutput<TOutputSchema> = TOutputSchema extends z.ZodType<infer T> ? T : unknown;
24
29
 
30
+ /**
31
+ * Infer the parsed shape of an optional REST parameter schema.
32
+ * Resolves to `undefined` when that source has no schema configured.
33
+ *
34
+ * Mirrors the helpers on the `OperationFn` authoring surface so both
35
+ * `operation()` entry points expose identical `beforeRequest` context types.
36
+ */
37
+ type InferRestParam<T> = T extends z.ZodType<infer O> ? O : undefined;
38
+
39
+ /**
40
+ * REST exposure config with per-source schema generics, so `pathParams`,
41
+ * `queryParams`, and `body` can be inferred into the `beforeRequest` context.
42
+ */
43
+ type RestConfigInput<
44
+ TPathParams extends z.ZodType | undefined,
45
+ TQueryParams extends z.ZodType | undefined,
46
+ TBody extends z.ZodType | undefined,
47
+ > = {
48
+ /** Route path (e.g. `/users/:id`) */
49
+ path: string;
50
+ /** HTTP method (defaults to GET) */
51
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
52
+ /** Zod schema for path parameters */
53
+ pathParams?: TPathParams;
54
+ /** Zod schema for query-string parameters */
55
+ queryParams?: TQueryParams;
56
+ /** Zod schema for the request body */
57
+ body?: TBody;
58
+ };
59
+
60
+ /**
61
+ * Hooks config where `beforeRequest` receives the merged `input` plus each REST
62
+ * source (`pathParams` / `queryParams` / `body`) typed from its schema, or
63
+ * `undefined` when that source's schema is omitted.
64
+ */
65
+ type OperationHooksConfig<
66
+ TInput,
67
+ TOutput,
68
+ TInitData,
69
+ TPathParams extends z.ZodType | undefined,
70
+ TQueryParams extends z.ZodType | undefined,
71
+ TBody extends z.ZodType | undefined,
72
+ TAfterInput = TOutput,
73
+ > = {
74
+ init?: (options: OperationOptions) => TInitData | Promise<TInitData>;
75
+ beforeRequest?: (
76
+ context: {
77
+ input: TInput;
78
+ pathParams: InferRestParam<TPathParams>;
79
+ queryParams: InferRestParam<TQueryParams>;
80
+ body: InferRestParam<TBody>;
81
+ },
82
+ initData: TInitData | undefined,
83
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
84
+ /**
85
+ * Transforms the result after a successful query/handler. `output` is the
86
+ * upstream value: for query operations the query result (typed from a gql.tada
87
+ * `query` document, otherwise `unknown`); for handler operations the handler's
88
+ * return. The value you return becomes the response payload, typed by the
89
+ * operation's `output` schema.
90
+ */
91
+ afterRequest?: (context: { output: TAfterInput }) => TOutput | Promise<TOutput>;
92
+ };
93
+
25
94
  /**
26
95
  * Config type for handler-based operations with inference
27
96
  */
@@ -30,13 +99,25 @@ type HandlerOperationConfig<
30
99
  TInputSchema extends z.ZodType | undefined,
31
100
  TOutputSchema extends z.ZodType | undefined,
32
101
  TInitData,
102
+ TPathParams extends z.ZodType | undefined,
103
+ TQueryParams extends z.ZodType | undefined,
104
+ TBody extends z.ZodType | undefined,
33
105
  > = Omit<
34
106
  HandlerOperation<InferInput<TInputSchema>, InferOutput<TOutputSchema>, TInitData, TRepository>,
35
- "handler" | "input" | "output"
107
+ "handler" | "input" | "output" | "hooks" | "rest"
36
108
  > & {
37
109
  input?: TInputSchema;
38
110
  output?: TOutputSchema;
39
111
  handler: OperationHandler<InferInput<TInputSchema>, InferOutput<TOutputSchema>, TRepository>;
112
+ hooks?: OperationHooksConfig<
113
+ InferInput<TInputSchema>,
114
+ InferOutput<TOutputSchema>,
115
+ TInitData,
116
+ TPathParams,
117
+ TQueryParams,
118
+ TBody
119
+ >;
120
+ rest?: RestConfigInput<TPathParams, TQueryParams, TBody>;
40
121
  };
41
122
 
42
123
  /**
@@ -46,12 +127,28 @@ type QueryOperationConfig<
46
127
  TInputSchema extends z.ZodType | undefined,
47
128
  TOutputSchema extends z.ZodType | undefined,
48
129
  TInitData,
130
+ TPathParams extends z.ZodType | undefined,
131
+ TQueryParams extends z.ZodType | undefined,
132
+ TBody extends z.ZodType | undefined,
133
+ TQuery extends string | AnyQueryDocument,
49
134
  > = Omit<
50
135
  QueryOperation<InferInput<TInputSchema>, InferOutput<TOutputSchema>, TInitData>,
51
- "input" | "output"
136
+ "input" | "output" | "hooks" | "rest" | "query"
52
137
  > & {
138
+ /** GraphQL query to execute (a string or a gql.tada `graphql()` document) */
139
+ query: TQuery;
53
140
  input?: TInputSchema;
54
141
  output?: TOutputSchema;
142
+ hooks?: OperationHooksConfig<
143
+ InferInput<TInputSchema>,
144
+ InferOutput<TOutputSchema>,
145
+ TInitData,
146
+ TPathParams,
147
+ TQueryParams,
148
+ TBody,
149
+ QueryResultOf<TQuery>
150
+ >;
151
+ rest?: RestConfigInput<TPathParams, TQueryParams, TBody>;
55
152
  };
56
153
 
57
154
  // Overload 1: Query-based operation (has `query`, no `handler`)
@@ -59,8 +156,20 @@ export function operation<
59
156
  TInputSchema extends z.ZodType | undefined = undefined,
60
157
  TOutputSchema extends z.ZodType | undefined = undefined,
61
158
  TInitData = unknown,
159
+ TPathParams extends z.ZodType | undefined = undefined,
160
+ TQueryParams extends z.ZodType | undefined = undefined,
161
+ TBody extends z.ZodType | undefined = undefined,
162
+ TQuery extends string | AnyQueryDocument = string,
62
163
  >(
63
- config: QueryOperationConfig<TInputSchema, TOutputSchema, TInitData>,
164
+ config: QueryOperationConfig<
165
+ TInputSchema,
166
+ TOutputSchema,
167
+ TInitData,
168
+ TPathParams,
169
+ TQueryParams,
170
+ TBody,
171
+ TQuery
172
+ >,
64
173
  ): QueryOperation<InferInput<TInputSchema>, InferOutput<TOutputSchema>, TInitData>;
65
174
 
66
175
  // Overload 2: Handler-based operation without custom repository type (no generic provided)
@@ -68,12 +177,33 @@ export function operation<
68
177
  TInputSchema extends z.ZodType | undefined = undefined,
69
178
  TOutputSchema extends z.ZodType | undefined = undefined,
70
179
  TInitData = unknown,
180
+ TPathParams extends z.ZodType | undefined = undefined,
181
+ TQueryParams extends z.ZodType | undefined = undefined,
182
+ TBody extends z.ZodType | undefined = undefined,
71
183
  >(
72
- config: HandlerOperationConfig<unknown, TInputSchema, TOutputSchema, TInitData>,
184
+ config: HandlerOperationConfig<
185
+ unknown,
186
+ TInputSchema,
187
+ TOutputSchema,
188
+ TInitData,
189
+ TPathParams,
190
+ TQueryParams,
191
+ TBody
192
+ >,
73
193
  ): HandlerOperation<InferInput<TInputSchema>, InferOutput<TOutputSchema>, TInitData, unknown>;
74
194
 
75
195
  // Implementation
76
196
  export function operation(config: unknown): unknown {
197
+ // gql.tada's `graphql()` yields a DocumentNode; normalize it to a string so
198
+ // the rest of the pipeline (validation, analyzeQuery) keeps working on strings.
199
+ const query = (config as { query?: unknown } | null | undefined)?.query;
200
+ if (
201
+ query != null &&
202
+ typeof query === "object" &&
203
+ (query as { kind?: unknown }).kind === "Document"
204
+ ) {
205
+ return { ...(config as object), query: print(query as DocumentNode) };
206
+ }
77
207
  return config;
78
208
  }
79
209
 
@@ -106,8 +236,19 @@ operation.typed = <TRepository>() => {
106
236
  TInputSchema extends z.ZodType | undefined = undefined,
107
237
  TOutputSchema extends z.ZodType | undefined = undefined,
108
238
  TInitData = unknown,
239
+ TPathParams extends z.ZodType | undefined = undefined,
240
+ TQueryParams extends z.ZodType | undefined = undefined,
241
+ TBody extends z.ZodType | undefined = undefined,
109
242
  >(
110
- config: HandlerOperationConfig<TRepository, TInputSchema, TOutputSchema, TInitData>,
243
+ config: HandlerOperationConfig<
244
+ TRepository,
245
+ TInputSchema,
246
+ TOutputSchema,
247
+ TInitData,
248
+ TPathParams,
249
+ TQueryParams,
250
+ TBody
251
+ >,
111
252
  ): HandlerOperation<
112
253
  InferInput<TInputSchema>,
113
254
  InferOutput<TOutputSchema>,
@@ -22,6 +22,7 @@
22
22
  import { z } from "zod";
23
23
 
24
24
  import type {
25
+ AnyQueryDocument,
25
26
  ConfigurationInput,
26
27
  CreateOneToBooleanMSSQLFn,
27
28
  CreateYAndNToBooleanMSSQLFn,
@@ -30,7 +31,7 @@ import type {
30
31
  OperationGraphQLConfig,
31
32
  OperationHandler,
32
33
  OperationOptions,
33
- OperationRestConfig,
34
+ QueryResultOf,
34
35
  TypedOperation,
35
36
  VirtualColumnExpressionFn,
36
37
  VirtualColumnFunctionFn,
@@ -52,6 +53,67 @@ type InferZodOutput<T> = T extends z.ZodType<infer O, any, any> ? O : never;
52
53
  type InferOutput<TOutputSchema> =
53
54
  TOutputSchema extends z.ZodType<any> ? InferZodOutput<TOutputSchema> : unknown;
54
55
 
56
+ /**
57
+ * Infer the parsed shape of an optional REST parameter schema.
58
+ * Resolves to `undefined` when that source has no schema configured.
59
+ */
60
+ type InferRestParam<T> = T extends z.ZodType<any> ? z.infer<T> : undefined;
61
+
62
+ /**
63
+ * REST exposure config with per-source schema generics, so `pathParams`,
64
+ * `queryParams`, and `body` can be inferred into the `beforeRequest` context.
65
+ */
66
+ type RestConfigInput<
67
+ TPathParams extends z.ZodType<any> | undefined,
68
+ TQueryParams extends z.ZodType<any> | undefined,
69
+ TBody extends z.ZodType<any> | undefined,
70
+ > = {
71
+ /** Route path (e.g. `/users/:id`) */
72
+ path: string;
73
+ /** HTTP method (defaults to GET) */
74
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
75
+ /** Zod schema for path parameters */
76
+ pathParams?: TPathParams;
77
+ /** Zod schema for query-string parameters */
78
+ queryParams?: TQueryParams;
79
+ /** Zod schema for the request body */
80
+ body?: TBody;
81
+ };
82
+
83
+ /**
84
+ * Hooks config shared by every operation overload. `beforeRequest` receives the
85
+ * merged `input` plus each REST source (`pathParams` / `queryParams` / `body`)
86
+ * typed from its schema, or `undefined` when that source's schema is omitted.
87
+ */
88
+ type OperationHooks<
89
+ TInput,
90
+ TOutput,
91
+ TInitData,
92
+ TPathParams extends z.ZodType<any> | undefined,
93
+ TQueryParams extends z.ZodType<any> | undefined,
94
+ TBody extends z.ZodType<any> | undefined,
95
+ TAfterInput = TOutput,
96
+ > = {
97
+ init?: (options: OperationOptions) => TInitData | Promise<TInitData>;
98
+ beforeRequest?: (
99
+ context: {
100
+ input: TInput;
101
+ pathParams: InferRestParam<TPathParams>;
102
+ queryParams: InferRestParam<TQueryParams>;
103
+ body: InferRestParam<TBody>;
104
+ },
105
+ initData: TInitData | undefined,
106
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
107
+ /**
108
+ * Transforms the result after a successful query/handler. `output` is the
109
+ * upstream value: for query operations the query result (typed from a gql.tada
110
+ * `query` document, otherwise `unknown`); for handler operations the handler's
111
+ * return. The value you return becomes the response payload, typed by the
112
+ * operation's `output` schema.
113
+ */
114
+ afterRequest?: (context: { output: TAfterInput }) => TOutput | Promise<TOutput>;
115
+ };
116
+
55
117
  /**
56
118
  * Type for the operation helper function provided by @graphoria/server.
57
119
  *
@@ -64,9 +126,13 @@ export type OperationFn = {
64
126
  TInputSchema extends z.ZodObject<any>,
65
127
  TOutputSchema extends z.ZodType<any> | undefined = undefined,
66
128
  TInitData = unknown,
129
+ TPathParams extends z.ZodType<any> | undefined = undefined,
130
+ TQueryParams extends z.ZodType<any> | undefined = undefined,
131
+ TBody extends z.ZodType<any> | undefined = undefined,
132
+ TQuery extends string | AnyQueryDocument = string,
67
133
  >(config: {
68
- /** GraphQL query to execute */
69
- query: string;
134
+ /** GraphQL query to execute (a string or a gql.tada `graphql()` document) */
135
+ query: TQuery;
70
136
  /** Custom handler is not allowed with query */
71
137
  handler?: never;
72
138
  /** Description for documentation */
@@ -76,18 +142,17 @@ export type OperationFn = {
76
142
  /** Output schema (Zod) */
77
143
  output?: TOutputSchema;
78
144
  /** Hooks for initialization and request transformation */
79
- hooks?: {
80
- init?: (options: OperationOptions) => TInitData | Promise<TInitData>;
81
- beforeRequest?: (
82
- context: { input: z.infer<TInputSchema> },
83
- initData: TInitData | undefined,
84
- ) => Record<string, unknown> | Promise<Record<string, unknown>>;
85
- afterRequest?: (context: {
86
- output: InferOutput<TOutputSchema>;
87
- }) => InferOutput<TOutputSchema> | Promise<InferOutput<TOutputSchema>>;
88
- };
145
+ hooks?: OperationHooks<
146
+ z.infer<TInputSchema>,
147
+ InferOutput<TOutputSchema>,
148
+ TInitData,
149
+ TPathParams,
150
+ TQueryParams,
151
+ TBody,
152
+ QueryResultOf<TQuery>
153
+ >;
89
154
  /** REST exposure configuration */
90
- rest?: OperationRestConfig;
155
+ rest?: RestConfigInput<TPathParams, TQueryParams, TBody>;
91
156
  /** GraphQL exposure configuration (enabled by default) */
92
157
  graphql?: OperationGraphQLConfig;
93
158
  /** Cache configuration */
@@ -95,9 +160,16 @@ export type OperationFn = {
95
160
  }): TypedOperation<z.infer<TInputSchema>, InferOutput<TOutputSchema>, TInitData, unknown>;
96
161
 
97
162
  // Query operation overload - without input schema
98
- <TOutputSchema extends z.ZodType<any> | undefined = undefined, TInitData = unknown>(config: {
99
- /** GraphQL query to execute */
100
- query: string;
163
+ <
164
+ TOutputSchema extends z.ZodType<any> | undefined = undefined,
165
+ TInitData = unknown,
166
+ TPathParams extends z.ZodType<any> | undefined = undefined,
167
+ TQueryParams extends z.ZodType<any> | undefined = undefined,
168
+ TBody extends z.ZodType<any> | undefined = undefined,
169
+ TQuery extends string | AnyQueryDocument = string,
170
+ >(config: {
171
+ /** GraphQL query to execute (a string or a gql.tada `graphql()` document) */
172
+ query: TQuery;
101
173
  /** Custom handler is not allowed with query */
102
174
  handler?: never;
103
175
  /** Description for documentation */
@@ -107,18 +179,17 @@ export type OperationFn = {
107
179
  /** Output schema (Zod) */
108
180
  output?: TOutputSchema;
109
181
  /** Hooks for initialization and request transformation */
110
- hooks?: {
111
- init?: (options: OperationOptions) => TInitData | Promise<TInitData>;
112
- beforeRequest?: (
113
- context: { input: DefaultInput },
114
- initData: TInitData | undefined,
115
- ) => Record<string, unknown> | Promise<Record<string, unknown>>;
116
- afterRequest?: (context: {
117
- output: InferOutput<TOutputSchema>;
118
- }) => InferOutput<TOutputSchema> | Promise<InferOutput<TOutputSchema>>;
119
- };
182
+ hooks?: OperationHooks<
183
+ DefaultInput,
184
+ InferOutput<TOutputSchema>,
185
+ TInitData,
186
+ TPathParams,
187
+ TQueryParams,
188
+ TBody,
189
+ QueryResultOf<TQuery>
190
+ >;
120
191
  /** REST exposure configuration */
121
- rest?: OperationRestConfig;
192
+ rest?: RestConfigInput<TPathParams, TQueryParams, TBody>;
122
193
  /** GraphQL exposure configuration (enabled by default) */
123
194
  graphql?: OperationGraphQLConfig;
124
195
  /** Cache configuration */
@@ -130,6 +201,9 @@ export type OperationFn = {
130
201
  TInputSchema extends z.ZodObject<any>,
131
202
  TOutputSchema extends z.ZodType<any> | undefined = undefined,
132
203
  TInitData = unknown,
204
+ TPathParams extends z.ZodType<any> | undefined = undefined,
205
+ TQueryParams extends z.ZodType<any> | undefined = undefined,
206
+ TBody extends z.ZodType<any> | undefined = undefined,
133
207
  >(config: {
134
208
  /** Query is not allowed with handler */
135
209
  query?: never;
@@ -142,18 +216,16 @@ export type OperationFn = {
142
216
  /** Output schema (Zod) */
143
217
  output?: TOutputSchema;
144
218
  /** Hooks for initialization and request transformation */
145
- hooks?: {
146
- init?: (options: OperationOptions) => TInitData | Promise<TInitData>;
147
- beforeRequest?: (
148
- context: { input: z.infer<TInputSchema> },
149
- initData: TInitData | undefined,
150
- ) => Record<string, unknown> | Promise<Record<string, unknown>>;
151
- afterRequest?: (context: {
152
- output: InferOutput<TOutputSchema>;
153
- }) => InferOutput<TOutputSchema> | Promise<InferOutput<TOutputSchema>>;
154
- };
219
+ hooks?: OperationHooks<
220
+ z.infer<TInputSchema>,
221
+ InferOutput<TOutputSchema>,
222
+ TInitData,
223
+ TPathParams,
224
+ TQueryParams,
225
+ TBody
226
+ >;
155
227
  /** REST exposure configuration */
156
- rest?: OperationRestConfig;
228
+ rest?: RestConfigInput<TPathParams, TQueryParams, TBody>;
157
229
  /** GraphQL exposure configuration (enabled by default) */
158
230
  graphql?: OperationGraphQLConfig;
159
231
  /** Cache configuration */
@@ -161,7 +233,13 @@ export type OperationFn = {
161
233
  }): TypedOperation<z.infer<TInputSchema>, InferOutput<TOutputSchema>, TInitData, unknown>;
162
234
 
163
235
  // Handler operation overload - without input schema
164
- <TOutputSchema extends z.ZodType<any> | undefined = undefined, TInitData = unknown>(config: {
236
+ <
237
+ TOutputSchema extends z.ZodType<any> | undefined = undefined,
238
+ TInitData = unknown,
239
+ TPathParams extends z.ZodType<any> | undefined = undefined,
240
+ TQueryParams extends z.ZodType<any> | undefined = undefined,
241
+ TBody extends z.ZodType<any> | undefined = undefined,
242
+ >(config: {
165
243
  /** Query is not allowed with handler */
166
244
  query?: never;
167
245
  /** Custom handler function */
@@ -173,18 +251,16 @@ export type OperationFn = {
173
251
  /** Output schema (Zod) */
174
252
  output?: TOutputSchema;
175
253
  /** Hooks for initialization and request transformation */
176
- hooks?: {
177
- init?: (options: OperationOptions) => TInitData | Promise<TInitData>;
178
- beforeRequest?: (
179
- context: { input: DefaultInput },
180
- initData: TInitData | undefined,
181
- ) => Record<string, unknown> | Promise<Record<string, unknown>>;
182
- afterRequest?: (context: {
183
- output: InferOutput<TOutputSchema>;
184
- }) => InferOutput<TOutputSchema> | Promise<InferOutput<TOutputSchema>>;
185
- };
254
+ hooks?: OperationHooks<
255
+ DefaultInput,
256
+ InferOutput<TOutputSchema>,
257
+ TInitData,
258
+ TPathParams,
259
+ TQueryParams,
260
+ TBody
261
+ >;
186
262
  /** REST exposure configuration */
187
- rest?: OperationRestConfig;
263
+ rest?: RestConfigInput<TPathParams, TQueryParams, TBody>;
188
264
  /** GraphQL exposure configuration (enabled by default) */
189
265
  graphql?: OperationGraphQLConfig;
190
266
  /** Cache configuration */
@@ -203,6 +279,9 @@ export type OperationFn = {
203
279
  TInputSchema extends z.ZodObject<any>,
204
280
  TOutputSchema extends z.ZodType<any> | undefined = undefined,
205
281
  TInitData = unknown,
282
+ TPathParams extends z.ZodType<any> | undefined = undefined,
283
+ TQueryParams extends z.ZodType<any> | undefined = undefined,
284
+ TBody extends z.ZodType<any> | undefined = undefined,
206
285
  >(config: {
207
286
  /** Query is not allowed with handler */
208
287
  query?: never;
@@ -215,18 +294,16 @@ export type OperationFn = {
215
294
  /** Output schema (Zod) */
216
295
  output?: TOutputSchema;
217
296
  /** Hooks for initialization and request transformation */
218
- hooks?: {
219
- init?: (options: OperationOptions) => TInitData | Promise<TInitData>;
220
- beforeRequest?: (
221
- context: { input: z.infer<TInputSchema> },
222
- initData: TInitData | undefined,
223
- ) => Record<string, unknown> | Promise<Record<string, unknown>>;
224
- afterRequest?: (context: {
225
- output: InferOutput<TOutputSchema>;
226
- }) => InferOutput<TOutputSchema> | Promise<InferOutput<TOutputSchema>>;
227
- };
297
+ hooks?: OperationHooks<
298
+ z.infer<TInputSchema>,
299
+ InferOutput<TOutputSchema>,
300
+ TInitData,
301
+ TPathParams,
302
+ TQueryParams,
303
+ TBody
304
+ >;
228
305
  /** REST exposure configuration */
229
- rest?: OperationRestConfig;
306
+ rest?: RestConfigInput<TPathParams, TQueryParams, TBody>;
230
307
  /** GraphQL exposure configuration (enabled by default) */
231
308
  graphql?: OperationGraphQLConfig;
232
309
  /** Cache configuration */
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  * Model Context Protocol (MCP) server configuration, nested under `ai.mcp`.
5
5
  * When enabled, exposes a `/mcp` endpoint for LLM agents.
6
6
  */
7
- export const MCPZod = z.object({
7
+ export const MCPZod = z.strictObject({
8
8
  /** Whether the MCP server is enabled. Off by default. */
9
9
  enabled: z.boolean().default(false),
10
10
  });
@@ -20,7 +20,7 @@ export type MCPConfig = z.input<typeof MCPZod>;
20
20
  * (`LLM_PROVIDER`, `LLM_MODEL`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`,
21
21
  * `DEEPSEEK_API_KEY`, `OLLAMA_HOST`), not from this config.
22
22
  */
23
- export const AIZod = z.object({
23
+ export const AIZod = z.strictObject({
24
24
  /** Whether the AI agent is enabled. Off by default. */
25
25
  enabled: z.boolean().default(false),
26
26
  /** Overrides the built-in system prompt sent to the LLM. */