diff --git a/README.md b/README.md index 9a212163..46adf6a6 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,26 @@ export class Person extends BaseEntity Admin supports ManyToOne relationship but you also have to define @RealationId as stated in the example above. +## Get soft deleted entities + +Sometimes you might want to find entities that soft deleted. You can extend the existing entity with `withDeleted` property: + +``` +@Entity() +export class Person extends BaseEntity { + @DeleteDateColumn() + deletedAt: Date|null; +} + +// admin person model +@Entity('person') +export class AdminPagePerson extends Person { + static withDeleted = true +} +``` + +You can pass `AdminPagePerson` to the resource configuration to include soft deleted entities in the query builder. + ## Contribution ### Running the example app diff --git a/spec/Database.spec.ts b/spec/Database.spec.ts index 0160d01f..15abf615 100644 --- a/spec/Database.spec.ts +++ b/spec/Database.spec.ts @@ -22,7 +22,7 @@ describe('Database', () => { describe('#resources', () => { it('returns all entities', async () => { - expect(new Database(dataSource).resources()).to.have.lengthOf(3) + expect(new Database(dataSource).resources()).to.have.lengthOf(4) }) }) }) diff --git a/spec/Resource.spec.ts b/spec/Resource.spec.ts index ab9ce646..21f9b064 100644 --- a/spec/Resource.spec.ts +++ b/spec/Resource.spec.ts @@ -8,6 +8,7 @@ import { dataSource } from './utils/test-data-source' import { Resource } from '../src/Resource' import { CarBuyer } from './entities/CarBuyer' +import { CarWithDeleted } from './entities/CarWithDeleted' describe('Resource', () => { let resource: Resource @@ -68,12 +69,12 @@ describe('Resource', () => { describe('#properties', () => { it('returns all the properties', () => { - expect(resource.properties()).to.have.lengthOf(12) + expect(resource.properties()).to.have.lengthOf(13) }) it('returns all properties with the correct position', () => { expect(resource.properties().map((property) => property.position())).to.deep.equal([ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ]) }) }) @@ -300,4 +301,47 @@ describe('Resource', () => { } }) }) + + describe("when model's withDeleted property is true", () => { + let softDeletedEntity: Car + let record: BaseRecord|null = null + + beforeEach(async () => { + resource = new Resource(CarWithDeleted) + softDeletedEntity = await resource.create(data) as Car + record = await resource.findOne(softDeletedEntity.carId) + await Car.softRemove(softDeletedEntity as Car) + }) + + afterEach(async () => { + await Car.delete({}) + }) + + describe('#find', () => { + it('returns soft deleted resource', async () => { + const filter = new Filter({}, resource) + return expect(await resource.find(filter, { sort: { sortBy: 'name' } })).to.have.lengthOf(1) + }) + }) + + describe('#update', () => { + it('updates record name', async () => { + const ford = 'Ford' + await resource.update((record && record.id()) as string, { + name: ford, + }) + const recordInDb = await resource.findOne((record && record.id()) as string) + + expect(recordInDb && recordInDb.get('name')).to.equal(ford) + }) + }) + + describe('#delete', () => { + it('deletes the resource', async () => { + expect(await resource.count({} as Filter)).to.eq(1) + await resource.delete(softDeletedEntity.carId) + expect(await resource.count({} as Filter)).to.eq(0) + }) + }) + }) }) diff --git a/spec/entities/Car.ts b/spec/entities/Car.ts index 28b30ec5..67db1152 100644 --- a/spec/entities/Car.ts +++ b/spec/entities/Car.ts @@ -1,6 +1,6 @@ import { Entity, Column, PrimaryGeneratedColumn, BaseEntity, ManyToOne, - JoinColumn, UpdateDateColumn, CreateDateColumn, RelationId, + JoinColumn, UpdateDateColumn, CreateDateColumn, RelationId, DeleteDateColumn, } from 'typeorm' import { IsDefined, Min, Max } from 'class-validator' import { CarDealer } from './CarDealer' @@ -74,4 +74,7 @@ export class Car extends BaseEntity { @UpdateDateColumn({ name: 'updated_at' }) public updatedAt: Date; + + @DeleteDateColumn({ name: 'deleted_at' }) + public deletedAt: Date; } diff --git a/spec/entities/CarWithDeleted.ts b/spec/entities/CarWithDeleted.ts new file mode 100644 index 00000000..55cdfa5b --- /dev/null +++ b/spec/entities/CarWithDeleted.ts @@ -0,0 +1,7 @@ +import { Entity } from 'typeorm' +import { Car } from './Car' + +@Entity('car') +export class CarWithDeleted extends Car { + static withDeleted = true +} diff --git a/src/Resource.ts b/src/Resource.ts index 8f3f47d2..13523e3b 100644 --- a/src/Resource.ts +++ b/src/Resource.ts @@ -8,18 +8,28 @@ import safeParseNumber from './utils/safe-parse-number' type ParamsType = Record; +type ModelType = typeof BaseEntity & { + withDeleted?: (() => boolean) | boolean; +} + export class Resource extends BaseResource { public static validate: any; - private model: typeof BaseEntity; + private model: ModelType; private propsObject: Record = {}; - constructor(model: typeof BaseEntity) { + private withDeleted = false + + constructor(model: ModelType) { super(model) this.model = model this.propsObject = this.prepareProps() + + this.withDeleted = typeof this.model.withDeleted === 'function' + ? !!this.model.withDeleted() + : !!this.model.withDeleted } public databaseName(): string { @@ -53,6 +63,7 @@ export class Resource extends BaseResource { public async count(filter: Filter): Promise { return this.model.count(({ where: convertFilter(filter), + withDeleted: this.withDeleted })) } @@ -70,6 +81,7 @@ export class Resource extends BaseResource { order: { [sortBy]: (direction || 'asc').toUpperCase(), }, + withDeleted: this.withDeleted, }) return instances.map((instance) => new BaseRecord(instance, this)) } @@ -78,7 +90,7 @@ export class Resource extends BaseResource { const reference: any = {} reference[this.idName()] = id - const instance = await this.model.findOneBy(reference) + const instance = await this.model.findOne({ where: reference, withDeleted: this.withDeleted }) if (!instance) { return null } @@ -88,7 +100,7 @@ export class Resource extends BaseResource { public async findMany(ids: Array): Promise> { const reference: any = {} reference[this.idName()] = In(ids) - const instances = await this.model.findBy(reference) + const instances = await this.model.find({ where: reference, withDeleted: this.withDeleted }) return instances.map((instance) => new BaseRecord(instance, this)) } @@ -104,7 +116,7 @@ export class Resource extends BaseResource { public async update(pk: string | number, params: any = {}): Promise { const reference: any = {} reference[this.idName()] = pk - const instance = await this.model.findOneBy(reference) + const instance = await this.model.findOne({ where: reference, withDeleted: this.withDeleted }) if (instance) { const preparedParams = flat.unflatten(this.prepareParams(params)) Object.keys(preparedParams).forEach((paramName) => { @@ -120,7 +132,7 @@ export class Resource extends BaseResource { const reference: any = {} reference[this.idName()] = pk try { - const instance = await this.model.findOneBy(reference) + const instance = await this.model.findOne({ where: reference, withDeleted: this.withDeleted }) if (instance) { await instance.remove() }