diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index eace20a..fe832f2 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -66,7 +66,7 @@ export abstract class MongooseRepository> async save( entity: S | PartialEntityWithIdAndOptionalDiscriminatorKey, - userId?: string | number, + userId?: string, ): Promise { if (!entity) throw new IllegalArgumentException('The given entity must be valid'); @@ -123,11 +123,11 @@ export abstract class MongooseRepository> private async insert( entity: S, - userId?: string | number, + userId?: string, ): Promise> { try { this.setDiscriminatorKeyOn(entity); - const document = this.assignUserId(entity, userId); + const document = this.createDocumentAndSetUserId(entity, userId); return (await document.save()) as HydratedDocument; } catch (error) { if (error.message.includes('duplicate key error')) { @@ -151,17 +151,17 @@ export abstract class MongooseRepository> } } - private assignUserId(entity: S, userId?: string | number) { - const tempEntity = new this.entityModel(entity); + private createDocumentAndSetUserId(entity: S, userId?: string) { + const document = new this.entityModel(entity); if (isAuditable(entity) && userId) { - tempEntity.$locals.userId = userId; + document.$locals.userId = userId; } - return tempEntity; + return document; } private async update( entity: PartialEntityWithId, - userId?: string | number, + userId?: string, ): Promise | null> { const document = await this.entityModel.findById>( entity.id, diff --git a/src/repository.ts b/src/repository.ts index 8bcb79c..3c161cf 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -4,11 +4,35 @@ import { Optional } from 'typescript-optional'; export type PartialEntityWithId = { id: string } & Partial; export interface Repository { + /** + * Find an entity by ID. + * @param id the ID of the entity. + * @returns an Optional specifying the entity or null. + * */ findById: (id: string) => Promise>; + + /** + * Find all entities. + * @param filters some combination of entity property values to filter the result. + * @param sortBy some entity property to sort the result. + * */ findAll: (filters?: any, sortBy?: any) => Promise; + + /** + * Save (insert or update) an entity. + * @param entity the entity to save. + * @param userId the ID of the user executing the action. + * @returns the saved version of the entity. + */ save: ( entity: S | PartialEntityWithId, - userId?: string | number, + userId?: string, ) => Promise; + + /** + * Delete an entity by ID. + * @param id the ID of the entity. + * @returns true if the entity was deleted, false otherwise. + */ deleteById: (id: string) => Promise; } diff --git a/src/util/audit.ts b/src/util/audit.ts index 7191623..98106d5 100644 --- a/src/util/audit.ts +++ b/src/util/audit.ts @@ -1,15 +1,15 @@ export interface Auditable { createdAt?: Date; - createdBy?: string | number; + createdBy?: string; updatedAt?: Date; - updatedBy?: string | number; + updatedBy?: string; } export abstract class AuditableClass implements Auditable { readonly createdAt?: Date; - readonly createdBy?: string | number; + readonly createdBy?: string; readonly updatedAt?: Date; - readonly updatedBy?: string | number; + readonly updatedBy?: string; constructor(entity: Auditable) { this.createdAt = entity.createdAt; diff --git a/src/util/schema.ts b/src/util/schema.ts index 601bebf..8f1c8c2 100644 --- a/src/util/schema.ts +++ b/src/util/schema.ts @@ -16,8 +16,8 @@ export const BaseSchema = new Schema( export const AuditableSchema = extendSchema( BaseSchema, { - createdBy: { type: String, required: false }, - updatedBy: { type: String, required: false }, + createdBy: { type: String }, + updatedBy: { type: String }, }, { timestamps: true }, ); diff --git a/test/repository/auditable.book.repository.test.ts b/test/repository/auditable.book.repository.test.ts index 520217f..d30e82f 100644 --- a/test/repository/auditable.book.repository.test.ts +++ b/test/repository/auditable.book.repository.test.ts @@ -7,156 +7,132 @@ import { } from '../util/mongo-server'; import { AuditableMongooseBookRepository } from './auditable.book.repository'; -describe('', () => { +describe('Given an instance of auditable book repository and a user ID', () => { let bookRepository: Repository; + const createdBy = '1234'; beforeAll(async () => { setupConnection(); bookRepository = new AuditableMongooseBookRepository(); }); - describe('Given an instance of auditable book repository', () => { - describe('when saving an auditable book', () => { - describe('that is new', () => { - describe('and that is of supertype AuditableBook', () => { - it('then the inserted book includes the expected audit data', async () => { - const bookToInsert = new AuditableBook({ - title: 'Continuous Delivery', - description: - 'Reliable Software Releases Through Build, Test, and Deployment Automation', - isbn: '9780321601919', - }); - - const auditableBook = await bookRepository.save( - bookToInsert, - '1234', - ); - expect(auditableBook.createdAt).toBeDefined(); - expect(auditableBook.updatedAt).toBeDefined(); - expect(auditableBook.createdBy).toBeDefined(); - expect(auditableBook.updatedBy).toBeDefined(); - }); + describe('when creating an auditable book', () => { + describe('that is of supertype AuditableBook', () => { + it('then the inserted book includes the expected audit data', async () => { + const bookToInsert = new AuditableBook({ + title: 'Continuous Delivery', + description: + 'Reliable Software Releases Through Build, Test, and Deployment Automation', + isbn: '9780321601919', }); - describe('and that is of a subtype of AuditableBook', () => { - it('then the inserted book includes the expected audit data', async () => { - const bookToInsert = new AuditablePaperBook({ - title: 'Implementing Domain-Driven Design', - description: 'Describes Domain-Driven Design in depth', - edition: 1, - isbn: '9780321834577', - }); - - const auditableBook = await bookRepository.save( - bookToInsert, - '1234', - ); - expect(auditableBook.createdAt).toBeDefined(); - expect(auditableBook.updatedAt).toBeDefined(); - expect(auditableBook.createdBy).toBeDefined(); - expect(auditableBook.updatedBy).toBeDefined(); - }); - }); + const auditableBook = await bookRepository.save( + bookToInsert, + createdBy, + ); + expect(auditableBook.createdAt).toBeDefined(); + expect(auditableBook.updatedAt).toBeDefined(); + expect(auditableBook.createdBy).toEqual(createdBy); + expect(auditableBook.updatedBy).toEqual(createdBy); }); + }); - describe('that is not new', () => { - let storedAuditableBook: AuditableBook; - - beforeEach(async () => { - const auditableBook = new AuditableBook({ - title: 'The Phoenix Project', - description: - 'Building and Scaling High Performing Technology Organizations', - isbn: '1942788339', - }); - storedAuditableBook = await bookRepository.save( - auditableBook, - '1234', - ); + describe('that is of a subtype of AuditableBook', () => { + it('then the inserted book includes the expected audit data', async () => { + const bookToInsert = new AuditablePaperBook({ + title: 'Implementing Domain-Driven Design', + description: 'Describes Domain-Driven Design in depth', + edition: 1, + isbn: '9780321834577', }); - describe('and that is of supertype AuditableBook', () => { - it('then the updated book includes the expected audit data', async () => { - const bookToUpdate = { - id: storedAuditableBook.id, - description: - 'A Novel About IT, DevOps, and Helping Your Business Win', - } as AuditableBook; - - const auditableBook = await bookRepository.save( - bookToUpdate, - '5678', - ); - expect(auditableBook.createdAt).toEqual( - storedAuditableBook.createdAt, - ); - expect(auditableBook.updatedAt?.getTime()).toBeGreaterThan( - storedAuditableBook.updatedAt!.getTime(), - ); - expect(auditableBook.createdBy).toEqual( - storedAuditableBook.createdBy, - ); - expect(auditableBook.updatedBy).not.toEqual( - storedAuditableBook.createdBy, - ); - expect(auditableBook.updatedBy).not.toEqual( - storedAuditableBook.updatedBy, - ); - }); - }); + const auditableBook = await bookRepository.save( + bookToInsert, + createdBy, + ); + expect(auditableBook.createdAt).toBeDefined(); + expect(auditableBook.updatedAt).toBeDefined(); + expect(auditableBook.createdBy).toEqual(createdBy); + expect(auditableBook.updatedBy).toEqual(createdBy); + }); + }); + }); - describe('and that is of a subtype of AuditableBook', () => { - let storedAuditablePaperBook: AuditablePaperBook; - - beforeEach(async () => { - const auditablePaperBook = new AuditablePaperBook({ - title: 'Effective Java', - description: 'Great book on the Java programming language', - edition: 3, - isbn: '0134685997', - }); - - storedAuditablePaperBook = await bookRepository.save( - auditablePaperBook, - '1234', - ); - }); - - it('then the updated book includes the expected audit data', async () => { - const bookToUpdate = { - id: storedAuditablePaperBook.id, - edition: 4, - } as AuditablePaperBook; - - const auditablePaperBook = await bookRepository.save( - bookToUpdate, - '5678', - ); - expect(auditablePaperBook.createdAt).toEqual( - storedAuditablePaperBook.createdAt, - ); - expect(auditablePaperBook.updatedAt?.getTime()).toBeGreaterThan( - storedAuditablePaperBook.updatedAt!.getTime(), - ); - expect(auditablePaperBook.createdBy).toEqual( - storedAuditablePaperBook.createdBy, - ); - expect(auditablePaperBook.updatedBy).not.toEqual( - storedAuditablePaperBook.createdBy, - ); - expect(auditablePaperBook.updatedBy).not.toEqual( - storedAuditablePaperBook.updatedBy, - ); - }); - }); + describe('when updating an auditable book', () => { + let storedAuditableBook: AuditableBook; + const updatedBy = '5678'; + + beforeEach(async () => { + const auditableBook = new AuditableBook({ + title: 'The Phoenix Project', + description: + 'Building and Scaling High Performing Technology Organizations', + isbn: '1942788339', + }); + storedAuditableBook = await bookRepository.save(auditableBook, createdBy); + }); - afterEach(async () => { - await deleteAll('auditablebooks'); + describe('that is of supertype AuditableBook', () => { + it('then the updated book includes the expected audit data', async () => { + const bookToUpdate = { + id: storedAuditableBook.id, + description: + 'A Novel About IT, DevOps, and Helping Your Business Win', + } as AuditableBook; + + const auditableBook = await bookRepository.save(bookToUpdate, '5678'); + expect(auditableBook.createdAt).toEqual(storedAuditableBook.createdAt); + expect(auditableBook.updatedAt?.getTime()).toBeGreaterThan( + storedAuditableBook.updatedAt!.getTime(), + ); + expect(auditableBook.createdBy).toEqual(createdBy); + expect(auditableBook.updatedBy).toEqual(updatedBy); + }); + }); + + describe('that is of a subtype of AuditableBook', () => { + let storedAuditablePaperBook: AuditablePaperBook; + + beforeEach(async () => { + const auditablePaperBook = new AuditablePaperBook({ + title: 'Effective Java', + description: 'Great book on the Java programming language', + edition: 3, + isbn: '0134685997', }); + + storedAuditablePaperBook = await bookRepository.save( + auditablePaperBook, + createdBy, + ); + }); + + it('then the updated book includes the expected audit data', async () => { + const bookToUpdate = { + id: storedAuditablePaperBook.id, + edition: 4, + } as AuditablePaperBook; + + const auditablePaperBook = await bookRepository.save( + bookToUpdate, + updatedBy, + ); + expect(auditablePaperBook.createdAt).toEqual( + storedAuditablePaperBook.createdAt, + ); + expect(auditablePaperBook.updatedAt?.getTime()).toBeGreaterThan( + storedAuditablePaperBook.updatedAt!.getTime(), + ); + expect(auditablePaperBook.createdBy).toEqual(createdBy); + expect(auditablePaperBook.updatedBy).toEqual(updatedBy); }); }); }); + afterEach(async () => { + await deleteAll('auditablebooks'); + }); + afterAll(async () => { await closeMongoConnection(); });