@drax/crud-back 2.11.0 → 3.1.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.
Files changed (65) hide show
  1. package/dist/controllers/AbstractFastifyController.js +47 -24
  2. package/dist/repository/AbstractMongoRepository.js +12 -8
  3. package/dist/repository/AbstractSqliteRepository.js +40 -9
  4. package/dist/services/AbstractService.js +9 -6
  5. package/package.json +8 -7
  6. package/src/controllers/AbstractFastifyController.ts +56 -28
  7. package/src/repository/AbstractMongoRepository.ts +17 -8
  8. package/src/repository/AbstractSqliteRepository.ts +61 -10
  9. package/src/services/AbstractService.ts +12 -6
  10. package/test/_mocks/MockRepository.ts +1 -1
  11. package/test/controllers/PersonController.test.ts +547 -0
  12. package/test/people/controllers/CountryController.ts +40 -0
  13. package/test/people/controllers/LanguageController.ts +29 -0
  14. package/test/people/controllers/PersonController.ts +29 -0
  15. package/test/people/factory/services/CountryServiceFactory.ts +41 -0
  16. package/test/people/factory/services/LanguageServiceFactory.ts +41 -0
  17. package/test/people/factory/services/PersonServiceFactory.ts +41 -0
  18. package/test/people/interfaces/ICountry.ts +28 -0
  19. package/test/people/interfaces/ICountryRepository.ts +11 -0
  20. package/test/people/interfaces/ILanguage.ts +32 -0
  21. package/test/people/interfaces/ILanguageRepository.ts +11 -0
  22. package/test/people/interfaces/IPerson.ts +58 -0
  23. package/test/people/interfaces/IPersonRepository.ts +11 -0
  24. package/test/people/models/CountryModel.ts +38 -0
  25. package/test/people/models/LanguageModel.ts +40 -0
  26. package/test/people/models/PersonModel.ts +61 -0
  27. package/test/people/permissions/CountryPermissions.ts +14 -0
  28. package/test/people/permissions/LanguagePermissions.ts +14 -0
  29. package/test/people/permissions/PersonPermissions.ts +18 -0
  30. package/test/people/repository/mongo/CountryMongoRepository.ts +22 -0
  31. package/test/people/repository/mongo/LanguageMongoRepository.ts +22 -0
  32. package/test/people/repository/mongo/PersonMongoRepository.ts +22 -0
  33. package/test/people/repository/sqlite/CountrySqliteRepository.ts +33 -0
  34. package/test/people/repository/sqlite/LanguageSqliteRepository.ts +37 -0
  35. package/test/people/repository/sqlite/PersonSqliteRepository.ts +42 -0
  36. package/test/people/routes/CountryRoutes.ts +34 -0
  37. package/test/people/routes/LanguageRoutes.ts +34 -0
  38. package/test/people/routes/PersonRoutes.ts +40 -0
  39. package/test/people/schemas/CountrySchema.ts +22 -0
  40. package/test/people/schemas/LanguageSchema.ts +22 -0
  41. package/test/people/schemas/PersonSchema.ts +42 -0
  42. package/test/people/services/CountryService.ts +20 -0
  43. package/test/people/services/LanguageService.ts +20 -0
  44. package/test/people/services/PersonService.ts +20 -0
  45. package/test/services/AbstractService.test.ts +11 -7
  46. package/test/setup/MongoInMemory.ts +56 -0
  47. package/test/setup/TestSetup.ts +344 -0
  48. package/test/setup/data/admin-role.ts +13 -0
  49. package/test/setup/data/basic-user.ts +14 -0
  50. package/test/setup/data/one-tenant.ts +6 -0
  51. package/test/setup/data/restricted-role.ts +16 -0
  52. package/test/setup/data/root-user.ts +14 -0
  53. package/test/setup/data/tenant-one-user.ts +13 -0
  54. package/test/setup/data/tenant-two-user.ts +14 -0
  55. package/test/setup/data/two-tenant.ts +6 -0
  56. package/test/workers/ExportCsvWorker.test.ts +1 -1
  57. package/tsconfig.json +2 -1
  58. package/tsconfig.tsbuildinfo +1 -1
  59. package/types/controllers/AbstractFastifyController.d.ts.map +1 -1
  60. package/types/repository/AbstractMongoRepository.d.ts +3 -3
  61. package/types/repository/AbstractMongoRepository.d.ts.map +1 -1
  62. package/types/repository/AbstractSqliteRepository.d.ts +5 -4
  63. package/types/repository/AbstractSqliteRepository.d.ts.map +1 -1
  64. package/types/services/AbstractService.d.ts +4 -2
  65. 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.onUpdated) {
162
- await this.onUpdated(item)
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/common-share";
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
+