adorn-api 1.0.39 → 1.0.41

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.
@@ -16,7 +16,13 @@ function attachOpenApi(app, controllers, options) {
16
16
  controllers
17
17
  });
18
18
  app.get(openApiPath, (_req, res) => {
19
- res.json(document);
19
+ if (options.prettyPrint) {
20
+ res.setHeader("Content-Type", "application/json");
21
+ res.send(JSON.stringify(document, null, 2));
22
+ }
23
+ else {
24
+ res.json(document);
25
+ }
20
26
  });
21
27
  if (!options.docs) {
22
28
  return;
@@ -62,6 +62,9 @@ function serializeWithSchema(value, schema) {
62
62
  }
63
63
  }
64
64
  function serializeString(value, format) {
65
+ if (format === "byte" && Buffer.isBuffer(value)) {
66
+ return value.toString("base64");
67
+ }
65
68
  if (!(value instanceof Date)) {
66
69
  return value;
67
70
  }
@@ -94,6 +94,8 @@ export interface OpenApiExpressOptions {
94
94
  servers?: OpenApiServer[];
95
95
  /** Path for OpenAPI JSON endpoint */
96
96
  path?: string;
97
+ /** Whether to pretty-print the JSON output (defaults to false for minified output) */
98
+ prettyPrint?: boolean;
97
99
  /** Documentation UI configuration */
98
100
  docs?: boolean | OpenApiDocsOptions;
99
101
  }
@@ -8,6 +8,11 @@ exports.getAllDtos = getAllDtos;
8
8
  exports.registerController = registerController;
9
9
  exports.getControllerMeta = getControllerMeta;
10
10
  exports.getAllControllers = getAllControllers;
11
+ // Ensure standard decorator metadata is available for Stage 3 decorators.
12
+ const symbolMetadata = Symbol.metadata;
13
+ if (!symbolMetadata) {
14
+ Symbol.metadata = Symbol("Symbol.metadata");
15
+ }
11
16
  const dtoStore = new Map();
12
17
  const controllerStore = new Map();
13
18
  exports.META_KEY = Symbol.for("adorn.metadata");
@@ -210,6 +210,14 @@ export declare const t: {
210
210
  * @returns Date-time string schema
211
211
  */
212
212
  dateTime: (opts?: Omit<StringSchema, "kind" | "format">) => StringSchema;
213
+ /**
214
+ * Creates a bytes (base64-encoded binary) string schema.
215
+ * Maps to OpenAPI type: "string" with format: "byte".
216
+ * Buffer values are automatically base64-encoded during response serialization.
217
+ * @param opts - String schema options
218
+ * @returns Bytes string schema
219
+ */
220
+ bytes: (opts?: Omit<StringSchema, "kind" | "format">) => StringSchema;
213
221
  /**
214
222
  * Creates a number schema.
215
223
  * @param opts - Number schema options
@@ -34,6 +34,18 @@ exports.t = {
34
34
  format: "date-time",
35
35
  ...opts
36
36
  }),
37
+ /**
38
+ * Creates a bytes (base64-encoded binary) string schema.
39
+ * Maps to OpenAPI type: "string" with format: "byte".
40
+ * Buffer values are automatically base64-encoded during response serialization.
41
+ * @param opts - String schema options
42
+ * @returns Bytes string schema
43
+ */
44
+ bytes: (opts = {}) => ({
45
+ kind: "string",
46
+ format: "byte",
47
+ ...opts
48
+ }),
37
49
  /**
38
50
  * Creates a number schema.
39
51
  * @param opts - Number schema options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adorn-api",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "Decorator-first web framework with OpenAPI 3.1 schema generation.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "express": "^4.19.2",
18
- "metal-orm": "^1.0.109"
18
+ "metal-orm": "^1.0.114"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@electric-sql/pglite": "^0.3.15",
@@ -22,7 +22,12 @@ export function attachOpenApi(
22
22
  });
23
23
 
24
24
  app.get(openApiPath, (_req, res) => {
25
- res.json(document);
25
+ if (options.prettyPrint) {
26
+ res.setHeader("Content-Type", "application/json");
27
+ res.send(JSON.stringify(document, null, 2));
28
+ } else {
29
+ res.json(document);
30
+ }
26
31
  });
27
32
 
28
33
  if (!options.docs) {
@@ -65,6 +65,9 @@ function serializeWithSchema(value: unknown, schema: SchemaNode): unknown {
65
65
  }
66
66
 
67
67
  function serializeString(value: unknown, format: string | undefined): unknown {
68
+ if (format === "byte" && Buffer.isBuffer(value)) {
69
+ return value.toString("base64");
70
+ }
68
71
  if (!(value instanceof Date)) {
69
72
  return value;
70
73
  }
@@ -107,6 +107,8 @@ export interface OpenApiExpressOptions {
107
107
  servers?: OpenApiServer[];
108
108
  /** Path for OpenAPI JSON endpoint */
109
109
  path?: string;
110
+ /** Whether to pretty-print the JSON output (defaults to false for minified output) */
111
+ prettyPrint?: boolean;
110
112
  /** Documentation UI configuration */
111
113
  docs?: boolean | OpenApiDocsOptions;
112
114
  }
@@ -255,6 +255,19 @@ export const t = {
255
255
  ...opts
256
256
  }),
257
257
 
258
+ /**
259
+ * Creates a bytes (base64-encoded binary) string schema.
260
+ * Maps to OpenAPI type: "string" with format: "byte".
261
+ * Buffer values are automatically base64-encoded during response serialization.
262
+ * @param opts - String schema options
263
+ * @returns Bytes string schema
264
+ */
265
+ bytes: (opts: Omit<StringSchema, "kind" | "format"> = {}): StringSchema => ({
266
+ kind: "string",
267
+ format: "byte",
268
+ ...opts
269
+ }),
270
+
258
271
  /**
259
272
  * Creates a number schema.
260
273
  * @param opts - Number schema options
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
2
2
  import request from "supertest";
3
3
  import {
4
4
  Controller,
5
+ Get,
5
6
  Post,
6
7
  UploadedFile,
7
8
  UploadedFiles,
@@ -146,4 +147,80 @@ describe("File Upload E2E", () => {
146
147
  description: "The file to upload"
147
148
  });
148
149
  });
150
+
151
+ it("should pretty print OpenAPI JSON when enabled", async () => {
152
+ @Controller("/api")
153
+ class PrettyPrintController {
154
+ @Get("/test")
155
+ @Returns({ status: 200, description: "Success" })
156
+ async test() {
157
+ return { success: true };
158
+ }
159
+ }
160
+
161
+ const app = await createExpressApp({
162
+ controllers: [PrettyPrintController],
163
+ openApi: {
164
+ info: { title: "Test API", version: "1.0.0" },
165
+ path: "/openapi.json",
166
+ prettyPrint: true
167
+ }
168
+ });
169
+
170
+ const response = await request(app).get("/openapi.json");
171
+ expect(response.status).toBe(200);
172
+ expect(response.headers["content-type"]).toContain("application/json");
173
+ expect(response.text).toContain(' "openapi": "3.1.0"');
174
+ expect(response.text).toContain(' "info": {');
175
+ expect(response.text).toContain(' "title": "Test API"');
176
+ });
177
+
178
+ it("should return minified JSON when prettyPrint is disabled", async () => {
179
+ @Controller("/api")
180
+ class MinifiedController {
181
+ @Get("/test")
182
+ @Returns({ status: 200, description: "Success" })
183
+ async test() {
184
+ return { success: true };
185
+ }
186
+ }
187
+
188
+ const app = await createExpressApp({
189
+ controllers: [MinifiedController],
190
+ openApi: {
191
+ info: { title: "Test API", version: "1.0.0" },
192
+ path: "/openapi.json",
193
+ prettyPrint: false
194
+ }
195
+ });
196
+
197
+ const response = await request(app).get("/openapi.json");
198
+ expect(response.status).toBe(200);
199
+ expect(response.text).not.toContain(' "openapi": "3.1.0"');
200
+ expect(response.text).toContain('"openapi":"3.1.0"');
201
+ });
202
+
203
+ it("should return minified JSON by default when prettyPrint is not set", async () => {
204
+ @Controller("/api")
205
+ class DefaultController {
206
+ @Get("/test")
207
+ @Returns({ status: 200, description: "Success" })
208
+ async test() {
209
+ return { success: true };
210
+ }
211
+ }
212
+
213
+ const app = await createExpressApp({
214
+ controllers: [DefaultController],
215
+ openApi: {
216
+ info: { title: "Test API", version: "1.0.0" },
217
+ path: "/openapi.json"
218
+ }
219
+ });
220
+
221
+ const response = await request(app).get("/openapi.json");
222
+ expect(response.status).toBe(200);
223
+ expect(response.text).not.toContain(' "openapi": "3.1.0"');
224
+ expect(response.text).toContain('"openapi":"3.1.0"');
225
+ });
149
226
  });
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { serializeResponse } from "../../src/adapter/express/response-serializer";
3
+ import { t } from "../../src/core/schema";
4
+
5
+ describe("serializeResponse", () => {
6
+ describe("Buffer with format: byte", () => {
7
+ it("serializes Buffer to base64 string when format is byte", () => {
8
+ const buffer = Buffer.from("Hello, World!");
9
+ const schema = t.bytes();
10
+
11
+ const result = serializeResponse(buffer, schema);
12
+
13
+ expect(result).toBe("SGVsbG8sIFdvcmxkIQ==");
14
+ });
15
+
16
+ it("serializes Buffer in object property with format: byte", () => {
17
+ const data = { content: Buffer.from("binary data") };
18
+ const schema = t.object({ content: t.bytes() });
19
+
20
+ const result = serializeResponse(data, schema);
21
+
22
+ expect(result).toEqual({ content: "YmluYXJ5IGRhdGE=" });
23
+ });
24
+
25
+ it("serializes Buffer in array with format: byte", () => {
26
+ const data = [Buffer.from("first"), Buffer.from("second")];
27
+ const schema = t.array(t.bytes());
28
+
29
+ const result = serializeResponse(data, schema);
30
+
31
+ expect(result).toEqual(["Zmlyc3Q=", "c2Vjb25k"]);
32
+ });
33
+
34
+ it("handles empty Buffer", () => {
35
+ const buffer = Buffer.alloc(0);
36
+ const schema = t.bytes();
37
+
38
+ const result = serializeResponse(buffer, schema);
39
+
40
+ expect(result).toBe("");
41
+ });
42
+
43
+ it("handles binary data (non-UTF8)", () => {
44
+ const buffer = Buffer.from([0x00, 0xff, 0x80, 0x7f]);
45
+ const schema = t.bytes();
46
+
47
+ const result = serializeResponse(buffer, schema);
48
+
49
+ expect(result).toBe("AP+Afw==");
50
+ });
51
+
52
+ it("does not affect non-Buffer values with format: byte", () => {
53
+ const schema = t.bytes();
54
+
55
+ expect(serializeResponse("already a string", schema)).toBe("already a string");
56
+ expect(serializeResponse(123, schema)).toBe(123);
57
+ expect(serializeResponse(null, schema)).toBe(null);
58
+ });
59
+
60
+ it("does not convert Buffer to base64 when format is not byte", () => {
61
+ const buffer = Buffer.from("test");
62
+ const schema = t.string();
63
+
64
+ const result = serializeResponse(buffer, schema);
65
+
66
+ expect(result).toEqual(buffer);
67
+ });
68
+ });
69
+
70
+ describe("Date serialization (unchanged)", () => {
71
+ it("serializes Date to ISO string for date-time format", () => {
72
+ const date = new Date("2024-01-15T10:30:00.000Z");
73
+ const schema = t.dateTime();
74
+
75
+ const result = serializeResponse(date, schema);
76
+
77
+ expect(result).toBe("2024-01-15T10:30:00.000Z");
78
+ });
79
+
80
+ it("serializes Date to date string for date format", () => {
81
+ const date = new Date("2024-01-15T10:30:00.000Z");
82
+ const schema = t.string({ format: "date" });
83
+
84
+ const result = serializeResponse(date, schema);
85
+
86
+ expect(result).toBe("2024-01-15");
87
+ });
88
+
89
+ it("does not serialize invalid Date", () => {
90
+ const date = new Date("invalid");
91
+ const schema = t.dateTime();
92
+
93
+ const result = serializeResponse(date, schema);
94
+
95
+ expect(result).toBe(date);
96
+ });
97
+ });
98
+
99
+ describe("record with bytes values", () => {
100
+ it("serializes all Buffer values in a record to base64", () => {
101
+ const data = {
102
+ file1: Buffer.from("content1"),
103
+ file2: Buffer.from("content2"),
104
+ };
105
+ const schema = t.record(t.bytes());
106
+
107
+ const result = serializeResponse(data, schema);
108
+
109
+ expect(result).toEqual({
110
+ file1: "Y29udGVudDE=",
111
+ file2: "Y29udGVudDI=",
112
+ });
113
+ });
114
+ });
115
+
116
+ describe("union with bytes", () => {
117
+ it("serializes Buffer in union when format is byte", () => {
118
+ const buffer = Buffer.from("union content");
119
+ const schema = t.union([t.bytes(), t.string()]);
120
+
121
+ const result = serializeResponse(buffer, schema);
122
+
123
+ expect(result).toBe("dW5pb24gY29udGVudA==");
124
+ });
125
+ });
126
+ });
127
+
128
+ describe("t.bytes() helper", () => {
129
+ it("creates a string schema with format: byte", () => {
130
+ const schema = t.bytes();
131
+
132
+ expect(schema).toEqual({
133
+ kind: "string",
134
+ format: "byte",
135
+ });
136
+ });
137
+
138
+ it("accepts additional options", () => {
139
+ const schema = t.bytes({ description: "Binary data" });
140
+
141
+ expect(schema).toEqual({
142
+ kind: "string",
143
+ format: "byte",
144
+ description: "Binary data",
145
+ });
146
+ });
147
+ });