adorn-api 1.0.40 → 1.0.42

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/README.md CHANGED
@@ -9,7 +9,7 @@ A modern, decorator-first web framework built on Express with built-in OpenAPI 3
9
9
  - 🔌 **Express Integration**: Built on top of Express for familiarity and extensibility
10
10
  - 🎯 **Type-Safe Data Transfer Objects**: Define schemas with TypeScript for compile-time checks
11
11
  - 🔄 **DTO Composition**: Reuse and compose DTOs with PickDto, OmitDto, PartialDto, and MergeDto
12
- - 📦 **Metal ORM Integration**: First-class support for Metal ORM with auto-generated CRUD DTOs, including transformer-aware schema generation
12
+ - 📦 **Metal ORM Integration**: First-class support for Metal ORM with auto-generated CRUD DTOs, transformer-aware schema generation, and tree DTOs for nested set (MPTT) models
13
13
  - 🚀 **Streaming Support**: Server-Sent Events (SSE) and streaming responses
14
14
  - 📝 **Request Validation**: Automatic validation of request bodies, params, query, and headers
15
15
  - 🔧 **Transformers**: Custom field transformations with @Transform decorator and built-in transform functions
@@ -18,15 +18,15 @@ A modern, decorator-first web framework built on Express with built-in OpenAPI 3
18
18
  - 🌐 **CORS Support**: Built-in CORS configuration
19
19
  - 🏗️ **Lifecycle Hooks**: Application bootstrap and shutdown lifecycle events
20
20
 
21
- ## Installation
22
-
23
- ```bash
24
- npm install adorn-api
25
- ```
26
-
27
- Note: Adorn uses Stage 3 decorator metadata (`Symbol.metadata`). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import to keep decorator metadata consistent.
28
-
29
- ## Quick Start
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install adorn-api
25
+ ```
26
+
27
+ Note: Adorn uses Stage 3 decorator metadata (`Symbol.metadata`). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import to keep decorator metadata consistent.
28
+
29
+ ## Quick Start
30
30
 
31
31
  ### 1. Define DTOs
32
32
 
@@ -159,9 +159,9 @@ async getOne(ctx: RequestContext) {
159
159
  }
160
160
  ```
161
161
 
162
- ### DTOs (Data Transfer Objects)
163
-
164
- DTOs define the shape of data sent to and from your API. They provide validation, documentation, and type safety.
162
+ ### DTOs (Data Transfer Objects)
163
+
164
+ DTOs define the shape of data sent to and from your API. They provide validation, documentation, and type safety.
165
165
 
166
166
  ```typescript
167
167
  @Dto({ description: "User data" })
@@ -171,16 +171,16 @@ export class UserDto {
171
171
 
172
172
  @Field(t.string({ minLength: 2, maxLength: 100 }))
173
173
  name!: string;
174
- }
175
- ```
176
-
177
- ### Stage 3 Decorator Metadata
178
-
179
- Adorn relies on Stage 3 decorator metadata (`Symbol.metadata`) to connect information across decorators (DTO fields, routes, params, etc.). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import so decorators share a consistent metadata object.
180
-
181
- ### Request Context
182
-
183
- Each route handler receives a `RequestContext` object that provides access to:
174
+ }
175
+ ```
176
+
177
+ ### Stage 3 Decorator Metadata
178
+
179
+ Adorn relies on Stage 3 decorator metadata (`Symbol.metadata`) to connect information across decorators (DTO fields, routes, params, etc.). If the runtime does not provide it, Adorn polyfills `Symbol.metadata` on import so decorators share a consistent metadata object.
180
+
181
+ ### Request Context
182
+
183
+ Each route handler receives a `RequestContext` object that provides access to:
184
184
  - `ctx.body` - The request body (validated and typed)
185
185
  - `ctx.params` - Route parameters
186
186
  - `ctx.query` - Query parameters
@@ -267,10 +267,10 @@ class UploadController {
267
267
  }
268
268
  ```
269
269
 
270
- ## Metal ORM Integration
270
+ ## Metal ORM Integration
271
271
 
272
- Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
273
- Transformer decorators such as `@Email`, `@Length`, `@Pattern`, and `@Alphanumeric` are reflected in the generated DTO schemas (validation + OpenAPI).
272
+ Adorn API has first-class support for Metal ORM, providing automatic CRUD DTO generation.
273
+ Transformer decorators such as `@Email`, `@Length`, `@Pattern`, and `@Alphanumeric` are reflected in the generated DTO schemas (validation + OpenAPI).
274
274
 
275
275
  ### 1. Define Entities
276
276
 
@@ -308,7 +308,7 @@ export const {
308
308
  } = createMetalCrudDtoClasses(User);
309
309
  ```
310
310
 
311
- ### 3. Create a CRUD Controller
311
+ ### 3. Create a CRUD Controller
312
312
 
313
313
  ```typescript
314
314
  // user.controller.ts
@@ -376,8 +376,46 @@ export class UserController {
376
376
  }
377
377
 
378
378
  // Other CRUD operations...
379
- }
380
- ```
379
+ }
380
+ ```
381
+
382
+ ### Tree DTOs (Nested Set / MPTT)
383
+
384
+ Metal ORM's tree helpers map cleanly into Adorn. Use `createMetalTreeDtoClasses` to generate DTOs for tree nodes,
385
+ node results, threaded trees, and tree lists. These schemas are included in OpenAPI automatically.
386
+
387
+ ```typescript
388
+ // category.dtos.ts
389
+ import { createMetalTreeDtoClasses } from "adorn-api";
390
+ import { CategoryDto } from "./category.dtos";
391
+ import { Category } from "./category.entity";
392
+
393
+ export const {
394
+ node: CategoryNodeDto,
395
+ nodeResult: CategoryNodeResultDto,
396
+ threadedNode: CategoryThreadedNodeDto,
397
+ treeListEntry: CategoryTreeListEntryDto,
398
+ treeListSchema: CategoryTreeListSchema,
399
+ threadedTreeSchema: CategoryThreadedTreeSchema
400
+ } = createMetalTreeDtoClasses(Category, {
401
+ entityDto: CategoryDto
402
+ });
403
+ ```
404
+
405
+ ```typescript
406
+ // category.controller.ts
407
+ import { Controller, Get, Returns } from "adorn-api";
408
+ import { CategoryThreadedTreeSchema } from "./category.dtos";
409
+
410
+ @Controller("/categories")
411
+ class CategoryController {
412
+ @Get("/tree")
413
+ @Returns(CategoryThreadedTreeSchema)
414
+ async tree() {
415
+ // return threaded tree data
416
+ }
417
+ }
418
+ ```
381
419
 
382
420
  ## Configuration
383
421
 
@@ -723,14 +761,15 @@ export class UserDto {
723
761
  }
724
762
  ```
725
763
 
726
- ## Examples
764
+ ## Examples
727
765
 
728
766
  Check out the `examples/` directory for more comprehensive examples:
729
767
 
730
768
  - `basic/` - Simple API with controllers and DTOs
731
769
  - `restful/` - RESTful API with complete CRUD operations
732
- - `metal-orm-sqlite/` - Metal ORM integration with SQLite
733
- - `metal-orm-sqlite-music/` - Complex relations with Metal ORM
770
+ - `metal-orm-sqlite/` - Metal ORM integration with SQLite
771
+ - `metal-orm-tree/` - Metal ORM tree (nested set) DTO + OpenAPI integration
772
+ - `metal-orm-sqlite-music/` - Complex relations with Metal ORM
734
773
  - `streaming/` - SSE and streaming responses
735
774
  - `openapi/` - OpenAPI documentation customization
736
775
  - `validation/` - Comprehensive validation examples with various schema types
@@ -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
@@ -1,12 +1,12 @@
1
1
  import { createExpressApp } from "../../src";
2
2
  import { UserController } from "./user.controller";
3
3
 
4
- export function createApp() {
5
- return createExpressApp({
6
- controllers: [UserController],
7
- openApi: {
8
- info: {
9
- title: "Adorn API",
4
+ export async function createApp() {
5
+ return createExpressApp({
6
+ controllers: [UserController],
7
+ openApi: {
8
+ info: {
9
+ title: "Adorn API",
10
10
  version: "1.0.0"
11
11
  },
12
12
  docs: true
@@ -1,6 +1,12 @@
1
- import { createApp } from "./app";
2
- import { startExampleServer } from "../utils/start-server";
3
-
4
- const app = createApp();
5
-
6
- startExampleServer(app, { name: "Adorn API" });
1
+ import { createApp } from "./app";
2
+ import { startExampleServer } from "../utils/start-server";
3
+
4
+ async function start() {
5
+ const app = await createApp();
6
+ startExampleServer(app, { name: "Adorn API" });
7
+ }
8
+
9
+ start().catch((err) => {
10
+ console.error(err);
11
+ process.exit(1);
12
+ });
@@ -7,7 +7,7 @@ import { startExampleServer } from "../utils/start-server";
7
7
  export async function start() {
8
8
  await initializeDatabase();
9
9
 
10
- const app = createExpressApp({
10
+ const app = await createExpressApp({
11
11
  controllers: [UserController, PostController],
12
12
  openApi: {
13
13
  info: { title: "Postgres (PGlite) + MetalORM REST example", version: "1.0.0" },
@@ -7,12 +7,12 @@ import { startExampleServer } from "../utils/start-server";
7
7
  export async function start() {
8
8
  await initializeDatabase();
9
9
 
10
- const app = createExpressApp({
11
- controllers: [UserController, PostController],
12
- openApi: {
13
- info: { title: "SQLite + MetalORM REST example", version: "1.0.0" },
14
- docs: true
15
- }
10
+ const app = await createExpressApp({
11
+ controllers: [UserController, PostController],
12
+ openApi: {
13
+ info: { title: "SQLite + MetalORM REST example", version: "1.0.0" },
14
+ docs: true
15
+ }
16
16
  });
17
17
  startExampleServer(app, { name: "SQLite + MetalORM REST example" });
18
18
  }
@@ -8,12 +8,12 @@ import { startExampleServer } from "../utils/start-server";
8
8
  export async function start() {
9
9
  await initializeDatabase();
10
10
 
11
- const app = createExpressApp({
12
- controllers: [ArtistController, AlbumController, TrackController],
13
- openApi: {
14
- info: { title: "Music Library API", version: "1.0.0" },
15
- docs: true
16
- }
11
+ const app = await createExpressApp({
12
+ controllers: [ArtistController, AlbumController, TrackController],
13
+ openApi: {
14
+ info: { title: "Music Library API", version: "1.0.0" },
15
+ docs: true
16
+ }
17
17
  });
18
18
  startExampleServer(app, { name: "Music Library API" });
19
19
  }
@@ -0,0 +1,24 @@
1
+ import { createExpressApp } from "../../src";
2
+ import { initializeDatabase } from "./db";
3
+ import { CategoryController } from "./entity.controller";
4
+ import { startExampleServer } from "../utils/start-server";
5
+
6
+ export async function start() {
7
+ await initializeDatabase();
8
+
9
+ const app = await createExpressApp({
10
+ controllers: [CategoryController],
11
+ openApi: {
12
+ info: { title: "MetalORM Tree example", version: "1.0.0" },
13
+ docs: true
14
+ }
15
+ });
16
+
17
+ startExampleServer(app, {
18
+ name: "MetalORM Tree example",
19
+ extraLogs: [
20
+ (port) => `Tree endpoint: http://localhost:${port}/categories/tree`,
21
+ (port) => `List endpoint: http://localhost:${port}/categories/list`
22
+ ]
23
+ });
24
+ }
@@ -0,0 +1,92 @@
1
+ import sqlite3 from "sqlite3";
2
+ import {
3
+ Orm,
4
+ SqliteDialect,
5
+ createSqliteExecutor,
6
+ type SqliteClientLike
7
+ } from "metal-orm";
8
+
9
+ let db: sqlite3.Database | null = null;
10
+ let orm: Orm | null = null;
11
+
12
+ function execSql(database: sqlite3.Database, sql: string): Promise<void> {
13
+ return new Promise((resolve, reject) => {
14
+ database.exec(sql, (err) => {
15
+ if (err) {
16
+ reject(err);
17
+ return;
18
+ }
19
+ resolve();
20
+ });
21
+ });
22
+ }
23
+
24
+ function createSqliteClient(database: sqlite3.Database): SqliteClientLike {
25
+ return {
26
+ all(sql, params = []) {
27
+ return new Promise((resolve, reject) => {
28
+ database.all(sql, params, (err, rows) => {
29
+ if (err) {
30
+ reject(err);
31
+ return;
32
+ }
33
+ resolve(rows as Record<string, unknown>[]);
34
+ });
35
+ });
36
+ },
37
+ beginTransaction() {
38
+ return execSql(database, "BEGIN");
39
+ },
40
+ commitTransaction() {
41
+ return execSql(database, "COMMIT");
42
+ },
43
+ rollbackTransaction() {
44
+ return execSql(database, "ROLLBACK");
45
+ }
46
+ };
47
+ }
48
+
49
+ export async function initializeDatabase() {
50
+ db = new sqlite3.Database(":memory:");
51
+ await execSql(db, "pragma foreign_keys = ON");
52
+ await execSql(
53
+ db,
54
+ `create table categories (
55
+ id integer primary key autoincrement,
56
+ name text not null,
57
+ parentId integer,
58
+ lft integer not null,
59
+ rght integer not null,
60
+ depth integer,
61
+ foreign key(parentId) references categories(id)
62
+ )`
63
+ );
64
+
65
+ await execSql(
66
+ db,
67
+ `insert into categories (id, name, parentId, lft, rght, depth) values
68
+ (1, 'Electronics', null, 1, 10, 0),
69
+ (2, 'Computers', 1, 2, 5, 1),
70
+ (3, 'Laptops', 2, 3, 4, 2),
71
+ (4, 'Phones', 1, 6, 9, 1),
72
+ (5, 'Smartphones', 4, 7, 8, 2)
73
+ `
74
+ );
75
+
76
+ const executor = createSqliteExecutor(createSqliteClient(db));
77
+ orm = new Orm({
78
+ dialect: new SqliteDialect(),
79
+ executorFactory: {
80
+ createExecutor: () => executor,
81
+ createTransactionalExecutor: () => executor,
82
+ dispose: async () => {}
83
+ }
84
+ });
85
+ }
86
+
87
+ export function createSession() {
88
+ if (!orm) {
89
+ throw new Error("ORM not initialized");
90
+ }
91
+ return orm.createSession();
92
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ Controller,
3
+ Get,
4
+ HttpError,
5
+ Params,
6
+ Returns,
7
+ parseIdOrThrow,
8
+ withSession,
9
+ type RequestContext
10
+ } from "../../src";
11
+ import {
12
+ createTreeManager,
13
+ formatTreeList,
14
+ getTableDefFromEntity,
15
+ threadResults,
16
+ treeQuery
17
+ } from "metal-orm";
18
+ import { createSession } from "./db";
19
+ import { Category } from "./entity.entity";
20
+ import {
21
+ CategoryNodeResultDto,
22
+ CategoryParamsDto,
23
+ CategoryThreadedTreeSchema,
24
+ CategoryTreeListSchema
25
+ } from "./entity.dtos";
26
+
27
+ const categoryTable = getTableDefFromEntity(Category);
28
+ if (!categoryTable) {
29
+ throw new Error("Category entity metadata was not initialized.");
30
+ }
31
+
32
+ const tree = treeQuery(categoryTable, {
33
+ parentKey: "parentId",
34
+ leftKey: "lft",
35
+ rightKey: "rght",
36
+ depthKey: "depth"
37
+ });
38
+
39
+ @Controller("/categories")
40
+ export class CategoryController {
41
+ @Get("/tree")
42
+ @Returns(CategoryThreadedTreeSchema)
43
+ async tree() {
44
+ return withSession(createSession, async (session) => {
45
+ const rows = await tree.findTreeList().execute(session);
46
+ return threadResults(rows, tree.config.leftKey, tree.config.rightKey);
47
+ });
48
+ }
49
+
50
+ @Get("/list")
51
+ @Returns(CategoryTreeListSchema)
52
+ async list() {
53
+ return withSession(createSession, async (session) => {
54
+ const rows = await tree.findTreeList().execute(session);
55
+ return formatTreeList(rows, {
56
+ keyPath: "id",
57
+ valuePath: "name",
58
+ depthKey: tree.config.depthKey,
59
+ leftKey: tree.config.leftKey,
60
+ rightKey: tree.config.rightKey
61
+ });
62
+ });
63
+ }
64
+
65
+ @Get("/:id")
66
+ @Params(CategoryParamsDto)
67
+ @Returns(CategoryNodeResultDto)
68
+ async getNode(ctx: RequestContext<unknown, undefined, { id: string | number }>) {
69
+ const id = parseIdOrThrow(ctx.params.id, "Category");
70
+ return withSession(createSession, async (session) => {
71
+ const manager = createTreeManager(session, categoryTable, tree.config);
72
+ const node = await manager.getNode(id);
73
+ if (!node) {
74
+ throw new HttpError(404, "Category not found.");
75
+ }
76
+ return node;
77
+ });
78
+ }
79
+ }
@@ -0,0 +1,31 @@
1
+ import {
2
+ createMetalCrudDtoClasses,
3
+ createMetalTreeDtoClasses
4
+ } from "../../src";
5
+ import { Category } from "./entity.entity";
6
+
7
+ const categoryCrud = createMetalCrudDtoClasses(Category, {
8
+ response: {
9
+ description: "Category returned by API."
10
+ },
11
+ mutationExclude: ["id", "lft", "rght", "depth"]
12
+ });
13
+
14
+ export const {
15
+ response: CategoryDto,
16
+ create: CreateCategoryDto,
17
+ replace: ReplaceCategoryDto,
18
+ update: UpdateCategoryDto,
19
+ params: CategoryParamsDto
20
+ } = categoryCrud;
21
+
22
+ export const {
23
+ node: CategoryNodeDto,
24
+ nodeResult: CategoryNodeResultDto,
25
+ threadedNode: CategoryThreadedNodeDto,
26
+ treeListEntry: CategoryTreeListEntryDto,
27
+ treeListSchema: CategoryTreeListSchema,
28
+ threadedTreeSchema: CategoryThreadedTreeSchema
29
+ } = createMetalTreeDtoClasses(Category, {
30
+ entityDto: CategoryDto
31
+ });
@@ -0,0 +1,29 @@
1
+ import { Column, Entity, PrimaryKey, Tree, TreeChildren, TreeParent, col } from "metal-orm";
2
+
3
+ @Entity({ tableName: "categories" })
4
+ @Tree({ parentKey: "parentId", leftKey: "lft", rightKey: "rght", depthKey: "depth" })
5
+ export class Category {
6
+ @PrimaryKey(col.autoIncrement(col.int()))
7
+ id!: number;
8
+
9
+ @Column(col.notNull(col.text()))
10
+ name!: string;
11
+
12
+ @Column(col.int())
13
+ parentId?: number | null;
14
+
15
+ @Column(col.notNull(col.int()))
16
+ lft!: number;
17
+
18
+ @Column(col.notNull(col.int()))
19
+ rght!: number;
20
+
21
+ @Column(col.int())
22
+ depth?: number | null;
23
+
24
+ @TreeParent()
25
+ parent?: Category;
26
+
27
+ @TreeChildren()
28
+ children?: Category[];
29
+ }
@@ -0,0 +1,6 @@
1
+ import { start } from "./app";
2
+
3
+ start().catch((err) => {
4
+ console.error(err);
5
+ process.exit(1);
6
+ });
@@ -1,12 +1,12 @@
1
1
  import { createExpressApp } from "../../src";
2
2
  import { TaskController } from "./task.controller";
3
3
 
4
- export function createApp() {
5
- return createExpressApp({
6
- controllers: [TaskController],
7
- openApi: {
8
- info: {
9
- title: "Tasks API",
4
+ export async function createApp() {
5
+ return createExpressApp({
6
+ controllers: [TaskController],
7
+ openApi: {
8
+ info: {
9
+ title: "Tasks API",
10
10
  version: "1.0.0"
11
11
  },
12
12
  docs: true
@@ -1,9 +1,15 @@
1
- import { createApp } from "./app";
2
- import { startExampleServer } from "../utils/start-server";
3
-
4
- const app = createApp();
5
-
6
- startExampleServer(app, {
7
- name: "Tasks API",
8
- extraLogs: [(port) => `Swagger UI available at http://localhost:${port}/docs`]
9
- });
1
+ import { createApp } from "./app";
2
+ import { startExampleServer } from "../utils/start-server";
3
+
4
+ async function start() {
5
+ const app = await createApp();
6
+ startExampleServer(app, {
7
+ name: "Tasks API",
8
+ extraLogs: [(port) => `Swagger UI available at http://localhost:${port}/docs`]
9
+ });
10
+ }
11
+
12
+ start().catch((err) => {
13
+ console.error(err);
14
+ process.exit(1);
15
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adorn-api",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
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.115"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@electric-sql/pglite": "^0.3.15",
@@ -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
  }