Skip to content

Commit 447b976

Browse files
authored
feat: added control of soft deletion in query params (#1097)
1 parent 71e120c commit 447b976

File tree

6 files changed

+75
-2
lines changed

6 files changed

+75
-2
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ The following code exposes a route that can be utilized like so:
5353
#### Endpoint
5454

5555
```url
56-
http://localhost:3000/cats?limit=5&page=2&sortBy=color:DESC&search=i&filter.age=$gte:3&select=id,name,color,age
56+
http://localhost:3000/cats?limit=5&page=2&sortBy=color:DESC&search=i&filter.age=$gte:3&select=id,name,color,age&withDeleted=true
5757
```
5858

5959
#### Result
@@ -339,6 +339,13 @@ const paginateConfig: PaginateConfig<CatEntity> {
339339
*/
340340
withDeleted: false,
341341

342+
/**
343+
* Required: false
344+
* Type: boolean
345+
* Description: Allows to specify withDeleted in query params to retrieve soft deleted records, convinient when you have archive functionality and some toggle to show or hide them. If not enabled explicitly the withDeleted query param will be ignored.
346+
*/
347+
allowWithDeletedInQuery: false,
348+
342349
/**
343350
* Required: false
344351
* Type: string

src/decorator.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ describe('Decorator', () => {
109109
filter: undefined,
110110
select: undefined,
111111
cursor: undefined,
112+
withDeleted: undefined,
112113
path: 'http://localhost/items',
113114
})
114115
})
@@ -127,6 +128,7 @@ describe('Decorator', () => {
127128
filter: undefined,
128129
select: undefined,
129130
cursor: undefined,
131+
withDeleted: undefined,
130132
path: 'http://localhost/items',
131133
})
132134
})
@@ -137,6 +139,7 @@ describe('Decorator', () => {
137139
limit: '20',
138140
sortBy: ['id:ASC', 'createdAt:DESC'],
139141
search: 'white',
142+
withDeleted: 'true',
140143
'filter.name': '$not:$eq:Kitty',
141144
'filter.createdAt': ['$gte:2020-01-01', '$lte:2020-12-31'],
142145
select: ['name', 'createdAt'],
@@ -154,6 +157,7 @@ describe('Decorator', () => {
154157
],
155158
search: 'white',
156159
searchBy: undefined,
160+
withDeleted: true,
157161
select: ['name', 'createdAt'],
158162
path: 'http://localhost/items',
159163
filter: {
@@ -170,6 +174,7 @@ describe('Decorator', () => {
170174
limit: '20',
171175
sortBy: ['id:ASC', 'createdAt:DESC'],
172176
search: 'white',
177+
withDeleted: 'false',
173178
'filter.name': '$not:$eq:Kitty',
174179
'filter.createdAt': ['$gte:2020-01-01', '$lte:2020-12-31'],
175180
select: ['name', 'createdAt'],
@@ -187,6 +192,7 @@ describe('Decorator', () => {
187192
],
188193
search: 'white',
189194
searchBy: undefined,
195+
withDeleted: false,
190196
path: 'http://localhost/items',
191197
filter: {
192198
name: '$not:$eq:Kitty',

src/decorator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface PaginateQuery {
2020
filter?: { [column: string]: string | string[] }
2121
select?: string[]
2222
cursor?: string
23+
withDeleted?: boolean
2324
path: string
2425
}
2526

@@ -103,6 +104,7 @@ export const Paginate = createParamDecorator((_data: unknown, ctx: ExecutionCont
103104
filter: Object.keys(filter).length ? filter : undefined,
104105
select,
105106
cursor: query.cursor ? query.cursor.toString() : undefined,
107+
withDeleted: query.withDeleted === 'true' ? true : query.withDeleted === 'false' ? false : undefined,
106108
path,
107109
}
108110
})

src/paginate.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2627,6 +2627,51 @@ describe('paginate', () => {
26272627
await catRepo.restore({ id: cats[0].id })
26282628
})
26292629

2630+
it('should return all items even if deleted, by passing with deleted in query params', async () => {
2631+
const config: PaginateConfig<CatEntity> = {
2632+
sortableColumns: ['id'],
2633+
allowWithDeletedInQuery: true,
2634+
}
2635+
const query: PaginateQuery = {
2636+
path: '',
2637+
withDeleted: true,
2638+
}
2639+
await catRepo.softDelete({ id: cats[0].id })
2640+
const result = await paginate<CatEntity>(query, catRepo, config)
2641+
expect(result.meta.totalItems).toBe(cats.length)
2642+
await catRepo.restore({ id: cats[0].id })
2643+
})
2644+
2645+
it('should return all items even if deleted if config specified withDeleted false', async () => {
2646+
const config: PaginateConfig<CatEntity> = {
2647+
sortableColumns: ['id'],
2648+
allowWithDeletedInQuery: true,
2649+
withDeleted: false,
2650+
}
2651+
const query: PaginateQuery = {
2652+
path: '',
2653+
withDeleted: true,
2654+
}
2655+
await catRepo.softDelete({ id: cats[0].id })
2656+
const result = await paginate<CatEntity>(query, catRepo, config)
2657+
expect(result.meta.totalItems).toBe(cats.length)
2658+
await catRepo.restore({ id: cats[0].id })
2659+
})
2660+
2661+
it('should not return items with deleted not allowed in config', async () => {
2662+
const config: PaginateConfig<CatEntity> = {
2663+
sortableColumns: ['id'],
2664+
}
2665+
const query: PaginateQuery = {
2666+
path: '',
2667+
withDeleted: true,
2668+
}
2669+
await catRepo.softDelete({ id: cats[0].id })
2670+
const result = await paginate<CatEntity>(query, catRepo, config)
2671+
expect(result.meta.totalItems).toBe(cats.length - 1)
2672+
await catRepo.restore({ id: cats[0].id })
2673+
})
2674+
26302675
it('should return all relation items even if deleted', async () => {
26312676
const config: PaginateConfig<CatHomeEntity> = {
26322677
sortableColumns: ['id'],

src/paginate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export interface PaginateConfig<T> {
9494
filterableColumns?: Partial<MappedColumns<T, (FilterOperator | FilterSuffix)[] | true>>
9595
loadEagerRelations?: boolean
9696
withDeleted?: boolean
97+
allowWithDeletedInQuery?: boolean
9798
paginationType?: PaginationType
9899
relativePath?: boolean
99100
origin?: string
@@ -653,7 +654,7 @@ export async function paginate<T extends ObjectLiteral>(
653654
}
654655
}
655656

656-
if (config.withDeleted) {
657+
if (config.withDeleted || (config.allowWithDeletedInQuery && query.withDeleted)) {
657658
queryBuilder.withDeleted()
658659
}
659660

src/swagger/api-paginated-query.decorator.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ ${li('Available Fields', paginateConfig.searchableColumns)}
180180
})
181181
}
182182

183+
export function WithDeleted(paginateConfig: PaginateConfig<any>) {
184+
if (!paginateConfig.allowWithDeletedInQuery) return
185+
186+
return ApiQuery({
187+
name: 'withDeleted',
188+
description: `Retrieve records including soft deleted ones`,
189+
required: false,
190+
type: 'boolean',
191+
})
192+
}
193+
183194
export const ApiPaginationQuery = (paginationConfig: PaginateConfig<any>) => {
184195
return applyDecorators(
185196
...[
@@ -190,6 +201,7 @@ export const ApiPaginationQuery = (paginationConfig: PaginateConfig<any>) => {
190201
Search(paginationConfig),
191202
SearchBy(paginationConfig),
192203
Select(paginationConfig),
204+
WithDeleted(paginationConfig),
193205
].filter((v): v is MethodDecorator => v !== undefined)
194206
)
195207
}

0 commit comments

Comments
 (0)