@geekmidas/constructs 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/endpoints/AmazonApiGatewayEndpointAdaptor.ts +62 -13
- package/src/endpoints/Endpoint.ts +243 -19
- package/src/endpoints/HonoEndpointAdaptor.ts +52 -16
- package/src/endpoints/TestEndpointAdaptor.ts +45 -12
- package/src/endpoints/__tests__/AmazonApiGatewayV1EndpointAdaptor.spec.ts +240 -0
- package/src/endpoints/__tests__/AmazonApiGatewayV2EndpointAdaptor.spec.ts +177 -200
- package/src/endpoints/__tests__/Endpoint.cookies.spec.ts +120 -0
- package/src/endpoints/__tests__/Endpoint.spec.ts +12 -0
- package/src/endpoints/__tests__/ResponseBuilder.spec.ts +235 -0
- package/src/endpoints/__tests__/TestEndpointAdaptor.spec.ts +348 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/constructs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
"@geekmidas/schema": "0.0.1",
|
|
70
70
|
"@geekmidas/services": "0.0.1",
|
|
71
71
|
"@geekmidas/logger": "0.0.1",
|
|
72
|
-
"@geekmidas/errors": "0.0.1",
|
|
73
|
-
"@geekmidas/rate-limit": "0.1.0",
|
|
74
72
|
"@geekmidas/events": "0.0.1",
|
|
75
|
-
"@geekmidas/
|
|
73
|
+
"@geekmidas/errors": "0.0.1",
|
|
74
|
+
"@geekmidas/cache": "0.0.7",
|
|
75
|
+
"@geekmidas/rate-limit": "0.1.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@types/lodash.pick": "~4.4.9",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from '@geekmidas/logger';
|
|
2
2
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
3
|
import type { HttpMethod } from '../types';
|
|
4
|
-
import { Endpoint, type EndpointSchemas } from './Endpoint';
|
|
4
|
+
import { Endpoint, type EndpointSchemas, ResponseBuilder } from './Endpoint';
|
|
5
5
|
|
|
6
6
|
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
7
7
|
import middy, { type MiddlewareObj } from '@middy/core';
|
|
@@ -88,6 +88,7 @@ export abstract class AmazonApiGatewayEndpoint<
|
|
|
88
88
|
const { body, query, params } = this.getInput(req.event);
|
|
89
89
|
const headers = req.event.headers as Record<string, string>;
|
|
90
90
|
const header = Endpoint.createHeaders(headers);
|
|
91
|
+
const cookie = Endpoint.createCookies(headers.cookie);
|
|
91
92
|
|
|
92
93
|
set(req.event, 'body', await this.endpoint.parseInput(body, 'body'));
|
|
93
94
|
|
|
@@ -102,6 +103,7 @@ export abstract class AmazonApiGatewayEndpoint<
|
|
|
102
103
|
await this.endpoint.parseInput(params, 'params'),
|
|
103
104
|
);
|
|
104
105
|
set(req.event, 'header', header);
|
|
106
|
+
set(req.event, 'cookie', cookie);
|
|
105
107
|
} catch (error) {
|
|
106
108
|
// Convert validation errors to 422 Unprocessable Entity
|
|
107
109
|
if (error && typeof error === 'object' && Array.isArray(error)) {
|
|
@@ -151,10 +153,12 @@ export abstract class AmazonApiGatewayEndpoint<
|
|
|
151
153
|
const logger = req.event.logger as TLogger;
|
|
152
154
|
const services = req.event.services;
|
|
153
155
|
const header = req.event.header;
|
|
156
|
+
const cookie = req.event.cookie;
|
|
154
157
|
const session = req.event.session as TSession;
|
|
155
158
|
|
|
156
159
|
const isAuthorized = await this.endpoint.authorize({
|
|
157
160
|
header,
|
|
161
|
+
cookie,
|
|
158
162
|
services,
|
|
159
163
|
logger,
|
|
160
164
|
session,
|
|
@@ -180,6 +184,7 @@ export abstract class AmazonApiGatewayEndpoint<
|
|
|
180
184
|
logger,
|
|
181
185
|
services,
|
|
182
186
|
header: req.event.header,
|
|
187
|
+
cookie: req.event.cookie,
|
|
183
188
|
})) as TSession;
|
|
184
189
|
},
|
|
185
190
|
};
|
|
@@ -216,25 +221,66 @@ export abstract class AmazonApiGatewayEndpoint<
|
|
|
216
221
|
) {
|
|
217
222
|
const input = this.endpoint.refineInput(event);
|
|
218
223
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
const responseBuilder = new ResponseBuilder();
|
|
225
|
+
const response = await this.endpoint.handler(
|
|
226
|
+
{
|
|
227
|
+
header: event.header,
|
|
228
|
+
cookie: event.cookie,
|
|
229
|
+
logger: event.logger,
|
|
230
|
+
services: event.services,
|
|
231
|
+
session: event.session,
|
|
232
|
+
...input,
|
|
233
|
+
},
|
|
234
|
+
responseBuilder,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Check if response has metadata
|
|
238
|
+
let data = response;
|
|
239
|
+
let metadata = responseBuilder.getMetadata();
|
|
240
|
+
|
|
241
|
+
if (Endpoint.hasMetadata(response)) {
|
|
242
|
+
data = response.data;
|
|
243
|
+
metadata = response.metadata;
|
|
244
|
+
}
|
|
226
245
|
|
|
227
|
-
const output =
|
|
246
|
+
const output = this.endpoint.outputSchema
|
|
247
|
+
? await this.endpoint.parseOutput(data)
|
|
248
|
+
: undefined;
|
|
228
249
|
|
|
229
|
-
const body = output ? JSON.stringify(output) : undefined;
|
|
250
|
+
const body = output !== undefined ? JSON.stringify(output) : undefined;
|
|
230
251
|
|
|
231
252
|
// Store response for middleware access
|
|
232
|
-
(event as any).__response =
|
|
253
|
+
(event as any).__response = output;
|
|
233
254
|
|
|
234
|
-
|
|
235
|
-
|
|
255
|
+
// Build response with metadata
|
|
256
|
+
const lambdaResponse: AmazonApiGatewayEndpointHandlerResponse = {
|
|
257
|
+
statusCode: metadata.status ?? this.endpoint.status,
|
|
236
258
|
body,
|
|
237
259
|
};
|
|
260
|
+
|
|
261
|
+
// Add custom headers
|
|
262
|
+
if (metadata.headers && Object.keys(metadata.headers).length > 0) {
|
|
263
|
+
lambdaResponse.headers = { ...metadata.headers };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Format cookies as Set-Cookie headers
|
|
267
|
+
if (metadata.cookies && metadata.cookies.size > 0) {
|
|
268
|
+
const setCookieHeaders: string[] = [];
|
|
269
|
+
for (const [name, { value, options }] of metadata.cookies) {
|
|
270
|
+
setCookieHeaders.push(
|
|
271
|
+
Endpoint.formatCookieHeader(name, value, options),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (setCookieHeaders.length > 0) {
|
|
276
|
+
lambdaResponse.multiValueHeaders = {
|
|
277
|
+
...lambdaResponse.multiValueHeaders,
|
|
278
|
+
'Set-Cookie': setCookieHeaders,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return lambdaResponse;
|
|
238
284
|
}
|
|
239
285
|
|
|
240
286
|
get handler() {
|
|
@@ -260,6 +306,7 @@ export type Event<
|
|
|
260
306
|
services: ServiceRecord<TServices>;
|
|
261
307
|
logger: TLogger;
|
|
262
308
|
header(key: string): string | undefined;
|
|
309
|
+
cookie(name: string): string | undefined;
|
|
263
310
|
session: TSession;
|
|
264
311
|
} & TEvent &
|
|
265
312
|
InferComposableStandardSchema<TInput>;
|
|
@@ -275,6 +322,8 @@ type Middleware<
|
|
|
275
322
|
export type AmazonApiGatewayEndpointHandlerResponse = {
|
|
276
323
|
statusCode: number;
|
|
277
324
|
body: string | undefined;
|
|
325
|
+
headers?: Record<string, string>;
|
|
326
|
+
multiValueHeaders?: Record<string, string[]>;
|
|
278
327
|
};
|
|
279
328
|
|
|
280
329
|
export type LoggerContext = {
|
|
@@ -6,11 +6,7 @@ import type { OpenAPIV3_1 } from 'openapi-types';
|
|
|
6
6
|
|
|
7
7
|
import type { Service, ServiceRecord } from '@geekmidas/services';
|
|
8
8
|
import { ConstructType } from '../Construct';
|
|
9
|
-
import {
|
|
10
|
-
Function,
|
|
11
|
-
type FunctionContext,
|
|
12
|
-
type FunctionHandler,
|
|
13
|
-
} from '../functions';
|
|
9
|
+
import { Function, type FunctionHandler } from '../functions';
|
|
14
10
|
|
|
15
11
|
import type {
|
|
16
12
|
EventPublisher,
|
|
@@ -88,6 +84,8 @@ export class Endpoint<
|
|
|
88
84
|
tags?: string[];
|
|
89
85
|
/** The HTTP success status code to return (default: 200) */
|
|
90
86
|
public readonly status: SuccessStatus;
|
|
87
|
+
/** Default headers to apply to all responses */
|
|
88
|
+
public readonly defaultHeaders: Record<string, string> = {};
|
|
91
89
|
/** Function to extract session data from the request context */
|
|
92
90
|
public getSession: SessionFn<TServices, TLogger, TSession> = () =>
|
|
93
91
|
({}) as TSession;
|
|
@@ -95,6 +93,14 @@ export class Endpoint<
|
|
|
95
93
|
public authorize: AuthorizeFn<TServices, TLogger, TSession> = () => true;
|
|
96
94
|
/** Optional rate limiting configuration */
|
|
97
95
|
public rateLimit?: RateLimitConfig;
|
|
96
|
+
/** The endpoint handler function */
|
|
97
|
+
private endpointFn!: EndpointHandler<
|
|
98
|
+
TInput,
|
|
99
|
+
TServices,
|
|
100
|
+
TLogger,
|
|
101
|
+
OutSchema,
|
|
102
|
+
TSession
|
|
103
|
+
>;
|
|
98
104
|
|
|
99
105
|
/**
|
|
100
106
|
* Builds a complete OpenAPI 3.1 schema from an array of endpoints.
|
|
@@ -188,6 +194,81 @@ export class Endpoint<
|
|
|
188
194
|
};
|
|
189
195
|
}
|
|
190
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Parses cookie string and creates a cookie lookup function.
|
|
199
|
+
*
|
|
200
|
+
* @param cookieHeader - The Cookie header value
|
|
201
|
+
* @returns Function to retrieve cookie values by name
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* const cookieFn = Endpoint.createCookies('session=abc123; theme=dark');
|
|
206
|
+
* cookieFn('session'); // Returns 'abc123'
|
|
207
|
+
* cookieFn('theme'); // Returns 'dark'
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
static createCookies(cookieHeader: string | undefined): CookieFn {
|
|
211
|
+
const cookieMap = new Map<string, string>();
|
|
212
|
+
|
|
213
|
+
if (cookieHeader) {
|
|
214
|
+
// Parse cookie string: "name1=value1; name2=value2"
|
|
215
|
+
const cookies = cookieHeader.split(';');
|
|
216
|
+
for (const cookie of cookies) {
|
|
217
|
+
const [name, ...valueParts] = cookie.trim().split('=');
|
|
218
|
+
if (name) {
|
|
219
|
+
const value = valueParts.join('='); // Handle values with = in them
|
|
220
|
+
cookieMap.set(name, decodeURIComponent(value));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return function get(name: string): string | undefined {
|
|
226
|
+
return cookieMap.get(name);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Formats a cookie as a Set-Cookie header string.
|
|
232
|
+
*
|
|
233
|
+
* @param name - Cookie name
|
|
234
|
+
* @param value - Cookie value
|
|
235
|
+
* @param options - Cookie options (httpOnly, secure, sameSite, etc.)
|
|
236
|
+
* @returns Formatted Set-Cookie header string
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* const header = Endpoint.formatCookieHeader('session', 'abc123', {
|
|
241
|
+
* httpOnly: true,
|
|
242
|
+
* secure: true,
|
|
243
|
+
* sameSite: 'strict',
|
|
244
|
+
* maxAge: 3600
|
|
245
|
+
* });
|
|
246
|
+
* // Returns: "session=abc123; Max-Age=3600; HttpOnly; Secure; SameSite=Strict"
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
static formatCookieHeader(
|
|
250
|
+
name: string,
|
|
251
|
+
value: string,
|
|
252
|
+
options?: CookieOptions,
|
|
253
|
+
): string {
|
|
254
|
+
let cookie = `${name}=${value}`;
|
|
255
|
+
|
|
256
|
+
if (options) {
|
|
257
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
258
|
+
if (options.path) cookie += `; Path=${options.path}`;
|
|
259
|
+
if (options.expires)
|
|
260
|
+
cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
261
|
+
if (options.maxAge !== undefined) cookie += `; Max-Age=${options.maxAge}`;
|
|
262
|
+
if (options.httpOnly) cookie += '; HttpOnly';
|
|
263
|
+
if (options.secure) cookie += '; Secure';
|
|
264
|
+
if (options.sameSite) {
|
|
265
|
+
cookie += `; SameSite=${options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)}`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return cookie;
|
|
270
|
+
}
|
|
271
|
+
|
|
191
272
|
/**
|
|
192
273
|
* Extracts and refines input data from the endpoint context.
|
|
193
274
|
*
|
|
@@ -207,18 +288,36 @@ export class Endpoint<
|
|
|
207
288
|
return input;
|
|
208
289
|
}
|
|
209
290
|
|
|
210
|
-
handler
|
|
291
|
+
handler = (
|
|
211
292
|
ctx: EndpointContext<TInput, TServices, TLogger, TSession>,
|
|
293
|
+
response: ResponseBuilder,
|
|
212
294
|
): OutSchema extends StandardSchemaV1
|
|
213
|
-
?
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
295
|
+
?
|
|
296
|
+
| InferStandardSchema<OutSchema>
|
|
297
|
+
| ResponseWithMetadata<InferStandardSchema<OutSchema>>
|
|
298
|
+
| Promise<InferStandardSchema<OutSchema>>
|
|
299
|
+
| Promise<ResponseWithMetadata<InferStandardSchema<OutSchema>>>
|
|
300
|
+
:
|
|
301
|
+
| any
|
|
302
|
+
| ResponseWithMetadata<any>
|
|
303
|
+
| Promise<any>
|
|
304
|
+
| Promise<ResponseWithMetadata<any>> => {
|
|
305
|
+
// Apply default headers to response builder
|
|
306
|
+
for (const [key, value] of Object.entries(this.defaultHeaders)) {
|
|
307
|
+
response.header(key, value);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return this.endpointFn(
|
|
311
|
+
{
|
|
312
|
+
...this.refineInput(ctx),
|
|
313
|
+
services: ctx.services,
|
|
314
|
+
logger: ctx.logger,
|
|
315
|
+
header: ctx.header,
|
|
316
|
+
cookie: ctx.cookie,
|
|
317
|
+
session: ctx.session,
|
|
318
|
+
} as EndpointContext<TInput, TServices, TLogger, TSession>,
|
|
319
|
+
response,
|
|
320
|
+
);
|
|
222
321
|
};
|
|
223
322
|
|
|
224
323
|
/**
|
|
@@ -235,6 +334,20 @@ export class Endpoint<
|
|
|
235
334
|
);
|
|
236
335
|
}
|
|
237
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Helper to check if response has metadata
|
|
339
|
+
*/
|
|
340
|
+
static hasMetadata<T>(
|
|
341
|
+
response: T | ResponseWithMetadata<T>,
|
|
342
|
+
): response is ResponseWithMetadata<T> {
|
|
343
|
+
return (
|
|
344
|
+
response !== null &&
|
|
345
|
+
typeof response === 'object' &&
|
|
346
|
+
'data' in response &&
|
|
347
|
+
'metadata' in response
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
238
351
|
/**
|
|
239
352
|
* Converts Express-style route params to OpenAPI format.
|
|
240
353
|
* @returns Route with ':param' converted to '{param}'
|
|
@@ -433,6 +546,8 @@ export class Endpoint<
|
|
|
433
546
|
this.description = description;
|
|
434
547
|
this.tags = tags;
|
|
435
548
|
this.status = status;
|
|
549
|
+
this.endpointFn = fn;
|
|
550
|
+
|
|
436
551
|
if (getSession) {
|
|
437
552
|
this.getSession = getSession;
|
|
438
553
|
}
|
|
@@ -553,6 +668,7 @@ export type AuthorizeContext<
|
|
|
553
668
|
services: ServiceRecord<TServices>;
|
|
554
669
|
logger: TLogger;
|
|
555
670
|
header: HeaderFn;
|
|
671
|
+
cookie: CookieFn;
|
|
556
672
|
session: TSession;
|
|
557
673
|
};
|
|
558
674
|
/**
|
|
@@ -587,6 +703,7 @@ export type SessionContext<
|
|
|
587
703
|
services: ServiceRecord<TServices>;
|
|
588
704
|
logger: TLogger;
|
|
589
705
|
header: HeaderFn;
|
|
706
|
+
cookie: CookieFn;
|
|
590
707
|
};
|
|
591
708
|
/**
|
|
592
709
|
* Function type for extracting session data from a request.
|
|
@@ -669,9 +786,96 @@ export type EndpointHeaders = Map<string, string>;
|
|
|
669
786
|
*/
|
|
670
787
|
export type HeaderFn = SingleHeaderFn;
|
|
671
788
|
|
|
789
|
+
/**
|
|
790
|
+
* Function type for retrieving cookie values.
|
|
791
|
+
*
|
|
792
|
+
* @param name - The cookie name
|
|
793
|
+
* @returns The cookie value or undefined if not found
|
|
794
|
+
*
|
|
795
|
+
* @example
|
|
796
|
+
* ```typescript
|
|
797
|
+
* const sessionId = cookie('session');
|
|
798
|
+
* ```
|
|
799
|
+
*/
|
|
800
|
+
export type CookieFn = (name: string) => string | undefined;
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Cookie options matching standard Set-Cookie attributes
|
|
804
|
+
*/
|
|
805
|
+
export interface CookieOptions {
|
|
806
|
+
domain?: string;
|
|
807
|
+
path?: string;
|
|
808
|
+
expires?: Date;
|
|
809
|
+
maxAge?: number;
|
|
810
|
+
httpOnly?: boolean;
|
|
811
|
+
secure?: boolean;
|
|
812
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Response metadata that handlers can set
|
|
817
|
+
*/
|
|
818
|
+
export interface ResponseMetadata {
|
|
819
|
+
headers?: Record<string, string>;
|
|
820
|
+
cookies?: Map<string, { value: string; options?: CookieOptions }>;
|
|
821
|
+
status?: SuccessStatus;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Return type for handlers that want to set response metadata
|
|
826
|
+
*/
|
|
827
|
+
export interface ResponseWithMetadata<T> {
|
|
828
|
+
data: T;
|
|
829
|
+
metadata: ResponseMetadata;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Response builder for fluent API in handlers
|
|
834
|
+
*/
|
|
835
|
+
export class ResponseBuilder {
|
|
836
|
+
private metadata: ResponseMetadata = {
|
|
837
|
+
headers: {},
|
|
838
|
+
cookies: new Map(),
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
header(key: string, value: string): this {
|
|
842
|
+
this.metadata.headers![key] = value;
|
|
843
|
+
return this;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
cookie(name: string, value: string, options?: CookieOptions): this {
|
|
847
|
+
this.metadata.cookies!.set(name, { value, options });
|
|
848
|
+
return this;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
deleteCookie(
|
|
852
|
+
name: string,
|
|
853
|
+
options?: Pick<CookieOptions, 'domain' | 'path'>,
|
|
854
|
+
): this {
|
|
855
|
+
this.metadata.cookies!.set(name, {
|
|
856
|
+
value: '',
|
|
857
|
+
options: { ...options, maxAge: 0, expires: new Date(0) },
|
|
858
|
+
});
|
|
859
|
+
return this;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
status(code: SuccessStatus): this {
|
|
863
|
+
this.metadata.status = code;
|
|
864
|
+
return this;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
send<T>(data: T): ResponseWithMetadata<T> {
|
|
868
|
+
return { data, metadata: this.metadata };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
getMetadata(): ResponseMetadata {
|
|
872
|
+
return this.metadata;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
672
876
|
/**
|
|
673
877
|
* The execution context provided to endpoint handlers.
|
|
674
|
-
* Contains all parsed input data, services, logger, headers, and session.
|
|
878
|
+
* Contains all parsed input data, services, logger, headers, cookies, and session.
|
|
675
879
|
*
|
|
676
880
|
* @template Input - The input schemas (body, query, params)
|
|
677
881
|
* @template TServices - Available service dependencies
|
|
@@ -690,6 +894,8 @@ export type EndpointContext<
|
|
|
690
894
|
logger: TLogger;
|
|
691
895
|
/** Function to retrieve request headers */
|
|
692
896
|
header: HeaderFn;
|
|
897
|
+
/** Function to retrieve request cookies */
|
|
898
|
+
cookie: CookieFn;
|
|
693
899
|
/** Session data extracted by getSession */
|
|
694
900
|
session: TSession;
|
|
695
901
|
} & InferComposableStandardSchema<Input>;
|
|
@@ -704,14 +910,23 @@ export type EndpointContext<
|
|
|
704
910
|
* @template TSession - Session data type
|
|
705
911
|
*
|
|
706
912
|
* @param ctx - The endpoint execution context
|
|
707
|
-
* @
|
|
913
|
+
* @param response - Response builder for setting cookies, headers, and status
|
|
914
|
+
* @returns The response data (validated if OutSchema is provided) or ResponseWithMetadata
|
|
708
915
|
*
|
|
709
916
|
* @example
|
|
710
917
|
* ```typescript
|
|
918
|
+
* // Simple response
|
|
711
919
|
* const handler: EndpointHandler<Input, [UserService], Logger, UserSchema> =
|
|
712
920
|
* async ({ params, services }) => {
|
|
713
921
|
* return await services.users.findById(params.id);
|
|
714
922
|
* };
|
|
923
|
+
*
|
|
924
|
+
* // With response builder
|
|
925
|
+
* const handler: EndpointHandler<Input, [UserService], Logger, UserSchema> =
|
|
926
|
+
* async ({ params, services }, response) => {
|
|
927
|
+
* const user = await services.users.findById(params.id);
|
|
928
|
+
* return response.header('X-User-Id', user.id).send(user);
|
|
929
|
+
* };
|
|
715
930
|
* ```
|
|
716
931
|
*/
|
|
717
932
|
export type EndpointHandler<
|
|
@@ -722,9 +937,18 @@ export type EndpointHandler<
|
|
|
722
937
|
TSession = unknown,
|
|
723
938
|
> = (
|
|
724
939
|
ctx: EndpointContext<TInput, TServices, TLogger, TSession>,
|
|
940
|
+
response: ResponseBuilder,
|
|
725
941
|
) => OutSchema extends StandardSchemaV1
|
|
726
|
-
?
|
|
727
|
-
|
|
942
|
+
?
|
|
943
|
+
| InferStandardSchema<OutSchema>
|
|
944
|
+
| ResponseWithMetadata<InferStandardSchema<OutSchema>>
|
|
945
|
+
| Promise<InferStandardSchema<OutSchema>>
|
|
946
|
+
| Promise<ResponseWithMetadata<InferStandardSchema<OutSchema>>>
|
|
947
|
+
:
|
|
948
|
+
| unknown
|
|
949
|
+
| ResponseWithMetadata<unknown>
|
|
950
|
+
| Promise<unknown>
|
|
951
|
+
| Promise<ResponseWithMetadata<unknown>>;
|
|
728
952
|
|
|
729
953
|
/**
|
|
730
954
|
* HTTP success status codes that can be returned by endpoints.
|
|
@@ -4,12 +4,14 @@ import type { Logger } from '@geekmidas/logger';
|
|
|
4
4
|
import { checkRateLimit, getRateLimitHeaders } from '@geekmidas/rate-limit';
|
|
5
5
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
6
6
|
import { type Context, Hono } from 'hono';
|
|
7
|
+
import { setCookie } from 'hono/cookie';
|
|
7
8
|
import { validator } from 'hono/validator';
|
|
8
9
|
import type { HttpMethod, LowerHttpMethod } from '../types';
|
|
9
10
|
import {
|
|
10
11
|
Endpoint,
|
|
11
12
|
type EndpointContext,
|
|
12
13
|
type EndpointSchemas,
|
|
14
|
+
ResponseBuilder,
|
|
13
15
|
} from './Endpoint';
|
|
14
16
|
import { getEndpointsFromRoutes } from './helpers';
|
|
15
17
|
import { parseHonoQuery } from './parseHonoQuery';
|
|
@@ -242,6 +244,7 @@ export class HonoEndpoint<
|
|
|
242
244
|
const headerValues = c.req.header();
|
|
243
245
|
|
|
244
246
|
const header = Endpoint.createHeaders(headerValues);
|
|
247
|
+
const cookie = Endpoint.createCookies(headerValues.cookie);
|
|
245
248
|
|
|
246
249
|
const services = await serviceDiscovery.register(endpoint.services);
|
|
247
250
|
|
|
@@ -249,10 +252,12 @@ export class HonoEndpoint<
|
|
|
249
252
|
services,
|
|
250
253
|
logger,
|
|
251
254
|
header,
|
|
255
|
+
cookie,
|
|
252
256
|
});
|
|
253
257
|
|
|
254
258
|
const isAuthorized = await endpoint.authorize({
|
|
255
259
|
header,
|
|
260
|
+
cookie,
|
|
256
261
|
services,
|
|
257
262
|
logger,
|
|
258
263
|
session,
|
|
@@ -286,29 +291,60 @@ export class HonoEndpoint<
|
|
|
286
291
|
}
|
|
287
292
|
}
|
|
288
293
|
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
294
|
+
const responseBuilder = new ResponseBuilder();
|
|
295
|
+
const response = await endpoint.handler(
|
|
296
|
+
{
|
|
297
|
+
services,
|
|
298
|
+
logger,
|
|
299
|
+
body: c.req.valid('json'),
|
|
300
|
+
query: c.req.valid('query'),
|
|
301
|
+
params: c.req.valid('param'),
|
|
302
|
+
session,
|
|
303
|
+
header: Endpoint.createHeaders(headerValues),
|
|
304
|
+
cookie: Endpoint.createCookies(headerValues.cookie),
|
|
305
|
+
} as unknown as EndpointContext<
|
|
306
|
+
TInput,
|
|
307
|
+
TServices,
|
|
308
|
+
TLogger,
|
|
309
|
+
TSession
|
|
310
|
+
>,
|
|
311
|
+
responseBuilder,
|
|
312
|
+
);
|
|
303
313
|
|
|
304
314
|
// Publish events if configured
|
|
305
315
|
|
|
306
316
|
// Validate output if schema is defined
|
|
307
317
|
|
|
308
318
|
try {
|
|
309
|
-
|
|
319
|
+
// Check if response has metadata
|
|
320
|
+
let data = response;
|
|
321
|
+
let metadata = responseBuilder.getMetadata();
|
|
322
|
+
let status = endpoint.status as ContentfulStatusCode;
|
|
323
|
+
|
|
324
|
+
if (Endpoint.hasMetadata(response)) {
|
|
325
|
+
data = response.data;
|
|
326
|
+
metadata = response.metadata;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Apply response metadata
|
|
330
|
+
if (metadata.status) {
|
|
331
|
+
status = metadata.status as ContentfulStatusCode;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (metadata.headers) {
|
|
335
|
+
for (const [key, value] of Object.entries(metadata.headers)) {
|
|
336
|
+
c.header(key, value);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (metadata.cookies) {
|
|
341
|
+
for (const [name, { value, options }] of metadata.cookies) {
|
|
342
|
+
setCookie(c, name, value, options);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
310
346
|
const output = endpoint.outputSchema
|
|
311
|
-
? await endpoint.parseOutput(
|
|
347
|
+
? await endpoint.parseOutput(data)
|
|
312
348
|
: ({} as any);
|
|
313
349
|
// @ts-ignore
|
|
314
350
|
c.set('__response', output);
|