adorn-api 1.1.4 → 1.1.6
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 +152 -1
- package/dist/adapter/metal-orm/crud-controller.d.ts +16 -0
- package/dist/adapter/metal-orm/crud-controller.js +156 -0
- package/dist/adapter/metal-orm/crud-dtos.js +12 -1
- package/dist/adapter/metal-orm/index.d.ts +2 -1
- package/dist/adapter/metal-orm/index.js +3 -1
- package/dist/adapter/metal-orm/sort.js +6 -3
- package/dist/adapter/metal-orm/types.d.ts +71 -0
- package/package.json +1 -1
- package/src/adapter/metal-orm/crud-controller.ts +188 -0
- package/src/adapter/metal-orm/crud-dtos.ts +13 -1
- package/src/adapter/metal-orm/index.ts +61 -53
- package/src/adapter/metal-orm/sort.ts +6 -3
- package/src/adapter/metal-orm/types.ts +256 -147
- package/tests/unit/crud-controller-factory.test.ts +222 -0
- package/tests/unit/crud-dtos.test.ts +150 -102
- package/tests/unit/parse-sort.test.ts +111 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildOpenApi,
|
|
4
|
+
createCrudController,
|
|
5
|
+
createMetalCrudDtoClasses,
|
|
6
|
+
t,
|
|
7
|
+
type RequestContext
|
|
8
|
+
} from "../../src/index";
|
|
9
|
+
import { getControllerMeta } from "../../src/core/metadata";
|
|
10
|
+
import { Column, Entity, PrimaryKey, col } from "metal-orm";
|
|
11
|
+
|
|
12
|
+
@Entity({ tableName: "crud_factory_entities" })
|
|
13
|
+
class CrudFactoryEntity {
|
|
14
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
15
|
+
id!: number;
|
|
16
|
+
|
|
17
|
+
@Column(col.notNull(col.text()))
|
|
18
|
+
nome!: string;
|
|
19
|
+
|
|
20
|
+
@Column(col.notNull(col.boolean()))
|
|
21
|
+
ativo!: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const crudDtos = createMetalCrudDtoClasses(CrudFactoryEntity, {
|
|
25
|
+
mutationExclude: ["id"],
|
|
26
|
+
query: {
|
|
27
|
+
filters: {
|
|
28
|
+
nomeContains: {
|
|
29
|
+
schema: t.string({ minLength: 1 }),
|
|
30
|
+
field: "nome",
|
|
31
|
+
operator: "contains"
|
|
32
|
+
},
|
|
33
|
+
ativo: {
|
|
34
|
+
schema: t.boolean(),
|
|
35
|
+
field: "ativo",
|
|
36
|
+
operator: "equals"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
sortableColumns: {
|
|
40
|
+
id: "id",
|
|
41
|
+
nome: "nome"
|
|
42
|
+
},
|
|
43
|
+
options: {
|
|
44
|
+
labelField: "nome"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
errors: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
class CrudFactoryService {
|
|
51
|
+
async list(_ctx: RequestContext<unknown, InstanceType<typeof crudDtos.queryDto>>) {
|
|
52
|
+
return { items: [], total: 0, page: 1, pageSize: 25 } as InstanceType<typeof crudDtos.pagedResponseDto>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async options(_ctx: RequestContext<unknown, InstanceType<typeof crudDtos.optionsQueryDto>>) {
|
|
56
|
+
return { items: [], total: 0, page: 1, pageSize: 25 } as InstanceType<typeof crudDtos.optionsDto>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getById(
|
|
60
|
+
id: number,
|
|
61
|
+
_ctx: RequestContext<unknown, undefined, InstanceType<typeof crudDtos.params>>
|
|
62
|
+
) {
|
|
63
|
+
return { id, nome: "Teste", ativo: true } as InstanceType<typeof crudDtos.response>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async create(
|
|
67
|
+
body: InstanceType<typeof crudDtos.create>,
|
|
68
|
+
_ctx: RequestContext<InstanceType<typeof crudDtos.create>>
|
|
69
|
+
) {
|
|
70
|
+
return { id: 1, ...body } as InstanceType<typeof crudDtos.response>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async replace(
|
|
74
|
+
id: number,
|
|
75
|
+
body: InstanceType<typeof crudDtos.replace>,
|
|
76
|
+
_ctx: RequestContext<InstanceType<typeof crudDtos.replace>, undefined, InstanceType<typeof crudDtos.params>>
|
|
77
|
+
) {
|
|
78
|
+
return { id, ...body } as InstanceType<typeof crudDtos.response>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async update(
|
|
82
|
+
id: number,
|
|
83
|
+
body: InstanceType<typeof crudDtos.update>,
|
|
84
|
+
_ctx: RequestContext<InstanceType<typeof crudDtos.update>, undefined, InstanceType<typeof crudDtos.params>>
|
|
85
|
+
) {
|
|
86
|
+
return { id, ...body } as InstanceType<typeof crudDtos.response>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async delete(
|
|
90
|
+
_id: number,
|
|
91
|
+
_ctx: RequestContext<unknown, undefined, InstanceType<typeof crudDtos.params>>
|
|
92
|
+
) {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const CrudFactoryController = createCrudController({
|
|
96
|
+
path: "/crud-factory",
|
|
97
|
+
service: CrudFactoryService,
|
|
98
|
+
dtos: crudDtos,
|
|
99
|
+
entityName: "CrudFactoryEntity"
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("createCrudController", () => {
|
|
103
|
+
it("registers CRUD routes with expected schemas and statuses", () => {
|
|
104
|
+
const meta = getControllerMeta(CrudFactoryController);
|
|
105
|
+
expect(meta).toBeDefined();
|
|
106
|
+
expect(meta?.basePath).toBe("/crud-factory");
|
|
107
|
+
|
|
108
|
+
const routeKeys = (meta?.routes ?? [])
|
|
109
|
+
.map((route) => `${route.httpMethod} ${route.path}`)
|
|
110
|
+
.sort();
|
|
111
|
+
|
|
112
|
+
expect(routeKeys).toEqual([
|
|
113
|
+
"delete /:id",
|
|
114
|
+
"get /",
|
|
115
|
+
"get /:id",
|
|
116
|
+
"get /options",
|
|
117
|
+
"patch /:id",
|
|
118
|
+
"post /",
|
|
119
|
+
"put /:id"
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const byKey = new Map(
|
|
123
|
+
(meta?.routes ?? []).map((route) => [`${route.httpMethod} ${route.path}`, route] as const)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const listRoute = byKey.get("get /");
|
|
127
|
+
expect(listRoute?.query?.schema).toBe(crudDtos.queryDto);
|
|
128
|
+
expect(listRoute?.responses).toEqual(
|
|
129
|
+
expect.arrayContaining([
|
|
130
|
+
expect.objectContaining({ status: 200, schema: crudDtos.pagedResponseDto })
|
|
131
|
+
])
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const optionsRoute = byKey.get("get /options");
|
|
135
|
+
expect(optionsRoute?.query?.schema).toBe(crudDtos.optionsQueryDto);
|
|
136
|
+
expect(optionsRoute?.responses).toEqual(
|
|
137
|
+
expect.arrayContaining([
|
|
138
|
+
expect.objectContaining({ status: 200, schema: crudDtos.optionsDto })
|
|
139
|
+
])
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const createRoute = byKey.get("post /");
|
|
143
|
+
expect(createRoute?.body?.schema).toBe(crudDtos.create);
|
|
144
|
+
expect(createRoute?.responses).toEqual(
|
|
145
|
+
expect.arrayContaining([
|
|
146
|
+
expect.objectContaining({ status: 201, schema: crudDtos.response })
|
|
147
|
+
])
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
for (const key of ["get /:id", "put /:id", "patch /:id", "delete /:id"]) {
|
|
151
|
+
const route = byKey.get(key);
|
|
152
|
+
expect(route?.params?.schema).toBe(crudDtos.params);
|
|
153
|
+
expect(route?.responses).toEqual(
|
|
154
|
+
expect.arrayContaining([
|
|
155
|
+
expect.objectContaining({ status: 400, error: true }),
|
|
156
|
+
expect.objectContaining({ status: 404, error: true })
|
|
157
|
+
])
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
expect(byKey.get("put /:id")?.body?.schema).toBe(crudDtos.replace);
|
|
162
|
+
expect(byKey.get("patch /:id")?.body?.schema).toBe(crudDtos.update);
|
|
163
|
+
expect(byKey.get("delete /:id")?.responses).toEqual(
|
|
164
|
+
expect.arrayContaining([expect.objectContaining({ status: 204 })])
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("exposes matching schemas/status in OpenAPI", () => {
|
|
169
|
+
const doc = buildOpenApi({
|
|
170
|
+
info: { title: "Test API", version: "1.0.0" },
|
|
171
|
+
controllers: [CrudFactoryController]
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const listResponses = (doc.paths["/crud-factory"]?.get as Record<string, any>)?.responses;
|
|
175
|
+
const createResponses = (doc.paths["/crud-factory"]?.post as Record<string, any>)?.responses;
|
|
176
|
+
const getByIdResponses = (doc.paths["/crud-factory/{id}"]?.get as Record<string, any>)?.responses;
|
|
177
|
+
const deleteResponses = (doc.paths["/crud-factory/{id}"]?.delete as Record<string, any>)?.responses;
|
|
178
|
+
|
|
179
|
+
expect(listResponses?.["200"]).toBeDefined();
|
|
180
|
+
expect(createResponses?.["201"]).toBeDefined();
|
|
181
|
+
expect(getByIdResponses?.["200"]).toBeDefined();
|
|
182
|
+
expect(getByIdResponses?.["400"]).toBeDefined();
|
|
183
|
+
expect(getByIdResponses?.["404"]).toBeDefined();
|
|
184
|
+
expect(deleteResponses?.["204"]).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("supports disabling optional routes with flags", () => {
|
|
188
|
+
const MinimalController = createCrudController({
|
|
189
|
+
path: "/crud-factory-minimal",
|
|
190
|
+
service: new CrudFactoryService(),
|
|
191
|
+
dtos: crudDtos,
|
|
192
|
+
entityName: "CrudFactoryEntity",
|
|
193
|
+
withOptionsRoute: false,
|
|
194
|
+
withReplace: false,
|
|
195
|
+
withPatch: false,
|
|
196
|
+
withDelete: false
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const meta = getControllerMeta(MinimalController);
|
|
200
|
+
const routeKeys = (meta?.routes ?? [])
|
|
201
|
+
.map((route) => `${route.httpMethod} ${route.path}`)
|
|
202
|
+
.sort();
|
|
203
|
+
|
|
204
|
+
expect(routeKeys).toEqual([
|
|
205
|
+
"get /",
|
|
206
|
+
"get /:id",
|
|
207
|
+
"post /"
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("uses entityName in parseIdOrThrow messages", async () => {
|
|
212
|
+
const controller = new CrudFactoryController();
|
|
213
|
+
await expect(
|
|
214
|
+
controller.getById({
|
|
215
|
+
params: { id: "invalid" }
|
|
216
|
+
} as any)
|
|
217
|
+
).rejects.toMatchObject({
|
|
218
|
+
status: 400,
|
|
219
|
+
message: "Invalid CrudFactoryEntity id."
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -7,98 +7,98 @@ import type { FilterMapping } from "../../src/adapter/metal-orm/index";
|
|
|
7
7
|
import { getDtoMeta } from "../../src/core/metadata";
|
|
8
8
|
import { t } from "../../src/core/schema";
|
|
9
9
|
import { Alphanumeric, Column, Email, Entity, Length, Pattern, PrimaryKey, col } from "metal-orm";
|
|
10
|
-
|
|
11
|
-
describe("createMetalCrudDtos", () => {
|
|
12
|
-
@Entity({ tableName: "crud_dto_entities" })
|
|
13
|
-
class CrudDtoEntity {
|
|
14
|
-
@PrimaryKey(col.autoIncrement(col.int()))
|
|
15
|
-
id!: number;
|
|
16
|
-
|
|
17
|
-
@Column(col.notNull(col.text()))
|
|
18
|
-
name!: string;
|
|
19
|
-
|
|
20
|
-
@Column(col.text())
|
|
21
|
-
nickname?: string | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
it("creates CRUD DTO decorators with defaults", () => {
|
|
25
|
-
const crud = createMetalCrudDtos(CrudDtoEntity, {
|
|
26
|
-
mutationExclude: ["id"]
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
@crud.response
|
|
30
|
-
class CrudDto {}
|
|
31
|
-
|
|
32
|
-
@crud.create
|
|
33
|
-
class CreateCrudDto {}
|
|
34
|
-
|
|
35
|
-
@crud.update
|
|
36
|
-
class UpdateCrudDto {}
|
|
37
|
-
|
|
38
|
-
@crud.params
|
|
39
|
-
class CrudParamsDto {}
|
|
40
|
-
|
|
41
|
-
const responseMeta = getDtoMeta(CrudDto);
|
|
42
|
-
const createMeta = getDtoMeta(CreateCrudDto);
|
|
43
|
-
const updateMeta = getDtoMeta(UpdateCrudDto);
|
|
44
|
-
const paramsMeta = getDtoMeta(CrudParamsDto);
|
|
45
|
-
|
|
46
|
-
expect(responseMeta?.fields.id).toBeDefined();
|
|
47
|
-
expect(createMeta?.fields.id).toBeUndefined();
|
|
48
|
-
expect(updateMeta?.fields.name?.optional).toBe(true);
|
|
49
|
-
expect(Object.keys(paramsMeta?.fields ?? {})).toEqual(["id"]);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
@Entity({ tableName: "transformer_entities" })
|
|
53
|
-
class TransformerEntity {
|
|
54
|
-
@PrimaryKey(col.autoIncrement(col.int()))
|
|
55
|
-
id!: number;
|
|
56
|
-
|
|
57
|
-
@Column(col.varchar(50))
|
|
58
|
-
@Length({ min: 2, max: 10 })
|
|
59
|
-
name!: string;
|
|
60
|
-
|
|
61
|
-
@Column(col.text())
|
|
62
|
-
@Pattern({ pattern: /^[A-Z]+$/ })
|
|
63
|
-
code!: string;
|
|
64
|
-
|
|
65
|
-
@Column(col.text())
|
|
66
|
-
@Email()
|
|
67
|
-
email!: string;
|
|
68
|
-
|
|
69
|
-
@Column(col.text())
|
|
70
|
-
@Alphanumeric({ allowHyphens: true })
|
|
71
|
-
slug!: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
it("maps transformer validators into string schemas", () => {
|
|
75
|
-
const crud = createMetalCrudDtos(TransformerEntity);
|
|
76
|
-
|
|
77
|
-
@crud.create
|
|
78
|
-
class CreateTransformerDto {}
|
|
79
|
-
|
|
80
|
-
const meta = getDtoMeta(CreateTransformerDto);
|
|
81
|
-
expect((meta?.fields.email?.schema as any).format).toBe("email");
|
|
82
|
-
expect((meta?.fields.name?.schema as any).minLength).toBe(2);
|
|
83
|
-
expect((meta?.fields.name?.schema as any).maxLength).toBe(10);
|
|
84
|
-
expect((meta?.fields.code?.schema as any).pattern).toBe("^[A-Z]+$");
|
|
85
|
-
expect((meta?.fields.slug?.schema as any).pattern).toBe("^[a-zA-Z0-9-]*$");
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("createMetalCrudDtoClasses", () => {
|
|
90
|
-
@Entity({ tableName: "crud_dto_class_entities" })
|
|
91
|
-
class CrudDtoClassEntity {
|
|
92
|
-
@PrimaryKey(col.autoIncrement(col.int()))
|
|
93
|
-
id!: number;
|
|
94
|
-
|
|
95
|
-
@Column(col.notNull(col.text()))
|
|
96
|
-
name!: string;
|
|
97
|
-
|
|
98
|
-
@Column(col.text())
|
|
99
|
-
nickname?: string | null;
|
|
100
|
-
}
|
|
101
|
-
|
|
10
|
+
|
|
11
|
+
describe("createMetalCrudDtos", () => {
|
|
12
|
+
@Entity({ tableName: "crud_dto_entities" })
|
|
13
|
+
class CrudDtoEntity {
|
|
14
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
15
|
+
id!: number;
|
|
16
|
+
|
|
17
|
+
@Column(col.notNull(col.text()))
|
|
18
|
+
name!: string;
|
|
19
|
+
|
|
20
|
+
@Column(col.text())
|
|
21
|
+
nickname?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it("creates CRUD DTO decorators with defaults", () => {
|
|
25
|
+
const crud = createMetalCrudDtos(CrudDtoEntity, {
|
|
26
|
+
mutationExclude: ["id"]
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
@crud.response
|
|
30
|
+
class CrudDto {}
|
|
31
|
+
|
|
32
|
+
@crud.create
|
|
33
|
+
class CreateCrudDto {}
|
|
34
|
+
|
|
35
|
+
@crud.update
|
|
36
|
+
class UpdateCrudDto {}
|
|
37
|
+
|
|
38
|
+
@crud.params
|
|
39
|
+
class CrudParamsDto {}
|
|
40
|
+
|
|
41
|
+
const responseMeta = getDtoMeta(CrudDto);
|
|
42
|
+
const createMeta = getDtoMeta(CreateCrudDto);
|
|
43
|
+
const updateMeta = getDtoMeta(UpdateCrudDto);
|
|
44
|
+
const paramsMeta = getDtoMeta(CrudParamsDto);
|
|
45
|
+
|
|
46
|
+
expect(responseMeta?.fields.id).toBeDefined();
|
|
47
|
+
expect(createMeta?.fields.id).toBeUndefined();
|
|
48
|
+
expect(updateMeta?.fields.name?.optional).toBe(true);
|
|
49
|
+
expect(Object.keys(paramsMeta?.fields ?? {})).toEqual(["id"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
@Entity({ tableName: "transformer_entities" })
|
|
53
|
+
class TransformerEntity {
|
|
54
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
55
|
+
id!: number;
|
|
56
|
+
|
|
57
|
+
@Column(col.varchar(50))
|
|
58
|
+
@Length({ min: 2, max: 10 })
|
|
59
|
+
name!: string;
|
|
60
|
+
|
|
61
|
+
@Column(col.text())
|
|
62
|
+
@Pattern({ pattern: /^[A-Z]+$/ })
|
|
63
|
+
code!: string;
|
|
64
|
+
|
|
65
|
+
@Column(col.text())
|
|
66
|
+
@Email()
|
|
67
|
+
email!: string;
|
|
68
|
+
|
|
69
|
+
@Column(col.text())
|
|
70
|
+
@Alphanumeric({ allowHyphens: true })
|
|
71
|
+
slug!: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
it("maps transformer validators into string schemas", () => {
|
|
75
|
+
const crud = createMetalCrudDtos(TransformerEntity);
|
|
76
|
+
|
|
77
|
+
@crud.create
|
|
78
|
+
class CreateTransformerDto {}
|
|
79
|
+
|
|
80
|
+
const meta = getDtoMeta(CreateTransformerDto);
|
|
81
|
+
expect((meta?.fields.email?.schema as any).format).toBe("email");
|
|
82
|
+
expect((meta?.fields.name?.schema as any).minLength).toBe(2);
|
|
83
|
+
expect((meta?.fields.name?.schema as any).maxLength).toBe(10);
|
|
84
|
+
expect((meta?.fields.code?.schema as any).pattern).toBe("^[A-Z]+$");
|
|
85
|
+
expect((meta?.fields.slug?.schema as any).pattern).toBe("^[a-zA-Z0-9-]*$");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("createMetalCrudDtoClasses", () => {
|
|
90
|
+
@Entity({ tableName: "crud_dto_class_entities" })
|
|
91
|
+
class CrudDtoClassEntity {
|
|
92
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
93
|
+
id!: number;
|
|
94
|
+
|
|
95
|
+
@Column(col.notNull(col.text()))
|
|
96
|
+
name!: string;
|
|
97
|
+
|
|
98
|
+
@Column(col.text())
|
|
99
|
+
nickname?: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
102
|
it("builds ready-to-export DTO classes", () => {
|
|
103
103
|
const classes = createMetalCrudDtoClasses(CrudDtoClassEntity, {
|
|
104
104
|
mutationExclude: ["id"]
|
|
@@ -127,16 +127,16 @@ describe("createMetalCrudDtoClasses", () => {
|
|
|
127
127
|
});
|
|
128
128
|
expect(classes.sortableColumns).toEqual({});
|
|
129
129
|
});
|
|
130
|
-
|
|
131
|
-
it("applies custom name overrides", () => {
|
|
132
|
-
const classes = createMetalCrudDtoClasses(CrudDtoClassEntity, {
|
|
133
|
-
baseName: "Person",
|
|
134
|
-
names: {
|
|
135
|
-
response: "PersonDto",
|
|
136
|
-
params: "PersonIdDto"
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
130
|
+
|
|
131
|
+
it("applies custom name overrides", () => {
|
|
132
|
+
const classes = createMetalCrudDtoClasses(CrudDtoClassEntity, {
|
|
133
|
+
baseName: "Person",
|
|
134
|
+
names: {
|
|
135
|
+
response: "PersonDto",
|
|
136
|
+
params: "PersonIdDto"
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
140
|
expect(classes.response.name).toBe("PersonDto");
|
|
141
141
|
expect(classes.params.name).toBe("PersonIdDto");
|
|
142
142
|
expect(classes.create.name).toBe("CreatePersonDto");
|
|
@@ -197,4 +197,52 @@ describe("createMetalCrudDtoClasses", () => {
|
|
|
197
197
|
});
|
|
198
198
|
expect(typeof classes.errors).toBe("function");
|
|
199
199
|
});
|
|
200
|
+
|
|
201
|
+
it("exposes listConfig with all query defaults ready for service layer", () => {
|
|
202
|
+
const classes = createMetalCrudDtoClasses(CrudDtoClassEntity, {
|
|
203
|
+
query: {
|
|
204
|
+
filters: {
|
|
205
|
+
nameContains: {
|
|
206
|
+
schema: t.string({ minLength: 1 }),
|
|
207
|
+
field: "name",
|
|
208
|
+
operator: "contains"
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
sortableColumns: {
|
|
212
|
+
name: "name",
|
|
213
|
+
nickname: "nickname"
|
|
214
|
+
},
|
|
215
|
+
defaultSortBy: "name",
|
|
216
|
+
defaultSortDirection: "desc",
|
|
217
|
+
defaultPageSize: 10,
|
|
218
|
+
maxPageSize: 50
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(classes.listConfig).toEqual({
|
|
223
|
+
filterMappings: classes.filterMappings,
|
|
224
|
+
sortableColumns: { name: "name", nickname: "nickname" },
|
|
225
|
+
defaultSortBy: "name",
|
|
226
|
+
defaultSortDirection: "desc",
|
|
227
|
+
defaultPageSize: 10,
|
|
228
|
+
maxPageSize: 50,
|
|
229
|
+
sortByKey: "sortBy",
|
|
230
|
+
sortDirectionKey: "sortDirection"
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("listConfig uses sensible defaults when query options are minimal", () => {
|
|
235
|
+
const classes = createMetalCrudDtoClasses(CrudDtoClassEntity);
|
|
236
|
+
|
|
237
|
+
expect(classes.listConfig).toEqual({
|
|
238
|
+
filterMappings: classes.filterMappings,
|
|
239
|
+
sortableColumns: {},
|
|
240
|
+
defaultSortBy: undefined,
|
|
241
|
+
defaultSortDirection: "asc",
|
|
242
|
+
defaultPageSize: 25,
|
|
243
|
+
maxPageSize: 100,
|
|
244
|
+
sortByKey: "sortBy",
|
|
245
|
+
sortDirectionKey: "sortDirection"
|
|
246
|
+
});
|
|
247
|
+
});
|
|
200
248
|
});
|
|
@@ -45,4 +45,115 @@ describe("parseSort", () => {
|
|
|
45
45
|
field: "createdAt"
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it("parses sortOrder with uppercase DESC", () => {
|
|
50
|
+
const result = parseSort({
|
|
51
|
+
query: { sortBy: "name", sortOrder: "DESC" },
|
|
52
|
+
sortableColumns: { name: "name" }
|
|
53
|
+
});
|
|
54
|
+
expect(result).toEqual({
|
|
55
|
+
sortBy: "name",
|
|
56
|
+
sortDirection: "desc",
|
|
57
|
+
field: "name"
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("parses sortOrder with uppercase ASC", () => {
|
|
62
|
+
const result = parseSort({
|
|
63
|
+
query: { sortBy: "name", sortOrder: "ASC" },
|
|
64
|
+
sortableColumns: { name: "name" }
|
|
65
|
+
});
|
|
66
|
+
expect(result).toEqual({
|
|
67
|
+
sortBy: "name",
|
|
68
|
+
sortDirection: "asc",
|
|
69
|
+
field: "name"
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("gives sortDirection precedence over sortOrder when both present", () => {
|
|
74
|
+
const result = parseSort({
|
|
75
|
+
query: { sortBy: "name", sortDirection: "asc", sortOrder: "DESC" },
|
|
76
|
+
sortableColumns: { name: "name" }
|
|
77
|
+
});
|
|
78
|
+
expect(result).toEqual({
|
|
79
|
+
sortBy: "name",
|
|
80
|
+
sortDirection: "asc",
|
|
81
|
+
field: "name"
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("falls back to sortOrder when sortDirection is absent", () => {
|
|
86
|
+
const result = parseSort({
|
|
87
|
+
query: { sortBy: "name", sortOrder: "desc" },
|
|
88
|
+
sortableColumns: { name: "name" }
|
|
89
|
+
});
|
|
90
|
+
expect(result).toEqual({
|
|
91
|
+
sortBy: "name",
|
|
92
|
+
sortDirection: "desc",
|
|
93
|
+
field: "name"
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("falls back to default when neither sortDirection nor sortOrder present", () => {
|
|
98
|
+
const result = parseSort({
|
|
99
|
+
query: { sortBy: "name" },
|
|
100
|
+
sortableColumns: { name: "name" },
|
|
101
|
+
defaultSortDirection: "desc"
|
|
102
|
+
});
|
|
103
|
+
expect(result).toEqual({
|
|
104
|
+
sortBy: "name",
|
|
105
|
+
sortDirection: "desc",
|
|
106
|
+
field: "name"
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("supports custom sortOrderKey", () => {
|
|
111
|
+
const result = parseSort({
|
|
112
|
+
query: { sortBy: "name", order: "DESC" },
|
|
113
|
+
sortableColumns: { name: "name" },
|
|
114
|
+
sortOrderKey: "order"
|
|
115
|
+
});
|
|
116
|
+
expect(result).toEqual({
|
|
117
|
+
sortBy: "name",
|
|
118
|
+
sortDirection: "desc",
|
|
119
|
+
field: "name"
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("ignores invalid sortOrder values and uses default", () => {
|
|
124
|
+
const result = parseSort({
|
|
125
|
+
query: { sortBy: "name", sortOrder: "INVALID" },
|
|
126
|
+
sortableColumns: { name: "name" },
|
|
127
|
+
defaultSortDirection: "asc"
|
|
128
|
+
});
|
|
129
|
+
expect(result).toEqual({
|
|
130
|
+
sortBy: "name",
|
|
131
|
+
sortDirection: "asc",
|
|
132
|
+
field: "name"
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("normalizes mixed-case sortDirection (e.g. Desc)", () => {
|
|
137
|
+
const result = parseSort({
|
|
138
|
+
query: { sortBy: "name", sortDirection: "Desc" },
|
|
139
|
+
sortableColumns: { name: "name" }
|
|
140
|
+
});
|
|
141
|
+
expect(result).toEqual({
|
|
142
|
+
sortBy: "name",
|
|
143
|
+
sortDirection: "desc",
|
|
144
|
+
field: "name"
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("works with positional args and sortOrder fallback", () => {
|
|
149
|
+
const result = parseSort(
|
|
150
|
+
{ sortBy: "name", sortOrder: "DESC" },
|
|
151
|
+
{ name: "name" }
|
|
152
|
+
);
|
|
153
|
+
expect(result).toEqual({
|
|
154
|
+
sortBy: "name",
|
|
155
|
+
sortDirection: "desc",
|
|
156
|
+
field: "name"
|
|
157
|
+
});
|
|
158
|
+
});
|
|
48
159
|
});
|