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.
@@ -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": "8.0.0",
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": ">=7.0.0",
31
- "@lokalise/fastify-api-contracts": ">=6.0.0",
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": "^7.0.0",
42
+ "@lokalise/api-contracts": "^8.0.0",
43
43
  "@lokalise/biome-config": "^3.1.1",
44
- "@lokalise/fastify-api-contracts": "^6.0.0",
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",