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 +71 -32
- package/dist/adapter/express/openapi.js +7 -1
- package/dist/adapter/express/response-serializer.js +3 -0
- package/dist/adapter/express/types.d.ts +2 -0
- package/dist/core/metadata.js +5 -0
- package/dist/core/schema.d.ts +8 -0
- package/dist/core/schema.js +12 -0
- package/examples/basic/app.ts +6 -6
- package/examples/basic/index.ts +12 -6
- package/examples/metal-orm-postgres/app.ts +1 -1
- package/examples/metal-orm-sqlite/app.ts +6 -6
- package/examples/metal-orm-sqlite-music/app.ts +6 -6
- package/examples/metal-orm-tree/app.ts +24 -0
- package/examples/metal-orm-tree/db.ts +92 -0
- package/examples/metal-orm-tree/entity.controller.ts +79 -0
- package/examples/metal-orm-tree/entity.dtos.ts +31 -0
- package/examples/metal-orm-tree/entity.entity.ts +29 -0
- package/examples/metal-orm-tree/index.ts +6 -0
- package/examples/restful/app.ts +6 -6
- package/examples/restful/index.ts +15 -9
- package/package.json +2 -2
- package/src/adapter/express/response-serializer.ts +3 -0
- package/src/adapter/metal-orm/index.ts +20 -12
- package/src/adapter/metal-orm/tree-dtos.ts +260 -0
- package/src/adapter/metal-orm/types.ts +80 -14
- package/src/core/schema.ts +13 -0
- package/tests/unit/metal-orm.test.ts +67 -3
- package/tests/unit/response-serializer.test.ts +147 -0
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,
|
|
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-
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
}
|
package/dist/core/metadata.js
CHANGED
|
@@ -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");
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -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
|
package/dist/core/schema.js
CHANGED
|
@@ -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/examples/basic/app.ts
CHANGED
|
@@ -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
|
package/examples/basic/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { createApp } from "./app";
|
|
2
|
-
import { startExampleServer } from "../utils/start-server";
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
}
|
package/examples/restful/app.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
startExampleServer(app, {
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|