crud icon indicating copy to clipboard operation
crud copied to clipboard

Soft Delete Support (TypeORM)

Open exequiel09 opened this issue 5 years ago • 9 comments

TypeORM recently release a new version containing the Soft Delete functionality. I think we should also support this functionality.

exequiel09 avatar Mar 02 '20 01:03 exequiel09

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 avatar Mar 20 '20 13:03 listochkin

@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 avatar Apr 30 '20 13:04 ruslanguns

@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];

listochkin avatar May 05 '20 12:05 listochkin

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) {}
}

JonatanSalas avatar May 20 '20 03:05 JonatanSalas

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
    );
  }
  }
}

bestickley avatar Jun 25 '20 17:06 bestickley

@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

quangtudng avatar Oct 04 '20 10:10 quangtudng

Any updates on this? Would love to see this feature officially supported in the future

Delta843 avatar Apr 13 '21 12:04 Delta843

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

Screenshot 2024-03-11 at 17 21 29

danyalutsevich avatar Mar 11 '24 15:03 danyalutsevich