Soft Delete Support (TypeORM)
TypeORM recently release a new version containing the Soft Delete functionality. I think we should also support this functionality.
Here's how I did it for User:
User Entity
@DeleteDateColumn()
deletedAt: Date;
Users Service
markDeleted(id) {
this.usersRepository.softDelete({ id })
return;
}
Users Controller
@Crud({
...
query: {
filter: {
deletedAt: {
$eq: null
}
},
},
...
})
@Controller('users')
export class UsersController implements CrudController<User> {
constructor(public service: UsersService) { }
@Override()
async deleteOne(@ParsedRequest() req: CrudRequest) {
const id = req.parsed.paramsFilter
.find(f => f.field === 'id' && f.operator === '$eq').value;
const res = await this.service.markDeleted(id);
return res;
}
}
Pretty consize overall, but build-in support would still be nice.
@listochkin good advice thanks!, I just have one question... I'd like to be able to list deletedUsers using CrudRequest feature for filtering, sorting or paging results from controller, but adding a custom option { withDeleted: true } , Could you please advice me on this?
const deletedUsers = await this.userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } });});
const deletedUsers = await this.userRepository.find(ids, { withDeleted: true, where: { deletedAt: Not(IsNull()) } });});
const deletedUser = await this.userRepository.findOne(id, { withDeleted: true, where: { deletedAt: Not(IsNull()) } });});
@ruslanguns how about using filter for that?
const deletedUsers = await fetch(`http://my.app/users?filter=${encodeURIComponent('deletedAt||$notnull')}`);
That would be an extra request on the UI (one for loading current users, another for deleted) but you could run them in parallel like this:
const [activeUsers, deletedUsers] = await Promise.all(
fetch(`http://my.app/users`),
fetch(`http://my.app/users?filter=${encodeURIComponent('deletedAt||$notnull')}`)
);
const allUsers = [...activeUsers, ...deletedUsers];
Would be pretty cool to support it via CRUD config like in this sample:
@Crud({
model: {
type: MyModel,
},
routes: {
deleteOneBase: {
softDelete: true,
},
},
})
@Controller('my-models')
export class MyModelController implements CrudController<MyModel> {
constructor(public service: MyModelService) {}
}
In the meantime, I created a generic helper function that you can use in your services:
// crudHelper.ts
export async function softDeleteOne<T>(
this: TypeOrmCrudService<T>,
req: CrudRequest
): Promise<void | T> {
if (req.options.routes && req.options.routes.deleteOneBase) {
const { returnDeleted } = req.options.routes.deleteOneBase;
const found = await this.getOneOrFail(req, returnDeleted);
const toReturn = returnDeleted
? plainToClass(this.entityType, { ...found })
: undefined;
await this.repo.softRemove(found);
return toReturn;
} else {
throw new InternalServerErrorException("Incomplete CrudRequest");
}
}
// user.service.ts
@Injectable()
export class UserService extends TypeOrmCrudService<User> {
constructor(
@InjectRepository(User) private readonly userRepository: Repository<User>
)
deleteOne(req: CrudRequest): Promise<void | User> {
async deleteOne(req: CrudRequest): Promise<void | User> {
return await softDeleteOne.call<
TypeOrmCrudService<User>,
[CrudRequest],
Promise<void | User>
>(this, req);
}
}
}
UPDATE: I can't use the above code because TypeOrmCrudService's methods are protected so here you go:
export async function softDeleteOne<T>(
req: CrudRequest,
getOneOrFail: (req: CrudRequest, shallow?: boolean) => Promise<T>,
entityType: ClassType<T>,
repo: Repository<T>
): Promise<void | T> {
if (!req.options.routes?.deleteOneBase)
throw new InternalServerErrorException(
"Incomplete CrudRequest: missing req.options.routes.deleteOneBase"
);
const { returnDeleted } = req.options.routes.deleteOneBase;
const found = await getOneOrFail(req, returnDeleted);
const toReturn = returnDeleted
? plainToClass(entityType, { ...found })
: undefined;
await repo.softRemove(found);
return toReturn;
}
// user.service.ts
@Injectable()
export class UserService extends TypeOrmCrudService<User> {
constructor(
@InjectRepository(User) private readonly userRepository: Repository<User>
)
deleteOne(req: CrudRequest): Promise<void | User> {
async deleteOne(req: CrudRequest): Promise<void | User> {
return await softDeleteOne(
req,
this.getOneOrFail,
this.entityType,
this.repo
);
}
}
}
@ruslanguns how about using filter for that?
const deletedUsers = await fetch(`http://my.app/users?filter=${encodeURIComponent('deletedAt||$notnull')}`);That would be an extra request on the UI (one for loading current users, another for deleted) but you could run them in parallel like this:
const [activeUsers, deletedUsers] = await Promise.all( fetch(`http://my.app/users`), fetch(`http://my.app/users?filter=${encodeURIComponent('deletedAt||$notnull')}`) ); const allUsers = [...activeUsers, ...deletedUsers];
The filter did not give me the deleted item unfortunately. Actually, i think it already ignored all the deleted item in the getMany route already
Any updates on this? Would love to see this feature officially supported in the future
i guess thats what yall looking for i dont know if that was already done in nestjsx/crud but this feature avaliable here https://github.com/gid-oss/dataui-nestjs-crud