opinionated-machine 8.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +117 -6
- package/dist/lib/DIContext.js +33 -27
- package/dist/lib/DIContext.js.map +1 -1
- package/dist/lib/dualmode/AbstractDualModeController.d.ts +1 -1
- package/dist/lib/dualmode/AbstractDualModeController.js +1 -1
- package/dist/lib/gateway/gatewayMetadata.d.ts +0 -5
- package/dist/lib/gateway/gatewayMetadata.js +0 -1
- package/dist/lib/gateway/gatewayMetadata.js.map +1 -1
- package/dist/lib/gateway/manifest/manifestSchema.d.ts +0 -10
- package/dist/lib/routes/fastifyRouteBuilder.js +102 -33
- package/dist/lib/routes/fastifyRouteBuilder.js.map +1 -1
- package/dist/lib/routes/fastifyRouteTypes.d.ts +97 -12
- package/dist/lib/routes/fastifyRouteTypes.js.map +1 -1
- package/dist/lib/routes/fastifyRouteUtils.js +15 -5
- package/dist/lib/routes/fastifyRouteUtils.js.map +1 -1
- package/dist/lib/routes/index.d.ts +2 -1
- package/dist/lib/routes/index.js +2 -0
- package/dist/lib/routes/index.js.map +1 -1
- package/dist/lib/routes/sseResponseSchema.d.ts +35 -0
- package/dist/lib/routes/sseResponseSchema.js +115 -0
- package/dist/lib/routes/sseResponseSchema.js.map +1 -0
- package/dist/lib/sse/AbstractSSEController.d.ts +1 -1
- package/dist/lib/sse/AbstractSSEController.js +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Shape of the error bodies Fastify and the SSE route builders emit themselves, rather than
|
|
4
|
+
* the handler: `FST_ERR_VALIDATION` when a request fails schema validation, whatever an
|
|
5
|
+
* application-level `setErrorHandler` returns, and the `{ statusCode, error, message }`
|
|
6
|
+
* envelope the builders send when a handler throws before streaming starts.
|
|
7
|
+
*
|
|
8
|
+
* A declared status schema is unioned with this so those bodies stay serializable. Without it
|
|
9
|
+
* Fastify serializes them against the handler's schema, fails, and turns a declared 400 or 404
|
|
10
|
+
* into a 500 `FST_ERR_FAILED_ERROR_SERIALIZATION`. Loose so a custom error handler's extra
|
|
11
|
+
* fields survive; `statusCode` and `message` are what keeps it from matching handler bodies.
|
|
12
|
+
* `error` and `code` are listed even though loose mode would carry them anyway, because
|
|
13
|
+
* Fastify defines them as non-enumerable properties that key enumeration would skip.
|
|
14
|
+
*/
|
|
15
|
+
const frameworkErrorSchema = z.looseObject({
|
|
16
|
+
statusCode: z.number(),
|
|
17
|
+
message: z.string(),
|
|
18
|
+
error: z.string().optional(),
|
|
19
|
+
code: z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Combine alternative body schemas for one status.
|
|
23
|
+
*
|
|
24
|
+
* A single schema is returned as-is rather than wrapped, so a status with only one possible
|
|
25
|
+
* body does not pick up a pointless `anyOf` in the generated spec.
|
|
26
|
+
*
|
|
27
|
+
* @returns The combined schema, or `undefined` when there is nothing to describe
|
|
28
|
+
*/
|
|
29
|
+
function unionOf(schemas) {
|
|
30
|
+
const [first, ...rest] = schemas;
|
|
31
|
+
if (!first) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return rest.length === 0 ? first : z.union(schemas);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Describe an SSE stream as the union of its event envelopes, following the OpenAPI 3.x
|
|
38
|
+
* convention for `text/event-stream`: one object schema per event type (`{ id?, event, data,
|
|
39
|
+
* retry? }`), discriminated on the `event` name so the contract's event payloads show up in
|
|
40
|
+
* the generated spec as a `oneOf` of envelopes with a `const` event name.
|
|
41
|
+
*
|
|
42
|
+
* Mirrors what `@lokalise/fastify-api-contracts` produces for an `sseBody()` response, so
|
|
43
|
+
* legacy SSE / dual-mode contracts and `ApiContract` ones document the same way.
|
|
44
|
+
*
|
|
45
|
+
* @returns The event envelope schema, or `undefined` when the contract declares no events
|
|
46
|
+
*/
|
|
47
|
+
export function buildSseEventSchema(serverSentEventSchemas) {
|
|
48
|
+
const eventSchemas = Object.entries(serverSentEventSchemas).map(([eventName, dataSchema]) => z.object({
|
|
49
|
+
id: z.string().optional(),
|
|
50
|
+
event: z.literal(eventName),
|
|
51
|
+
data: dataSchema,
|
|
52
|
+
retry: z.int().optional(),
|
|
53
|
+
}));
|
|
54
|
+
const [firstEventSchema, ...restEventSchemas] = eventSchemas;
|
|
55
|
+
if (!firstEventSchema) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return restEventSchemas.length === 0
|
|
59
|
+
? firstEventSchema
|
|
60
|
+
: z.discriminatedUnion('event', [firstEventSchema, ...restEventSchemas]);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the JSON schema for one declared status code.
|
|
64
|
+
*
|
|
65
|
+
* A status's schema has to accept every body the runtime can put out at that status,
|
|
66
|
+
* because Fastify serializes against it and rejects anything that does not match:
|
|
67
|
+
*
|
|
68
|
+
* - 2xx on a dual-mode contract: `handleSyncMode` validates the sync body against
|
|
69
|
+
* `successResponseBodySchema` while `processSSEHandlerResult` validates `sse.respond()`
|
|
70
|
+
* against the contract's schema for that status, so both shapes are possible.
|
|
71
|
+
* - Non-2xx: the handler's declared body, or a framework error envelope.
|
|
72
|
+
*/
|
|
73
|
+
function buildStatusSchema(statusCode, declaredSchema, syncSuccessSchema) {
|
|
74
|
+
const isSuccessStatus = statusCode >= 200 && statusCode < 300;
|
|
75
|
+
if (isSuccessStatus) {
|
|
76
|
+
return unionOf([syncSuccessSchema, declaredSchema].filter((schema) => schema !== undefined));
|
|
77
|
+
}
|
|
78
|
+
return declaredSchema && z.union([declaredSchema, frameworkErrorSchema]);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build the `schema.response` map for an SSE or dual-mode route from its contract.
|
|
82
|
+
*
|
|
83
|
+
* The 200 entry uses Fastify's per-media-type form so a single status can describe both the
|
|
84
|
+
* event stream and the JSON body: `text/event-stream` carries the event envelopes, and
|
|
85
|
+
* `application/json` carries the sync body (dual-mode), the body the contract declares for
|
|
86
|
+
* 200 (an SSE route that answers `sse.respond(200, ...)` before streaming starts), or both.
|
|
87
|
+
*
|
|
88
|
+
* Populating this drives both the OpenAPI spec and Fastify's serializer, so a status code the
|
|
89
|
+
* contract declares is now serialized against its schema instead of plain `JSON.stringify`.
|
|
90
|
+
*
|
|
91
|
+
* @param serverSentEventSchemas - Contract's event name to payload schema map
|
|
92
|
+
* @param responseBodySchemasByStatusCode - Contract's per-status response schemas, if any
|
|
93
|
+
* @param syncSuccessSchema - Dual-mode sync 2xx body schema; omit for SSE-only contracts
|
|
94
|
+
*/
|
|
95
|
+
export function buildSseResponseSchemas(serverSentEventSchemas, responseBodySchemasByStatusCode, syncSuccessSchema) {
|
|
96
|
+
const { 200: declaredOkSchema, ...otherStatusSchemas } = responseBodySchemasByStatusCode ?? {};
|
|
97
|
+
const jsonOkSchema = buildStatusSchema(200, declaredOkSchema, syncSuccessSchema);
|
|
98
|
+
const eventSchema = buildSseEventSchema(serverSentEventSchemas);
|
|
99
|
+
const okContent = {
|
|
100
|
+
...(eventSchema && { 'text/event-stream': { schema: eventSchema } }),
|
|
101
|
+
...(jsonOkSchema && { 'application/json': { schema: jsonOkSchema } }),
|
|
102
|
+
};
|
|
103
|
+
const responseSchemas = {};
|
|
104
|
+
if (Object.keys(okContent).length > 0) {
|
|
105
|
+
responseSchemas[200] = { content: okContent };
|
|
106
|
+
}
|
|
107
|
+
for (const [statusCode, declaredSchema] of Object.entries(otherStatusSchemas)) {
|
|
108
|
+
const schema = buildStatusSchema(Number(statusCode), declaredSchema, syncSuccessSchema);
|
|
109
|
+
if (schema) {
|
|
110
|
+
responseSchemas[statusCode] = schema;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return responseSchemas;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=sseResponseSchema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sseResponseSchema.js","sourceRoot":"","sources":["../../../lib/routes/sseResponseSchema.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,7 +23,7 @@ export type { AllContractEventNames, AllContractEvents, ExtractEventSchema, SSEC
|
|
|
23
23
|
* ```typescript
|
|
24
24
|
* class NotificationsSSEController extends AbstractSSEController<typeof contracts> {
|
|
25
25
|
* public static contracts = {
|
|
26
|
-
* notifications: buildSseContract({ ... }),
|
|
26
|
+
* notifications: buildSseContract({ visibility: 'public', ... }),
|
|
27
27
|
* } as const
|
|
28
28
|
*
|
|
29
29
|
* public buildSSERoutes() {
|
|
@@ -15,7 +15,7 @@ export { SSESessionSpy } from "./SSESessionSpy.js";
|
|
|
15
15
|
* ```typescript
|
|
16
16
|
* class NotificationsSSEController extends AbstractSSEController<typeof contracts> {
|
|
17
17
|
* public static contracts = {
|
|
18
|
-
* notifications: buildSseContract({ ... }),
|
|
18
|
+
* notifications: buildSseContract({ visibility: 'public', ... }),
|
|
19
19
|
* } as const
|
|
20
20
|
*
|
|
21
21
|
* public buildSSERoutes() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opinionated-machine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "Very opinionated DI framework for fastify, built on top of awilix ",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"ts-deepmerge": "^8.0.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@lokalise/api-contracts": ">=
|
|
31
|
-
"@lokalise/fastify-api-contracts": ">=
|
|
30
|
+
"@lokalise/api-contracts": ">=8.0.0",
|
|
31
|
+
"@lokalise/fastify-api-contracts": ">=7.0.0",
|
|
32
32
|
"@lokalise/node-core": ">=14.7.4",
|
|
33
33
|
"awilix": ">=13.0.0",
|
|
34
34
|
"awilix-manager": ">=6.0.0",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@biomejs/biome": "2.4.4",
|
|
41
41
|
"@changesets/cli": "^3.0.0",
|
|
42
|
-
"@lokalise/api-contracts": "^
|
|
42
|
+
"@lokalise/api-contracts": "^8.0.0",
|
|
43
43
|
"@lokalise/biome-config": "^3.1.1",
|
|
44
|
-
"@lokalise/fastify-api-contracts": "^
|
|
44
|
+
"@lokalise/fastify-api-contracts": "^7.0.0",
|
|
45
45
|
"@lokalise/node-core": "^14.7.4",
|
|
46
46
|
"@lokalise/tsconfig": "^3.1.0",
|
|
47
47
|
"@types/node": "^22.19.7",
|