Skip to content

Commit

Permalink
feat(repository): allow deletedBy id to be configurable using class p…
Browse files Browse the repository at this point in the history
…rotected property

BREAKING CHANGE:
change approach of deletedById key provider

gh-99
  • Loading branch information
Samarpan Bhattacharya authored and Samarpan Bhattacharya committed Nov 6, 2022
1 parent 1be1dc4 commit 28de1cd
Show file tree
Hide file tree
Showing 7 changed files with 477 additions and 40 deletions.
21 changes: 2 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class ItemRepository extends SoftCrudRepositoryMixin<

### deletedBy

Whenever any entry is deleted using deleteById, delete and deleteAll repository methods, it also sets deletedBy column with a value with user id whoever is logged in currently. Hence it uses a Getter function of IAuthUser type. However, if you want to use some other attribute of user model other than id, you can do it by sending extra options to repository methods - deleteById, delete and deleteAll. Here is an example.
Whenever any entry is deleted using deleteById, delete and deleteAll repository methods, it also sets deletedBy column with a value with user id whoever is logged in currently. Hence it uses a Getter function of IAuthUser type. However, if you want to use some other attribute of user model other than id, you can do it by overriding deletedByIdKey. Here is an example.

```ts
import {Getter, inject} from '@loopback/core';
Expand All @@ -212,27 +212,10 @@ export class UserRepository extends SoftCrudRepository<
@inject('datasources.pgdb') dataSource: PgdbDataSource,
@inject.getter(AuthenticationBindings.CURRENT_USER, {optional: true})
protected readonly getCurrentUser: Getter<IAuthUser | undefined>,
protected readonly deletedByIdKey: string = 'userTenantId',
) {
super(User, dataSource, getCurrentUser);
}

async delete(entity: User, options?: Options): Promise<void> {
return super.delete(entity, {
userIdentifierKey: 'userTenantId',
});
}

async deleteAll(where?: Where<User>, options?: Options): Promise<Count> {
return super.deleteAll(where, {
userIdentifierKey: 'userTenantId',
});
}

async deleteById(id: string, options?: Options): Promise<void> {
return super.deleteById(id, {
userIdentifierKey: 'userTenantId',
});
}
}
```

Expand Down
162 changes: 157 additions & 5 deletions src/__tests__/unit/mixin/soft-crud.mixin.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {expect} from '@loopback/testlab';

import {Constructor, Getter} from '@loopback/context';
import {
DefaultCrudRepository,
DefaultTransactionalRepository,
Entity,
EntityNotFoundError,
Expand All @@ -32,6 +33,16 @@ class Customer extends SoftDeleteEntity {
email: string;
}

@model()
class Customer2 extends SoftDeleteEntity {
@property({
id: true,
})
id: number;
@property()
email: string;
}

class CustomerCrudRepo extends SoftCrudRepositoryMixin<
Customer,
typeof Customer.prototype.id,
Expand All @@ -51,19 +62,45 @@ class CustomerCrudRepo extends SoftCrudRepositoryMixin<
}
}

class Customer2CrudRepo extends SoftCrudRepositoryMixin<
Customer,
typeof Customer.prototype.id,
Constructor<
DefaultCrudRepository<Customer, typeof Customer.prototype.id, {}>
>,
{}
>(DefaultCrudRepository) {
constructor(
entityClass: typeof Entity & {
prototype: Customer;
},
dataSource: juggler.DataSource,
readonly getCurrentUser: Getter<IAuthUser | undefined>,
readonly deletedByIdKey: string = 'id',
) {
super(entityClass, dataSource, getCurrentUser);
}
}

describe('SoftCrudRepositoryMixin', () => {
let repo: CustomerCrudRepo;
let repoWithCustomDeletedByKey: Customer2CrudRepo;
const userData = {
id: '1',
username: 'test',
};

before(() => {
const ds: juggler.DataSource = new juggler.DataSource({
name: 'db',
connector: 'memory',
});
repo = new CustomerCrudRepo(Customer, ds, () =>
Promise.resolve({
id: '1',
username: 'test',
}),
repo = new CustomerCrudRepo(Customer, ds, () => Promise.resolve(userData));
repoWithCustomDeletedByKey = new Customer2CrudRepo(
Customer2,
ds,
() => Promise.resolve(userData),
'username',
);
});

Expand Down Expand Up @@ -504,6 +541,56 @@ describe('SoftCrudRepositoryMixin', () => {
});
});

describe('deleteById', () => {
beforeEach(setupTestData);
afterEach(clearTestData);

it('should soft delete entries', async () => {
await repo.deleteById(1);
try {
await repo.findById(1);
fail();
} catch (e) {
expect(e.message).to.be.equal('EntityNotFound');
}
const afterDeleteIncludeSoftDeleted =
await repo.findByIdIncludeSoftDelete(1);
expect(afterDeleteIncludeSoftDeleted)
.to.have.property('email')
.equal('[email protected]');
});

it('should soft delete entries with deletedBy set to id', async () => {
await repo.deleteById(1);
try {
await repo.findById(1);
fail();
} catch (e) {
expect(e.message).to.be.equal('EntityNotFound');
}
const afterDeleteIncludeSoftDeleted =
await repo.findByIdIncludeSoftDelete(1);
expect(afterDeleteIncludeSoftDeleted)
.to.have.property('deletedBy')
.equal(userData.id);
});

it('should soft delete entries with deletedBy set to custom key provided', async () => {
await repoWithCustomDeletedByKey.deleteById(1);
try {
await repoWithCustomDeletedByKey.findById(1);
fail();
} catch (e) {
expect(e.message).to.be.equal('EntityNotFound');
}
const afterDeleteIncludeSoftDeleted =
await repoWithCustomDeletedByKey.findByIdIncludeSoftDelete(1);
expect(afterDeleteIncludeSoftDeleted)
.to.have.property('deletedBy')
.equal(userData.username);
});
});

describe('delete', () => {
beforeEach(setupTestData);
afterEach(clearTestData);
Expand All @@ -522,18 +609,73 @@ describe('SoftCrudRepositoryMixin', () => {
.to.have.property('email')
.equal('[email protected]');
});

it('should soft delete entries with deletedBy set to id', async () => {
const entity = await repo.findById(1);
await repo.delete(entity);
try {
await repo.findById(1);
fail();
} catch (e) {
expect(e.message).to.be.equal('EntityNotFound');
}
const afterDeleteIncludeSoftDeleted =
await repo.findByIdIncludeSoftDelete(1);
expect(afterDeleteIncludeSoftDeleted)
.to.have.property('deletedBy')
.equal(userData.id);
});

it('should soft delete entries with deletedBy set to custom key provided', async () => {
const entity = await repoWithCustomDeletedByKey.findById(1);
await repoWithCustomDeletedByKey.delete(entity);
try {
await repoWithCustomDeletedByKey.findById(1);
fail();
} catch (e) {
expect(e.message).to.be.equal('EntityNotFound');
}
const afterDeleteIncludeSoftDeleted =
await repoWithCustomDeletedByKey.findByIdIncludeSoftDelete(1);
expect(afterDeleteIncludeSoftDeleted)
.to.have.property('deletedBy')
.equal(userData.username);
});
});

describe('deleteAll', () => {
beforeEach(setupTestData);
afterEach(clearTestData);

it('should soft delete all entries', async () => {
await repo.deleteAll();
const customers = await repo.find();
expect(customers).to.have.length(0);
const afterDeleteAll = await repo.findAll();
expect(afterDeleteAll).to.have.length(4);
});

it('should soft delete entries with deletedBy set to id', async () => {
await repo.deleteAll();
const customers = await repo.find();
expect(customers).to.have.length(0);
const afterDeleteAll = await repo.findAll();
expect(afterDeleteAll).to.have.length(4);
afterDeleteAll.forEach((rec) => {
expect(rec).to.have.property('deletedBy').equal(userData.id);
});
});

it('should soft delete entries with deletedBy set to custom key provided', async () => {
await repoWithCustomDeletedByKey.deleteAll();
const customers = await repoWithCustomDeletedByKey.find();
expect(customers).to.have.length(0);
const afterDeleteAll = await repoWithCustomDeletedByKey.findAll();
expect(afterDeleteAll).to.have.length(4);
afterDeleteAll.forEach((rec) => {
expect(rec).to.have.property('deletedBy').equal(userData.username);
});
});
});

describe('deleteHard', () => {
Expand Down Expand Up @@ -571,9 +713,19 @@ describe('SoftCrudRepositoryMixin', () => {
await repo.create({id: 3, email: '[email protected]'});
await repo.create({id: 4, email: '[email protected]'});
await repo.deleteById(3);

await repoWithCustomDeletedByKey.create({id: 1, email: '[email protected]'});
await repoWithCustomDeletedByKey.create({id: 2, email: '[email protected]'});
await repoWithCustomDeletedByKey.create({
id: 3,
email: '[email protected]',
});
await repoWithCustomDeletedByKey.create({id: 4, email: '[email protected]'});
await repoWithCustomDeletedByKey.deleteById(3);
}

async function clearTestData() {
await repo.deleteAllHard();
await repoWithCustomDeletedByKey.deleteAllHard();
}
});
Loading

0 comments on commit 28de1cd

Please sign in to comment.