@drax/crud-back 2.11.0 → 3.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.
- package/dist/controllers/AbstractFastifyController.js +47 -24
- package/dist/repository/AbstractMongoRepository.js +12 -8
- package/dist/repository/AbstractSqliteRepository.js +40 -9
- package/dist/services/AbstractService.js +9 -6
- package/package.json +8 -7
- package/src/controllers/AbstractFastifyController.ts +56 -28
- package/src/repository/AbstractMongoRepository.ts +17 -8
- package/src/repository/AbstractSqliteRepository.ts +61 -10
- package/src/services/AbstractService.ts +12 -6
- package/test/_mocks/MockRepository.ts +1 -1
- package/test/controllers/PersonController.test.ts +547 -0
- package/test/people/controllers/CountryController.ts +40 -0
- package/test/people/controllers/LanguageController.ts +29 -0
- package/test/people/controllers/PersonController.ts +29 -0
- package/test/people/factory/services/CountryServiceFactory.ts +41 -0
- package/test/people/factory/services/LanguageServiceFactory.ts +41 -0
- package/test/people/factory/services/PersonServiceFactory.ts +41 -0
- package/test/people/interfaces/ICountry.ts +28 -0
- package/test/people/interfaces/ICountryRepository.ts +11 -0
- package/test/people/interfaces/ILanguage.ts +32 -0
- package/test/people/interfaces/ILanguageRepository.ts +11 -0
- package/test/people/interfaces/IPerson.ts +58 -0
- package/test/people/interfaces/IPersonRepository.ts +11 -0
- package/test/people/models/CountryModel.ts +38 -0
- package/test/people/models/LanguageModel.ts +40 -0
- package/test/people/models/PersonModel.ts +61 -0
- package/test/people/permissions/CountryPermissions.ts +14 -0
- package/test/people/permissions/LanguagePermissions.ts +14 -0
- package/test/people/permissions/PersonPermissions.ts +18 -0
- package/test/people/repository/mongo/CountryMongoRepository.ts +22 -0
- package/test/people/repository/mongo/LanguageMongoRepository.ts +22 -0
- package/test/people/repository/mongo/PersonMongoRepository.ts +22 -0
- package/test/people/repository/sqlite/CountrySqliteRepository.ts +33 -0
- package/test/people/repository/sqlite/LanguageSqliteRepository.ts +37 -0
- package/test/people/repository/sqlite/PersonSqliteRepository.ts +42 -0
- package/test/people/routes/CountryRoutes.ts +34 -0
- package/test/people/routes/LanguageRoutes.ts +34 -0
- package/test/people/routes/PersonRoutes.ts +40 -0
- package/test/people/schemas/CountrySchema.ts +22 -0
- package/test/people/schemas/LanguageSchema.ts +22 -0
- package/test/people/schemas/PersonSchema.ts +42 -0
- package/test/people/services/CountryService.ts +20 -0
- package/test/people/services/LanguageService.ts +20 -0
- package/test/people/services/PersonService.ts +20 -0
- package/test/services/AbstractService.test.ts +11 -7
- package/test/setup/MongoInMemory.ts +56 -0
- package/test/setup/TestSetup.ts +344 -0
- package/test/setup/data/admin-role.ts +13 -0
- package/test/setup/data/basic-user.ts +14 -0
- package/test/setup/data/one-tenant.ts +6 -0
- package/test/setup/data/restricted-role.ts +16 -0
- package/test/setup/data/root-user.ts +14 -0
- package/test/setup/data/tenant-one-user.ts +13 -0
- package/test/setup/data/tenant-two-user.ts +14 -0
- package/test/setup/data/two-tenant.ts +6 -0
- package/test/workers/ExportCsvWorker.test.ts +1 -1
- package/tsconfig.json +2 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/types/controllers/AbstractFastifyController.d.ts.map +1 -1
- package/types/repository/AbstractMongoRepository.d.ts +3 -3
- package/types/repository/AbstractMongoRepository.d.ts.map +1 -1
- package/types/repository/AbstractSqliteRepository.d.ts +5 -4
- package/types/repository/AbstractSqliteRepository.d.ts.map +1 -1
- package/types/services/AbstractService.d.ts +4 -2
- package/types/services/AbstractService.d.ts.map +1 -1
|
@@ -24,10 +24,12 @@ abstract class AbstractService<T, C, U> implements IDraxCrudService<T, C, U> {
|
|
|
24
24
|
|
|
25
25
|
transformCreate?: (data: C) => Promise<C>;
|
|
26
26
|
transformUpdate?: (data: U) => Promise<U>;
|
|
27
|
+
transformUpdatePartial?: (data: U) => Promise<U>;
|
|
27
28
|
transformRead?: (data: T) => Promise<T>;
|
|
28
29
|
|
|
29
30
|
onCreated?: (data: T) => Promise<void>;
|
|
30
31
|
onUpdated?: (data: T) => Promise<void>;
|
|
32
|
+
onUpdatedPartial?: (data: T) => Promise<void>;
|
|
31
33
|
onDeleted?: (id: string) => Promise<void>;
|
|
32
34
|
|
|
33
35
|
|
|
@@ -156,10 +158,14 @@ abstract class AbstractService<T, C, U> implements IDraxCrudService<T, C, U> {
|
|
|
156
158
|
|
|
157
159
|
data = await this.validateInputUpdatePartial(data)
|
|
158
160
|
|
|
161
|
+
if (this.transformUpdatePartial) {
|
|
162
|
+
data = await this.transformUpdatePartial(data)
|
|
163
|
+
}
|
|
164
|
+
|
|
159
165
|
let item: T = await this._repository.updatePartial(id, data)
|
|
160
166
|
|
|
161
|
-
if (this.
|
|
162
|
-
await this.
|
|
167
|
+
if (this.onUpdatedPartial) {
|
|
168
|
+
await this.onUpdatedPartial(item)
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
item = await this.validateOutput(item)
|
|
@@ -214,10 +220,10 @@ abstract class AbstractService<T, C, U> implements IDraxCrudService<T, C, U> {
|
|
|
214
220
|
|
|
215
221
|
|
|
216
222
|
|
|
217
|
-
async findOneBy(field: string, value: any): Promise<T | null> {
|
|
223
|
+
async findOneBy(field: string, value: any, filters: IDraxFieldFilter[] = []): Promise<T | null> {
|
|
218
224
|
try {
|
|
219
225
|
|
|
220
|
-
let item: T = await this._repository.findOneBy(field, value)
|
|
226
|
+
let item: T = await this._repository.findOneBy(field, value, filters)
|
|
221
227
|
|
|
222
228
|
if (item && this.transformRead) {
|
|
223
229
|
item = await this.transformRead(item)
|
|
@@ -286,10 +292,10 @@ abstract class AbstractService<T, C, U> implements IDraxCrudService<T, C, U> {
|
|
|
286
292
|
}
|
|
287
293
|
}
|
|
288
294
|
|
|
289
|
-
async findBy(field: string, value: any, limit: number = 1000): Promise<T[] | null> {
|
|
295
|
+
async findBy(field: string, value: any, limit: number = 1000, filters: IDraxFieldFilter[] = []): Promise<T[] | null> {
|
|
290
296
|
try {
|
|
291
297
|
|
|
292
|
-
let items: T[] = await this._repository.findBy(field, value, limit);
|
|
298
|
+
let items: T[] = await this._repository.findBy(field, value, limit, filters);
|
|
293
299
|
if (this.transformRead) {
|
|
294
300
|
items = await Promise.all(items.map(item => this.transformRead(item)))
|
|
295
301
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {IDraxCrud, IDraxPaginateOptions, IDraxPaginateResult} from "@drax/
|
|
1
|
+
import type {IDraxCrud, IDraxPaginateOptions, IDraxPaginateResult} from "@drax/crud-share";
|
|
2
2
|
|
|
3
3
|
class MockRepository implements IDraxCrud<any, any, any> {
|
|
4
4
|
async create(data: any): Promise<any> {
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { describe, it, beforeAll, afterAll, expect } from "vitest"
|
|
2
|
+
import { PersonFastifyRoutes } from '../people/routes/PersonRoutes'
|
|
3
|
+
import { PersonPermissions } from '../people/permissions/PersonPermissions'
|
|
4
|
+
|
|
5
|
+
import type { IPersonBase } from '../people/interfaces/IPerson'
|
|
6
|
+
|
|
7
|
+
import {LanguageModel, LanguageSchema} from "../people/models/LanguageModel.js"
|
|
8
|
+
import {CountryModel, CountrySchema} from "../people/models/CountryModel.js"
|
|
9
|
+
import {PersonModel, PersonSchema} from "../people/models/PersonModel.js"
|
|
10
|
+
|
|
11
|
+
import TestSetup from "../setup/TestSetup"
|
|
12
|
+
|
|
13
|
+
describe("Person Controller Test", function () {
|
|
14
|
+
|
|
15
|
+
console.log(LanguageSchema)
|
|
16
|
+
console.log(CountrySchema)
|
|
17
|
+
console.log(PersonSchema)
|
|
18
|
+
|
|
19
|
+
let testSetup = new TestSetup({
|
|
20
|
+
routes: [PersonFastifyRoutes],
|
|
21
|
+
permissions: [PersonPermissions]
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const defaultAddress = { street: "Main St" }
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
await testSetup.setup()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await testSetup.dropAndClose()
|
|
32
|
+
return
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Test users are logged in and get their details
|
|
36
|
+
it("Me Admin Root", async () => {
|
|
37
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
38
|
+
expect(accessToken).toBeTruthy()
|
|
39
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
40
|
+
method: 'get',
|
|
41
|
+
url: '/api/auth/me',
|
|
42
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
43
|
+
});
|
|
44
|
+
const body = resp.json()
|
|
45
|
+
expect(body.username).toBe(testSetup.rootUserData.username)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("Me Tenant One", async () => {
|
|
49
|
+
const { accessToken } = await testSetup.tenantOneUserLogin()
|
|
50
|
+
expect(accessToken).toBeTruthy()
|
|
51
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
52
|
+
method: 'get',
|
|
53
|
+
url: '/api/auth/me',
|
|
54
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
55
|
+
});
|
|
56
|
+
const body = resp.json()
|
|
57
|
+
expect(body.username).toBe(testSetup.tenantOneUserData.username)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("Me Tenant Two", async () => {
|
|
61
|
+
const { accessToken } = await testSetup.tenantTwoUserLogin()
|
|
62
|
+
expect(accessToken).toBeTruthy()
|
|
63
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
64
|
+
method: 'get',
|
|
65
|
+
url: '/api/auth/me',
|
|
66
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
67
|
+
});
|
|
68
|
+
const body = resp.json()
|
|
69
|
+
expect(body.username).toBe(testSetup.tenantTwoUserData.username)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// 1. Create and Find by ID
|
|
73
|
+
it("should create a new person and find by id", async () => {
|
|
74
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
75
|
+
expect(accessToken).toBeTruthy()
|
|
76
|
+
await testSetup.dropCollection('Person')
|
|
77
|
+
|
|
78
|
+
const newPerson: IPersonBase = {
|
|
79
|
+
fullname: "Test Person",
|
|
80
|
+
money: 100,
|
|
81
|
+
address: defaultAddress
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
85
|
+
method: 'POST',
|
|
86
|
+
url: '/api/person',
|
|
87
|
+
payload: newPerson,
|
|
88
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const person = await resp.json()
|
|
92
|
+
if (resp.statusCode !== 200) throw new Error("POST ERROR: " + JSON.stringify(person))
|
|
93
|
+
expect(resp.statusCode).toBe(200)
|
|
94
|
+
expect(person.fullname).toBe("Test Person")
|
|
95
|
+
expect(person._id).toBeDefined()
|
|
96
|
+
|
|
97
|
+
const getResp = await testSetup.fastifyInstance.inject({
|
|
98
|
+
method: 'GET',
|
|
99
|
+
url: '/api/person/' + person._id,
|
|
100
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const getEntity = await getResp.json()
|
|
104
|
+
expect(getResp.statusCode).toBe(200)
|
|
105
|
+
expect(getEntity.fullname).toBe("Test Person")
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// 2. Create and Update (Full Update - PUT)
|
|
109
|
+
it("should create and update a person and finally find by id", async () => {
|
|
110
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
111
|
+
await testSetup.dropCollection('Person')
|
|
112
|
+
|
|
113
|
+
const newPerson: IPersonBase = {
|
|
114
|
+
fullname: "Update Test",
|
|
115
|
+
money: 50,
|
|
116
|
+
address: defaultAddress
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
120
|
+
method: 'POST',
|
|
121
|
+
url: '/api/person',
|
|
122
|
+
payload: newPerson,
|
|
123
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const entity = await resp.json()
|
|
127
|
+
expect(resp.statusCode).toBe(200)
|
|
128
|
+
|
|
129
|
+
const updateData: IPersonBase = {
|
|
130
|
+
fullname: "Updated Person",
|
|
131
|
+
money: 150,
|
|
132
|
+
address: defaultAddress
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const updateResp = await testSetup.fastifyInstance.inject({
|
|
136
|
+
method: 'PUT',
|
|
137
|
+
url: `/api/person/${entity._id}`,
|
|
138
|
+
payload: updateData,
|
|
139
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(updateResp.statusCode).toBe(200)
|
|
143
|
+
|
|
144
|
+
const verifyResp = await testSetup.fastifyInstance.inject({
|
|
145
|
+
method: 'GET',
|
|
146
|
+
url: `/api/person/${entity._id}`,
|
|
147
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const verifiedEntity = await verifyResp.json()
|
|
151
|
+
expect(verifiedEntity.fullname).toBe("Updated Person")
|
|
152
|
+
expect(verifiedEntity.money).toBe(150)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// 3. Create and Partial Update (PATCH)
|
|
156
|
+
it("should create and update partial a person and finally find by id", async () => {
|
|
157
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
158
|
+
await testSetup.dropCollection('Person')
|
|
159
|
+
|
|
160
|
+
const newPerson: IPersonBase = {
|
|
161
|
+
fullname: "Patch Test",
|
|
162
|
+
money: 120,
|
|
163
|
+
address: defaultAddress
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
167
|
+
method: 'POST',
|
|
168
|
+
url: '/api/person',
|
|
169
|
+
payload: newPerson,
|
|
170
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const entity = await resp.json()
|
|
174
|
+
|
|
175
|
+
const updateResp = await testSetup.fastifyInstance.inject({
|
|
176
|
+
method: 'PATCH',
|
|
177
|
+
url: `/api/person/${entity._id}`,
|
|
178
|
+
payload: { fullname: "Patched Person" },
|
|
179
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
expect(updateResp.statusCode).toBe(200)
|
|
183
|
+
|
|
184
|
+
const verifyResp = await testSetup.fastifyInstance.inject({
|
|
185
|
+
method: 'GET',
|
|
186
|
+
url: `/api/person/${entity._id}`,
|
|
187
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const verifiedEntity = await verifyResp.json()
|
|
191
|
+
expect(verifiedEntity.fullname).toBe("Patched Person")
|
|
192
|
+
expect(verifiedEntity.money).toBe(120)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// 4. Create and Delete
|
|
196
|
+
it("should create and delete a person", async () => {
|
|
197
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
198
|
+
await testSetup.dropCollection('Person')
|
|
199
|
+
|
|
200
|
+
const newPerson: IPersonBase = { fullname: "Delete Me", address: defaultAddress }
|
|
201
|
+
|
|
202
|
+
const createResp = await testSetup.fastifyInstance.inject({
|
|
203
|
+
method: 'POST',
|
|
204
|
+
url: '/api/person',
|
|
205
|
+
payload: newPerson,
|
|
206
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const createdEntity = await createResp.json()
|
|
210
|
+
const entityId = createdEntity._id
|
|
211
|
+
|
|
212
|
+
const deleteResp = await testSetup.fastifyInstance.inject({
|
|
213
|
+
method: 'DELETE',
|
|
214
|
+
url: `/api/person/${entityId}`,
|
|
215
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(deleteResp.statusCode).toBe(200)
|
|
219
|
+
|
|
220
|
+
const verifyResp = await testSetup.fastifyInstance.inject({
|
|
221
|
+
method: 'GET',
|
|
222
|
+
url: `/api/person/${entityId}`,
|
|
223
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect(verifyResp.statusCode).toBe(404)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// 5. Create and Paginate
|
|
230
|
+
it("Should create and paginate people", async () => {
|
|
231
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
232
|
+
await testSetup.dropCollection('Person')
|
|
233
|
+
|
|
234
|
+
const entityData = [
|
|
235
|
+
{ fullname: "Person 1", address: defaultAddress },
|
|
236
|
+
{ fullname: "Person 2", address: defaultAddress }
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
for (const data of entityData) {
|
|
240
|
+
await testSetup.fastifyInstance.inject({
|
|
241
|
+
method: 'POST',
|
|
242
|
+
url: '/api/person',
|
|
243
|
+
payload: data,
|
|
244
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
249
|
+
method: 'GET',
|
|
250
|
+
url: '/api/person',
|
|
251
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const result = await resp.json()
|
|
255
|
+
expect(resp.statusCode).toBe(200)
|
|
256
|
+
expect(result.items.length).toBe(2)
|
|
257
|
+
expect(result.total).toBe(2)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// 6. Create and Search
|
|
261
|
+
it("should create and search for people", async () => {
|
|
262
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
263
|
+
await testSetup.dropCollection('Person')
|
|
264
|
+
|
|
265
|
+
const entityData = [
|
|
266
|
+
{ fullname: "Searchable Alice", address: defaultAddress },
|
|
267
|
+
{ fullname: "Searchable Bob", address: defaultAddress },
|
|
268
|
+
{ fullname: "Hidden Charlie", address: defaultAddress }
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
for (const data of entityData) {
|
|
272
|
+
await testSetup.fastifyInstance.inject({
|
|
273
|
+
method: 'POST',
|
|
274
|
+
url: '/api/person',
|
|
275
|
+
payload: data,
|
|
276
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const searchResp = await testSetup.fastifyInstance.inject({
|
|
281
|
+
method: 'GET',
|
|
282
|
+
url: '/api/person/search?search=Searchable',
|
|
283
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const searchResult = await searchResp.json()
|
|
287
|
+
expect(searchResp.statusCode).toBe(200)
|
|
288
|
+
expect(searchResult.length).toBe(2)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// 7. Create and Find with Filters
|
|
292
|
+
it("should create and find people with filters", async () => {
|
|
293
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
294
|
+
await testSetup.dropCollection('Person')
|
|
295
|
+
|
|
296
|
+
const entityData = [
|
|
297
|
+
{ fullname: "Active Person", live: true, address: defaultAddress },
|
|
298
|
+
{ fullname: "Inactive Person", live: false, address: defaultAddress }
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
for (const data of entityData) {
|
|
302
|
+
await testSetup.fastifyInstance.inject({
|
|
303
|
+
method: 'POST',
|
|
304
|
+
url: '/api/person',
|
|
305
|
+
payload: data,
|
|
306
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const findByResp = await testSetup.fastifyInstance.inject({
|
|
311
|
+
method: 'GET',
|
|
312
|
+
url: '/api/person/find?filters=live;eq;true',
|
|
313
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const findByResult = await findByResp.json()
|
|
317
|
+
expect(findByResp.statusCode).toBe(200)
|
|
318
|
+
expect(findByResult.length).toBe(1)
|
|
319
|
+
expect(findByResult[0].fullname).toBe("Active Person")
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// 8. Create and Group By
|
|
323
|
+
it("should create and groupBy for people", async () => {
|
|
324
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
325
|
+
await testSetup.dropCollection('Person')
|
|
326
|
+
|
|
327
|
+
const entityData = [
|
|
328
|
+
{ fullname: "Group A1", race: "human", address: defaultAddress },
|
|
329
|
+
{ fullname: "Group A2", race: "human", address: defaultAddress },
|
|
330
|
+
{ fullname: "Group B1", race: "elf", address: defaultAddress }
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
for (const data of entityData) {
|
|
334
|
+
await testSetup.fastifyInstance.inject({
|
|
335
|
+
method: 'POST',
|
|
336
|
+
url: '/api/person',
|
|
337
|
+
payload: data,
|
|
338
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const groupResp = await testSetup.fastifyInstance.inject({
|
|
343
|
+
method: 'GET',
|
|
344
|
+
url: '/api/person/group-by?fields=race',
|
|
345
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const groupResult = await groupResp.json()
|
|
349
|
+
expect(groupResp.statusCode).toBe(200)
|
|
350
|
+
expect(groupResult.length).toBeGreaterThan(0)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// 9. Error Handling - Not Found
|
|
354
|
+
it("should handle error responses correctly when person is not found", async () => {
|
|
355
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
356
|
+
const nonExistentId = "123456789012345678901234"
|
|
357
|
+
|
|
358
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
359
|
+
method: 'GET',
|
|
360
|
+
url: `/api/person/${nonExistentId}`,
|
|
361
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
expect(resp.statusCode).toBe(404)
|
|
365
|
+
const result = await resp.json()
|
|
366
|
+
expect(result.error).toBeDefined()
|
|
367
|
+
expect(result.message).toContain("Not found")
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// Unhappy Path - Auth
|
|
371
|
+
it("should return 401 when accessing endpoints without token", async () => {
|
|
372
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
373
|
+
method: 'GET',
|
|
374
|
+
url: '/api/person'
|
|
375
|
+
})
|
|
376
|
+
expect(resp.statusCode).toBe(401)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it("should return 400 when providing invalid ID format", async () => {
|
|
380
|
+
const { accessToken } = await testSetup.rootUserLogin()
|
|
381
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
382
|
+
method: 'GET',
|
|
383
|
+
url: '/api/person/invalid-id-format',
|
|
384
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
385
|
+
})
|
|
386
|
+
expect(resp.statusCode).toBe(400)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// ==========================================
|
|
390
|
+
// Multi-Tenancy Tests
|
|
391
|
+
// ==========================================
|
|
392
|
+
|
|
393
|
+
it("Tenant One records cannot be accessed by Tenant Two", async () => {
|
|
394
|
+
await testSetup.dropCollection('Person')
|
|
395
|
+
|
|
396
|
+
// 1. Login Tenant One & Two
|
|
397
|
+
const { accessToken: tokenOne } = await testSetup.tenantOneUserLogin()
|
|
398
|
+
const { accessToken: tokenTwo } = await testSetup.tenantTwoUserLogin()
|
|
399
|
+
expect(tokenOne).toBeTruthy()
|
|
400
|
+
expect(tokenTwo).toBeTruthy()
|
|
401
|
+
|
|
402
|
+
// 2. Tenant One creates a record
|
|
403
|
+
const createResp = await testSetup.fastifyInstance.inject({
|
|
404
|
+
method: 'POST',
|
|
405
|
+
url: '/api/person',
|
|
406
|
+
payload: { fullname: "Tenant One Person", address: defaultAddress },
|
|
407
|
+
headers: { Authorization: `Bearer ${tokenOne}` }
|
|
408
|
+
})
|
|
409
|
+
expect(createResp.statusCode).toBe(200)
|
|
410
|
+
const t1Person = await createResp.json()
|
|
411
|
+
const t1PersonId = t1Person._id
|
|
412
|
+
expect(t1Person.fullname).toBe("Tenant One Person")
|
|
413
|
+
|
|
414
|
+
// 3. Tenant Two attempts to GET the record
|
|
415
|
+
const getT2Resp = await testSetup.fastifyInstance.inject({
|
|
416
|
+
method: 'GET',
|
|
417
|
+
url: `/api/person/${t1PersonId}`,
|
|
418
|
+
headers: { Authorization: `Bearer ${tokenTwo}` }
|
|
419
|
+
})
|
|
420
|
+
expect(getT2Resp.statusCode).not.toBe(200)
|
|
421
|
+
|
|
422
|
+
// 4. Tenant Two attempts to paginate the record
|
|
423
|
+
const paginateT2Resp = await testSetup.fastifyInstance.inject({
|
|
424
|
+
method: 'GET',
|
|
425
|
+
url: `/api/person?search=Tenant One Person`,
|
|
426
|
+
headers: { Authorization: `Bearer ${tokenTwo}` }
|
|
427
|
+
})
|
|
428
|
+
expect(paginateT2Resp.statusCode).toBe(200)
|
|
429
|
+
const paginateT2Body = await paginateT2Resp.json()
|
|
430
|
+
expect(paginateT2Body.items).toEqual([])
|
|
431
|
+
expect(paginateT2Body.total).toBe(0)
|
|
432
|
+
|
|
433
|
+
// 5. Tenant Two attempts to find the record with filters
|
|
434
|
+
const findT2Resp = await testSetup.fastifyInstance.inject({
|
|
435
|
+
method: 'GET',
|
|
436
|
+
url: `/api/person/find?filters=fullname;eq;Tenant One Person`,
|
|
437
|
+
headers: { Authorization: `Bearer ${tokenTwo}` }
|
|
438
|
+
})
|
|
439
|
+
expect(findT2Resp.statusCode).toBe(200)
|
|
440
|
+
expect(await findT2Resp.json()).toEqual([])
|
|
441
|
+
|
|
442
|
+
// 6. Tenant Two attempts to findBy the record
|
|
443
|
+
const findByT2Resp = await testSetup.fastifyInstance.inject({
|
|
444
|
+
method: 'GET',
|
|
445
|
+
url: `/api/person/find-by/fullname/Tenant One Person`,
|
|
446
|
+
headers: { Authorization: `Bearer ${tokenTwo}` }
|
|
447
|
+
})
|
|
448
|
+
expect(findByT2Resp.statusCode).toBe(200)
|
|
449
|
+
expect(await findByT2Resp.json()).toEqual([])
|
|
450
|
+
|
|
451
|
+
// 7. Tenant Two attempts to findOneBy the record
|
|
452
|
+
const findOneByT2Resp = await testSetup.fastifyInstance.inject({
|
|
453
|
+
method: 'GET',
|
|
454
|
+
url: `/api/person/find-one-by/fullname/Tenant One Person`,
|
|
455
|
+
headers: { Authorization: `Bearer ${tokenTwo}` }
|
|
456
|
+
})
|
|
457
|
+
expect(findOneByT2Resp.statusCode).toBe(200)
|
|
458
|
+
expect(await findOneByT2Resp.json()).toEqual({})
|
|
459
|
+
|
|
460
|
+
// 8. Tenant Two attempts to UPDATE the record
|
|
461
|
+
const updateT2Resp = await testSetup.fastifyInstance.inject({
|
|
462
|
+
method: 'PUT',
|
|
463
|
+
url: `/api/person/${t1PersonId}`,
|
|
464
|
+
payload: { fullname: "Hacked by T2", address: defaultAddress },
|
|
465
|
+
headers: { Authorization: `Bearer ${tokenTwo}` }
|
|
466
|
+
})
|
|
467
|
+
expect(updateT2Resp.statusCode).not.toBe(200)
|
|
468
|
+
|
|
469
|
+
// 9. Tenant Two attempts to PATCH the record
|
|
470
|
+
const updatePartialT2Resp = await testSetup.fastifyInstance.inject({
|
|
471
|
+
method: 'PATCH',
|
|
472
|
+
url: `/api/person/${t1PersonId}`,
|
|
473
|
+
payload: { fullname: "Patched by T2" },
|
|
474
|
+
headers: { Authorization: `Bearer ${tokenTwo}` }
|
|
475
|
+
})
|
|
476
|
+
expect(updatePartialT2Resp.statusCode).not.toBe(200)
|
|
477
|
+
|
|
478
|
+
// 10. Tenant Two attempts to DELETE the record
|
|
479
|
+
const deleteT2Resp = await testSetup.fastifyInstance.inject({
|
|
480
|
+
method: 'DELETE',
|
|
481
|
+
url: `/api/person/${t1PersonId}`,
|
|
482
|
+
headers: { Authorization: `Bearer ${tokenTwo}` }
|
|
483
|
+
})
|
|
484
|
+
expect(deleteT2Resp.statusCode).not.toBe(200)
|
|
485
|
+
|
|
486
|
+
// 11. Tenant One can successfully read it unchanged
|
|
487
|
+
const getT1Resp = await testSetup.fastifyInstance.inject({
|
|
488
|
+
method: 'GET',
|
|
489
|
+
url: `/api/person/${t1PersonId}`,
|
|
490
|
+
headers: { Authorization: `Bearer ${tokenOne}` }
|
|
491
|
+
})
|
|
492
|
+
expect(getT1Resp.statusCode).toBe(200)
|
|
493
|
+
expect((await getT1Resp.json()).fullname).toBe("Tenant One Person")
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it("Root external admin can access records from any tenant", async () => {
|
|
497
|
+
await testSetup.dropCollection('Person')
|
|
498
|
+
|
|
499
|
+
// 1. Logins
|
|
500
|
+
const { accessToken: tokenOne } = await testSetup.tenantOneUserLogin()
|
|
501
|
+
const { accessToken: rootToken } = await testSetup.rootUserLogin()
|
|
502
|
+
|
|
503
|
+
// 2. Tenant One creates a record
|
|
504
|
+
const createResp = await testSetup.fastifyInstance.inject({
|
|
505
|
+
method: 'POST',
|
|
506
|
+
url: '/api/person',
|
|
507
|
+
payload: { fullname: "Tenant One Admin Test", address: defaultAddress },
|
|
508
|
+
headers: { Authorization: `Bearer ${tokenOne}` }
|
|
509
|
+
})
|
|
510
|
+
expect(createResp.statusCode).toBe(200)
|
|
511
|
+
const t1PersonId = (await createResp.json())._id
|
|
512
|
+
|
|
513
|
+
// 3. Root Admin views the record
|
|
514
|
+
const getRootResp = await testSetup.fastifyInstance.inject({
|
|
515
|
+
method: 'GET',
|
|
516
|
+
url: `/api/person/${t1PersonId}`,
|
|
517
|
+
headers: { Authorization: `Bearer ${rootToken}` }
|
|
518
|
+
})
|
|
519
|
+
expect(getRootResp.statusCode).toBe(200)
|
|
520
|
+
|
|
521
|
+
// 4. Root Admin updates the record
|
|
522
|
+
const updateRootResp = await testSetup.fastifyInstance.inject({
|
|
523
|
+
method: 'PUT',
|
|
524
|
+
url: `/api/person/${t1PersonId}`,
|
|
525
|
+
payload: { fullname: "Updated by Root", address: defaultAddress },
|
|
526
|
+
headers: { Authorization: `Bearer ${rootToken}` }
|
|
527
|
+
})
|
|
528
|
+
expect(updateRootResp.statusCode).toBe(200)
|
|
529
|
+
|
|
530
|
+
// 5. Root Admin deletes the record
|
|
531
|
+
const deleteRootResp = await testSetup.fastifyInstance.inject({
|
|
532
|
+
method: 'DELETE',
|
|
533
|
+
url: `/api/person/${t1PersonId}`,
|
|
534
|
+
headers: { Authorization: `Bearer ${rootToken}` }
|
|
535
|
+
})
|
|
536
|
+
expect(deleteRootResp.statusCode).toBe(200)
|
|
537
|
+
|
|
538
|
+
// Verify it was actually deleted
|
|
539
|
+
const getRootDeletedResp = await testSetup.fastifyInstance.inject({
|
|
540
|
+
method: 'GET',
|
|
541
|
+
url: `/api/person/${t1PersonId}`,
|
|
542
|
+
headers: { Authorization: `Bearer ${rootToken}` }
|
|
543
|
+
})
|
|
544
|
+
expect(getRootDeletedResp.statusCode).toBe(404)
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
import CountryServiceFactory from "../factory/services/CountryServiceFactory.js";
|
|
3
|
+
import {AbstractFastifyController, CustomRequest} from "@drax/crud-back";
|
|
4
|
+
import CountryPermissions from "../permissions/CountryPermissions.js";
|
|
5
|
+
import type {ICountry, ICountryBase} from "../interfaces/ICountry";
|
|
6
|
+
import {BadRequestError} from "@drax/common-back";
|
|
7
|
+
import type {FastifyReply} from "fastify";
|
|
8
|
+
import type {IDraxPaginateResult} from "@drax/crud-share/dist";
|
|
9
|
+
|
|
10
|
+
class CountryController extends AbstractFastifyController<ICountry, ICountryBase, ICountryBase> {
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super(CountryServiceFactory.instance, CountryPermissions)
|
|
14
|
+
this.tenantField = "tenant";
|
|
15
|
+
this.userField = "createdBy";
|
|
16
|
+
|
|
17
|
+
this.tenantFilter = true;
|
|
18
|
+
this.tenantSetter = true;
|
|
19
|
+
this.tenantAssert = true;
|
|
20
|
+
|
|
21
|
+
this.userFilter = true;
|
|
22
|
+
this.userSetter = true;
|
|
23
|
+
this.userAssert = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// async paginate(request: CustomRequest, reply: FastifyReply){
|
|
27
|
+
// if(true){
|
|
28
|
+
// throw new BadRequestError("Sarasa",'error.custom')
|
|
29
|
+
//
|
|
30
|
+
// }
|
|
31
|
+
// return {} as Promise<IDraxPaginateResult<ICountry>>
|
|
32
|
+
// }
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default CountryController;
|
|
37
|
+
export {
|
|
38
|
+
CountryController
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
import LanguageServiceFactory from "../factory/services/LanguageServiceFactory.js";
|
|
3
|
+
import {AbstractFastifyController} from "@drax/crud-back";
|
|
4
|
+
import LanguagePermissions from "../permissions/LanguagePermissions.js";
|
|
5
|
+
import type {ILanguage, ILanguageBase} from "../interfaces/ILanguage";
|
|
6
|
+
|
|
7
|
+
class LanguageController extends AbstractFastifyController<ILanguage, ILanguageBase, ILanguageBase> {
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super(LanguageServiceFactory.instance, LanguagePermissions)
|
|
11
|
+
this.tenantField = "tenant";
|
|
12
|
+
this.userField = "user";
|
|
13
|
+
|
|
14
|
+
this.tenantFilter = false;
|
|
15
|
+
this.tenantSetter = false;
|
|
16
|
+
this.tenantAssert = false;
|
|
17
|
+
|
|
18
|
+
this.userFilter = false;
|
|
19
|
+
this.userSetter = false;
|
|
20
|
+
this.userAssert = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default LanguageController;
|
|
26
|
+
export {
|
|
27
|
+
LanguageController
|
|
28
|
+
}
|
|
29
|
+
|