Skip to content

feat: add support for soft deleted entities #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion spec/Database.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
})
48 changes: 46 additions & 2 deletions spec/Resource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
])
})
})
Expand Down Expand Up @@ -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)
})
})
})
})
5 changes: 4 additions & 1 deletion spec/entities/Car.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -74,4 +74,7 @@ export class Car extends BaseEntity {

@UpdateDateColumn({ name: 'updated_at' })
public updatedAt: Date;

@DeleteDateColumn({ name: 'deleted_at' })
public deletedAt: Date;
}
7 changes: 7 additions & 0 deletions spec/entities/CarWithDeleted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Entity } from 'typeorm'
import { Car } from './Car'

@Entity('car')
export class CarWithDeleted extends Car {
static withDeleted = true
}
24 changes: 18 additions & 6 deletions src/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,28 @@ import safeParseNumber from './utils/safe-parse-number'

type ParamsType = Record<string, any>;

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<string, Property> = {};

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 {
Expand Down Expand Up @@ -53,6 +63,7 @@ export class Resource extends BaseResource {
public async count(filter: Filter): Promise<number> {
return this.model.count(({
where: convertFilter(filter),
withDeleted: this.withDeleted
}))
}

Expand All @@ -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))
}
Expand All @@ -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
}
Expand All @@ -88,7 +100,7 @@ export class Resource extends BaseResource {
public async findMany(ids: Array<string | number>): Promise<Array<BaseRecord>> {
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))
}
Expand All @@ -104,7 +116,7 @@ export class Resource extends BaseResource {
public async update(pk: string | number, params: any = {}): Promise<ParamsType> {
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<any, any>(this.prepareParams(params))
Object.keys(preparedParams).forEach((paramName) => {
Expand All @@ -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()
}
Expand Down