From 4604bc6c1481c42bb46958af48e2b88849d671ee Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 1 Jan 2024 18:59:42 +0100 Subject: [PATCH 01/48] feat: start saveAll development Created saveAll test case list draft. Also added Mongo transaction handling (required update from standalone Mongo to replica set for testing purposes) and fixtures to aid in testing. --- .../test/util/mongo-server.ts | 15 +- src/index.ts | 9 +- src/mongoose.repository.ts | 146 +++++-- src/repository.ts | 28 +- src/util/transaction.ts | 36 ++ test/domain/book.fixtures.ts | 50 +++ test/repository/book.repository.test.ts | 402 +++++++++++++----- test/util/mongo-server.ts | 17 +- yarn.lock | 12 +- 9 files changed, 543 insertions(+), 172 deletions(-) create mode 100644 src/util/transaction.ts create mode 100644 test/domain/book.fixtures.ts diff --git a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts index ff1a61a..50d3e94 100644 --- a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts +++ b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts @@ -1,23 +1,20 @@ -import { MongoMemoryServer } from 'mongodb-memory-server'; import { MongooseModule, MongooseModuleOptions } from '@nestjs/mongoose'; +import { MongoMemoryReplSet } from 'mongodb-memory-server'; import mongoose from 'mongoose'; import { Entity } from '../../../../src'; -let mongoServer: MongoMemoryServer; +let mongoServer: MongoMemoryReplSet; let dbName: string; export const rootMongooseTestModule = ( options: MongooseModuleOptions = {}, - port = 27016, dbName = 'book-repository', ) => MongooseModule.forRootAsync({ useFactory: async () => { - mongoServer = await MongoMemoryServer.create({ - instance: { - port, - dbName: dbName, - }, + mongoServer = await MongoMemoryReplSet.create({ + instanceOpts: [{ port: 27016 }], + replSet: { dbName, count: 1 }, }); const mongoUri = mongoServer.getUri(); return { @@ -55,7 +52,7 @@ export const deleteAll = async (collections: string[]) => { const setupConnection = async () => { if (!mongoServer) return; await mongoose.connect(mongoServer.getUri()); - await mongoose.connection.useDb(dbName); + mongoose.connection.useDb(dbName); }; export const closeMongoConnection = async () => { diff --git a/src/index.ts b/src/index.ts index 6fa6fb1..ff220d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ import { Constructor, - ConstructorMap, MongooseRepository, + TypeData, + TypeMap, } from './mongoose.repository'; import { PartialEntityWithId, Repository } from './repository'; import { Auditable, AuditableClass, isAuditable } from './util/audit'; @@ -12,6 +13,7 @@ import { ValidationException, } from './util/exceptions'; import { AuditableSchema, BaseSchema, extendSchema } from './util/schema'; +import { DbCallback, runInTransaction } from './util/transaction'; export { Auditable, @@ -19,14 +21,17 @@ export { AuditableSchema, BaseSchema, Constructor, - ConstructorMap, + DbCallback, Entity, IllegalArgumentException, MongooseRepository, PartialEntityWithId, Repository, + TypeData, + TypeMap, UndefinedConstructorException, ValidationException, extendSchema, isAuditable, + runInTransaction, }; diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index 589b707..7683d8f 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -1,4 +1,5 @@ import mongoose, { + ClientSession, Connection, HydratedDocument, Model, @@ -15,37 +16,86 @@ import { ValidationException, } from './util/exceptions'; import { SearchOptions } from './util/search-options'; +import { runInTransaction } from './util/transaction'; /** - * Models a persistable domain object constructor function. + * Models a domain object instance constructor. */ -export type Constructor = new (...args: any) => T; +export type Constructor = new (...args: any) => T; /** - * Models a persistable domain object constructor map. + * Models some domain object type data. */ -export interface ConstructorMap { - [index: string]: { type: Constructor; schema: Schema }; +export type TypeData = { + type: Constructor; + schema: Schema; +}; + +/** + * Models a map of domain object types supported by a custom repository. + */ +export interface TypeMap { + [type: string]: TypeData; +} + +class InnerTypeMap { + readonly types: string[]; + readonly data: TypeData[]; + + constructor(map: TypeMap) { + this.types = Object.keys(map); + this.data = Object.values(map); + } + + get(type: string): TypeData | undefined { + const index = this.types.indexOf(type); + return index !== -1 ? this.data[index] : undefined; + } + + getSupertypeData(): TypeData { + return this.get('Default')!; + } + + getSupertypeName(): string { + return this.getSupertypeData().type.name; + } + + getSubtypesData(): TypeData[] { + const subtypeData: TypeData[] = []; + for (const key of this.types) { + const value = this.get(key); + if (value && key !== 'Default') { + subtypeData.push(value); + } + } + return subtypeData; + } + + has(type: string): boolean { + return type === this.getSupertypeName() || this.types.indexOf(type) !== -1; + } } /** - * Abstract implementation of the {@link Repository} interface for MongoDB using Mongoose. + * Abstract Mongoose-based implementation of the {@link Repository} interface. */ export abstract class MongooseRepository> implements Repository { + private readonly typeMap: InnerTypeMap; protected readonly entityModel: Model; /** * Sets up the underlying configuration to enable Mongoose operation execution. - * @param {ConstructorMap} entityConstructorMap a map with all the persistable domain object types. + * @param {TypeMap} typeMap a map of domain object types supported by this repository. * @param {Connection=} connection (optional) a Mongoose connection to an instance of MongoDB. */ protected constructor( - private readonly entityConstructorMap: ConstructorMap, + typeMap: TypeMap, protected readonly connection?: Connection, ) { - this.entityModel = this.createEntityModel(entityConstructorMap, connection); + this.typeMap = new InnerTypeMap(typeMap); + this.entityModel = this.createEntityModel(connection); } /** @inheritdoc */ @@ -103,6 +153,7 @@ export abstract class MongooseRepository> async save( entity: S | PartialEntityWithId, userId?: string, + session?: ClientSession, ): Promise { if (!entity) throw new IllegalArgumentException('The given entity must be valid'); @@ -111,7 +162,11 @@ export abstract class MongooseRepository> if (!entity.id) { document = await this.insert(entity as S, userId); } else { - document = await this.update(entity as PartialEntityWithId, userId); + document = await this.update( + entity as PartialEntityWithId, + userId, + session, + ); } if (document) return this.instantiateFrom(document) as S; throw new IllegalArgumentException( @@ -131,44 +186,57 @@ export abstract class MongooseRepository> } } + /** @inheritdoc */ + async saveAll( + entities: S[] | PartialEntityWithId[], + userId?: string, + ): Promise { + return await runInTransaction( + async (session: ClientSession) => + await Promise.all( + entities.map( + async (entity) => await this.save(entity, userId, session), + ), + ), + ); + } + /** * Instantiates a persistable domain object from the given Mongoose Document. - * @param document the given Mongoose Document. - * @returns the resulting persistable domain object instance. + * @param {HydratedDocument | null} document the given Mongoose Document. + * @returns {S | null} the resulting persistable domain object instance. * @throws {UndefinedConstructorException} if there is no constructor available. */ protected instantiateFrom( document: HydratedDocument | null, ): S | null { if (!document) return null; - const discriminatorType = document.get('__t'); - const entityConstructor = - this.entityConstructorMap[discriminatorType ?? 'Default'].type; - if (entityConstructor) { - return new entityConstructor(document.toObject()) as S; + const entityKey = document.get('__t') ?? 'Default'; + const constructor = this.typeMap.get(entityKey)?.type; + if (constructor) { + return new constructor(document.toObject()) as S; } throw new UndefinedConstructorException( `There is no registered instance constructor for the document with ID ${document.id}`, ); } - private createEntityModel( - entityConstructorMap: ConstructorMap, - connection?: Connection, - ) { + private createEntityModel(connection?: Connection) { let entityModel; - const supertypeName = entityConstructorMap['Default'].type.name; - const supertypeSchema = entityConstructorMap['Default'].schema; + const supertypeData = this.typeMap.getSupertypeData(); if (connection) { - entityModel = connection.model(supertypeName, supertypeSchema); + entityModel = connection.model( + supertypeData.type.name, + supertypeData.schema, + ); } else { - entityModel = mongoose.model(supertypeName, supertypeSchema); + entityModel = mongoose.model( + supertypeData.type.name, + supertypeData.schema, + ); } - for (const subtypeName in entityConstructorMap) { - if (!(subtypeName === 'Default')) { - const subtypeSchema = entityConstructorMap[subtypeName].schema; - entityModel.discriminator(subtypeName, subtypeSchema); - } + for (const subtypeData of this.typeMap.getSubtypesData()) { + entityModel.discriminator(subtypeData.type.name, subtypeData.schema); } return entityModel; } @@ -177,6 +245,12 @@ export abstract class MongooseRepository> entity: S, userId?: string, ): Promise> { + const entityClassName = entity['constructor']['name']; + if (!this.typeMap.has(entityClassName)) { + throw new IllegalArgumentException( + `The entity with name ${entityClassName} is not included in the setup of the custom repository`, + ); + } this.setDiscriminatorKeyOn(entity); const document = this.createDocumentAndSetUserId(entity, userId); return (await document.save()) as HydratedDocument; @@ -186,10 +260,9 @@ export abstract class MongooseRepository> entity: S | PartialEntityWithId, ): void { const entityClassName = entity['constructor']['name']; + const isSubtype = entityClassName !== this.typeMap.getSupertypeName(); const hasEntityDiscriminatorKey = '__t' in entity; - const isEntitySupertype = - entityClassName !== this.entityConstructorMap['Default'].type.name; - if (!hasEntityDiscriminatorKey && isEntitySupertype) { + if (isSubtype && !hasEntityDiscriminatorKey) { entity['__t'] = entityClassName; } } @@ -206,10 +279,11 @@ export abstract class MongooseRepository> private async update( entity: PartialEntityWithId, userId?: string, + session?: ClientSession, ): Promise | null> { - const document = await this.entityModel.findById>( - entity.id, - ); + const document = await this.entityModel + .findById>(entity.id) + .session(session ?? null); if (document) { document.set(entity); document.isNew = false; diff --git a/src/repository.ts b/src/repository.ts index bc30d29..7c26eda 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -16,7 +16,7 @@ export type PartialEntityWithId = { */ export interface Repository { /** - * Find an entity by ID. + * Finds an entity by ID. * @param {string} id the ID of the entity. * @returns {Promise>} the entity or null. * @throws {IllegalArgumentException} if the given `id` is `undefined` or `null`. @@ -24,7 +24,7 @@ export interface Repository { findById: (id: string) => Promise>; /** - * Find all entities. + * Finds all entities. * @param {SearchOptions} options (optional) the desired search options (i.e., field filters, sorting, and pagination data). * @returns {Promise} all entities. * @throws {IllegalArgumentException} if the given `options` specifies an invalid parameter. @@ -32,13 +32,13 @@ export interface Repository { findAll: (options?: SearchOptions) => Promise; /** - * Save (insert or update) an entity. + * Saves (insert or update) an entity. * @param {S | PartialEntityWithId} entity the entity to save. * @param {string} userId (optional) the ID of the user executing the action. - * @returns {Promise} the saved version of the entity. - * @throws {IllegalArgumentException} if the given `entity` is `undefined` or `null` or + * @returns {Promise} the saved entity. + * @throws {IllegalArgumentException} if the given entity is `undefined` or `null` or * specifies an `id` not matching any existing entity. - * @throws {ValidationException} if the given `entity` specifies a field with some invalid value. + * @throws {ValidationException} if the given entity specifies a field with some invalid value. */ save: ( entity: S | PartialEntityWithId, @@ -46,7 +46,21 @@ export interface Repository { ) => Promise; /** - * Delete an entity by ID. + * Saves (insert or update) a list of entities. + * @param {S[] | PartialEntityWithId[]} entities the list of entities to save. + * @param {string} userId (optional) the ID of the user executing the action. + * @returns {Promise} the list of saved entities. + * @throws {IllegalArgumentException} if any of the given entities is `undefined` or `null` or + * specifies an `id` not matching any existing entity. + * @throws {ValidationException} if any of the given entities specifies a field with some invalid value. + */ + saveAll: ( + entities: S[] | PartialEntityWithId[], + userId?: string, + ) => Promise; + + /** + * Deletes an entity by ID. * @param {string} id the ID of the entity. * @returns {Promise} `true` if the entity was deleted, `false` otherwise. * @throws {IllegalArgumentException} if the given `id` is `undefined` or `null`. diff --git a/src/util/transaction.ts b/src/util/transaction.ts new file mode 100644 index 0000000..90a812c --- /dev/null +++ b/src/util/transaction.ts @@ -0,0 +1,36 @@ +import mongoose, { ClientSession, Connection } from 'mongoose'; + +/** + * Models a callback function that writes to and reads from the database using a session. + */ +export type DbCallback = (session: ClientSession) => Promise; + +/** + * Runs the provided callback function within a transaction and commits the changes to the database + * iff it has run successfully. + * + * @param {DbCallback} callback a callback function that writes to and reads from the database using a session. + * @param {Connection=} connection (optional) a Mongoose connection to create the session from. + */ +export async function runInTransaction( + callback: DbCallback, + connection?: Connection, +): Promise { + let session: ClientSession; + if (connection) { + session = await connection.startSession(); + } else { + session = await mongoose.startSession(); + } + session.startTransaction(); + try { + const result = await callback(session); + await session.commitTransaction(); + return result; + } catch (error) { + await session.abortTransaction(); + throw error; + } finally { + session.endSession(); + } +} diff --git a/test/domain/book.fixtures.ts b/test/domain/book.fixtures.ts new file mode 100644 index 0000000..20106d4 --- /dev/null +++ b/test/domain/book.fixtures.ts @@ -0,0 +1,50 @@ +import { AudioBook, Book, ElectronicBook, PaperBook } from './book'; + +export const bookFixture = (book: Partial = {}, id?: string): Book => { + const defaultBook = { + title: 'Accelerate', + description: 'Building High Performing Technology Organizations', + isbn: '1942788339', + }; + return new Book({ ...defaultBook, ...book, id }); +}; + +export const paperBookFixture = ( + book: Partial = {}, + id?: string, +): PaperBook => { + const defaultBook = { + title: 'Effective Java', + description: 'Great book to learn Java', + isbn: '0134685997', + edition: 3, + }; + return new PaperBook({ ...defaultBook, ...book, id }); +}; + +export const audioBookFixture = ( + book: Partial = {}, + id?: string, +): AudioBook => { + const defaultBook = { + title: 'The Phoenix Project', + description: 'Best IT novel ever', + isbn: '5573899870', + hostingPlatforms: ['Audible'], + format: 'MP3', + }; + return new AudioBook({ ...defaultBook, ...book, id }); +}; + +export const electronicBookFixture = ( + book: Partial = {}, + id?: string, +): ElectronicBook => { + const defaultBook = { + title: 'Clean Code', + description: 'Pragmatic tips to ensure that your code is readable', + isbn: '6875234013', + extension: 'epub', + }; + return new ElectronicBook({ ...defaultBook, ...book, id }); +}; diff --git a/test/repository/book.repository.test.ts b/test/repository/book.repository.test.ts index 81a497b..14f9c28 100644 --- a/test/repository/book.repository.test.ts +++ b/test/repository/book.repository.test.ts @@ -3,12 +3,20 @@ import { IllegalArgumentException, ValidationException, } from '../../src/util/exceptions'; -import { AudioBook, Book, ElectronicBook, PaperBook } from '../domain/book'; +import { AudioBook, Book, PaperBook } from '../domain/book'; +import { + audioBookFixture, + bookFixture, + electronicBookFixture, + paperBookFixture, +} from '../domain/book.fixtures'; import { closeMongoConnection, deleteAll, findById, + findOne, insert, + setupConnection, } from '../util/mongo-server'; import { BookRepository, MongooseBookRepository } from './book.repository'; @@ -19,55 +27,10 @@ describe('Given an instance of book repository', () => { let storedAudioBook: AudioBook; beforeAll(async () => { + setupConnection(); bookRepository = new MongooseBookRepository(); }); - beforeEach(async () => { - const bookToStore = new Book({ - title: 'Accelerate', - description: - 'Building and Scaling High Performing Technology Organizations', - isbn: '1942788339', - }); - const storedBookId = await insert(bookToStore, 'books'); - storedBook = new Book({ - ...bookToStore, - id: storedBookId, - }); - - const paperBookToStore = new PaperBook({ - title: 'Effective Java', - description: 'Great book on the Java programming language', - edition: 3, - isbn: '0134685997', - }); - const storedPaperBookId = await insert( - paperBookToStore, - 'books', - PaperBook.name, - ); - storedPaperBook = new PaperBook({ - ...paperBookToStore, - id: storedPaperBookId, - }); - - const audioBookToStore = new AudioBook({ - title: 'The Sandman', - description: 'Fantastic fantasy audio book', - hostingPlatforms: ['Audible'], - isbn: '5573899870', - }); - const storedAudioBookId = await insert( - audioBookToStore, - 'books', - AudioBook.name, - ); - storedAudioBook = new AudioBook({ - ...audioBookToStore, - id: storedAudioBookId, - }); - }); - describe('when searching a book by ID', () => { describe('by an undefined ID', () => { it('then throws an exception', async () => { @@ -93,6 +56,23 @@ describe('Given an instance of book repository', () => { }); describe('by the ID of an existent book', () => { + beforeEach(async () => { + const paperBookToStore = paperBookFixture(); + const storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + }); + + afterEach(async () => { + await deleteAll('books'); + }); + it('then retrieves the book', async () => { const book = await bookRepository.findById(storedPaperBook.id!); expect(book.isPresent()).toBe(true); @@ -126,6 +106,19 @@ describe('Given an instance of book repository', () => { }); describe('and there is one book matching the given search value', () => { + beforeEach(async () => { + const bookToStore = bookFixture(); + const storedBookId = await insert(bookToStore, 'books'); + storedBook = new Book({ + ...bookToStore, + id: storedBookId, + }); + }); + + afterEach(async () => { + await deleteAll('books'); + }); + it('then returns a book matching the given search value', async () => { const book = await bookRepository.findByIsbn(storedBook.isbn); expect(book.isPresent()).toBe(true); @@ -135,6 +128,41 @@ describe('Given an instance of book repository', () => { }); describe('when searching books', () => { + beforeEach(async () => { + const bookToStore = bookFixture(); + const storedBookId = await insert(bookToStore, 'books'); + storedBook = new Book({ + ...bookToStore, + id: storedBookId, + }); + + const paperBookToStore = paperBookFixture(); + const storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + + const audioBookToStore = audioBookFixture(); + const storedAudioBookId = await insert( + audioBookToStore, + 'books', + AudioBook.name, + ); + storedAudioBook = new AudioBook({ + ...audioBookToStore, + id: storedAudioBookId, + }); + }); + + afterEach(async () => { + await deleteAll('books'); + }); + describe('and not providing any optional parameter', () => { it('then retrieves a list with all books', async () => { const books = await bookRepository.findAll(); @@ -829,13 +857,10 @@ describe('Given an instance of book repository', () => { describe('when saving a book', () => { describe('that has not been registered as a Mongoose discriminator', () => { it('throws an exception', async () => { - const bookToInsert = new ElectronicBook({ - title: 'How to deal with ants at home?', - description: 'Shows several strategies to avoid having ants at home', - extension: 'epub', - isbn: '6875234013', - }); - await expect(bookRepository.save(bookToInsert)).rejects.toThrowError(); + const bookToInsert = electronicBookFixture(); + await expect(bookRepository.save(bookToInsert)).rejects.toThrowError( + IllegalArgumentException, + ); }); }); @@ -860,13 +885,14 @@ describe('Given an instance of book repository', () => { describe('and that is of supertype Book', () => { describe('and specifies an ID', () => { it('then throws an exception', async () => { - const bookToInsert = new Book({ - id: '00007032a61c4eda79230000', - title: 'Continuous Delivery', - description: - 'Reliable Software Releases Through Build, Test, and Deployment Automation', - isbn: '9780321601919', - }); + const bookToInsert = bookFixture( + { + title: 'Modern Software Engineering', + description: 'Build Better Software Faster', + isbn: '9780321601919', + }, + '00007032a61c4eda79230000', + ); await expect( bookRepository.save(bookToInsert), @@ -877,11 +903,10 @@ describe('Given an instance of book repository', () => { describe('and does not specify an ID', () => { describe('and some field values are invalid', () => { it('then throws an exception', async () => { - const bookToInsert = new Book({ - title: 'Continuous Delivery', - description: - 'Reliable Software Releases Through Build, Test, and Deployment Automation', - isbn: undefined as unknown as string, + const bookToInsert = bookFixture({ + title: 'Modern Software Engineering', + description: 'Build Better Software Faster', + isbn: undefined, }); await expect( @@ -892,10 +917,9 @@ describe('Given an instance of book repository', () => { describe('and all field values are valid', () => { it('then inserts the book', async () => { - const bookToInsert = new Book({ - title: 'Continuous Delivery', - description: - 'Reliable Software Releases Through Build, Test, and Deployment Automation', + const bookToInsert = bookFixture({ + title: 'Modern Software Engineering', + description: 'Build Better Software Faster', isbn: '9780321601919', }); @@ -911,11 +935,10 @@ describe('Given an instance of book repository', () => { describe('and that is of a subtype of Book', () => { describe('and some field values are invalid', () => { it('then throws an exception', async () => { - const bookToInsert = new PaperBook({ + const bookToInsert = paperBookFixture({ title: 'Implementing Domain-Driven Design', description: 'Describes Domain-Driven Design in depth', - edition: undefined as unknown as number, - isbn: '9780321834577', + isbn: undefined, }); await expect( @@ -926,11 +949,10 @@ describe('Given an instance of book repository', () => { describe('and all field values are valid', () => { it('then inserts the book', async () => { - const bookToInsert = new PaperBook({ + const bookToInsert = paperBookFixture({ title: 'Implementing Domain-Driven Design', description: 'Describes Domain-Driven Design in depth', - edition: 1, - isbn: '9780321834577', + isbn: '0134685998', }); const book = await bookRepository.save(bookToInsert); @@ -944,7 +966,20 @@ describe('Given an instance of book repository', () => { }); describe('that is not new', () => { - describe('and that is of Book supertype', () => { + describe('and that is of supertype Book', () => { + beforeEach(async () => { + const bookToStore = bookFixture(); + const storedBookId = await insert(bookToStore, 'books'); + storedBook = new Book({ + ...bookToStore, + id: storedBookId, + }); + }); + + afterEach(async () => { + await deleteAll('books'); + }); + describe('and that specifies partial contents of the supertype', () => { describe('and some field values are invalid', () => { it('then throws an exception', async () => { @@ -978,13 +1013,14 @@ describe('Given an instance of book repository', () => { }); describe('and that specifies all the contents of the supertype', () => { it('then updates the book', async () => { - const bookToUpdate = new Book({ - id: storedBook.id, - title: 'The Phoenix Project', - description: - 'A Novel About IT, DevOps, and Helping Your Business Win', - isbn: '1942788290', - }); + const bookToUpdate = bookFixture( + { + title: 'Continuous Delivery', + description: + 'Boost your development productivity via automation', + }, + storedBook.id, + ); const book = await bookRepository.save(bookToUpdate); expect(book.id).toBe(bookToUpdate.id); @@ -994,7 +1030,35 @@ describe('Given an instance of book repository', () => { }); }); - describe('and that is of Book subtype', () => { + describe('and that is of a subtype of Book', () => { + beforeEach(async () => { + const paperBookToStore = paperBookFixture(); + const storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + + const audioBookToStore = audioBookFixture(); + const storedAudioBookId = await insert( + audioBookToStore, + 'books', + AudioBook.name, + ); + storedAudioBook = new AudioBook({ + ...audioBookToStore, + id: storedAudioBookId, + }); + }); + + afterEach(async () => { + await deleteAll('books'); + }); + describe('and that specifies partial contents of the subtype', () => { describe('and some field values are invalid', () => { it('then throws an exception', async () => { @@ -1031,14 +1095,14 @@ describe('Given an instance of book repository', () => { describe('and that specifies all the contents of the subtype', () => { describe('and some field values are invalid', () => { it('then throws an exception', async () => { - const bookToUpdate = new AudioBook({ - id: storedAudioBook.id, - title: 'Don Quixote', - description: 'Important classic in Spanish literature', - hostingPlatforms: undefined as unknown as string[], - format: 'mp3', - isbn: '0142437239', - }); + const bookToUpdate = audioBookFixture( + { + title: 'The Pragmatic Programmer', + description: 'This book is a jewel for developers', + hostingPlatforms: undefined, + }, + storedAudioBook.id, + ); await expect( bookRepository.save(bookToUpdate), @@ -1048,14 +1112,13 @@ describe('Given an instance of book repository', () => { describe('and all field values are valid', () => { it('then updates the book', async () => { - const bookToUpdate = new AudioBook({ - id: storedAudioBook.id, - title: 'Don Quixote', - description: 'Important classic in Spanish literature', - hostingPlatforms: ['Spotify'], - format: 'mp3', - isbn: '0142437239', - }); + const bookToUpdate = audioBookFixture( + { + title: 'The Pragmatic Programmer', + description: 'This book is a jewel for developers', + }, + storedAudioBook.id, + ); const book = await bookRepository.save(bookToUpdate); expect(book.id).toBe(bookToUpdate.id); @@ -1073,6 +1136,126 @@ describe('Given an instance of book repository', () => { }); }); + describe('when saving a list of books', () => { + describe('that is empty', () => { + it('then returns an empty list of books', async () => { + const books = await bookRepository.saveAll([]); + + expect(books).toEqual([]); + }); + }); + + describe('that includes a book that has not been registered as a Mongoose discriminator', () => { + it('throws an exception', async () => { + const booksToInsert = [ + bookFixture({ isbn: '1942788340' }), + electronicBookFixture({ isbn: '1942788341' }), + ]; + await expect( + bookRepository.saveAll(booksToInsert), + ).rejects.toThrowError(IllegalArgumentException); + expect(await findOne({}, 'books')).toBeNull(); + }); + }); + + describe('that includes books that have been registered as a Mongoose discriminator', () => { + // describe('and one book is undefined', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and one book is null', () => { + // it('throws an exception', async () => {}); + // }); + + describe('and all books are new', () => { + // describe('and all books are of the same type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => {}); + // }); + + describe('and each book is of a different type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + describe('and all books specify valid field values', () => { + afterEach(async () => { + await deleteAll('books'); + }); + + it('then inserts the books', async () => { + const booksToInsert = [ + bookFixture({ isbn: '1942788342' }), + paperBookFixture({ isbn: '1942788343' }), + ]; + const savedBooks = await bookRepository.saveAll(booksToInsert); + expect(savedBooks.length).toBe(2); + }); + }); + }); + }); + + // describe('and none of the books is new', () => { + // describe('and all books are of the same type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => {}); + // }); + + // describe('and each book is of a different type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => {}); + // }); + // }); + + // describe('and one book is new while another is not new', () => { + // describe('and all books are of the same type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => { + // describe('and the book to insert has a partial content', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and the book to insert has a complete content', () => { + // describe('and the book to update has a partial content', () => {}); + + // describe('and the book to update has a complete content', () => {}); + // }); + // }); + // }); + + // describe('and each book is of a different type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => { + // describe('and the book to insert has a partial content', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and the book to insert has a complete content', () => { + // describe('and the book to update has a partial content', () => {}); + + // describe('and the book to update has a complete content', () => {}); + // }); + // }); + // }); + // }); + }); + }); + describe('when deleting a book', () => { describe('by an undefined ID', () => { it('then throws an exception', async () => { @@ -1100,6 +1283,19 @@ describe('Given an instance of book repository', () => { }); describe('by the ID of an existent book', () => { + beforeEach(async () => { + const bookToStore = bookFixture(); + const storedBookId = await insert(bookToStore, 'books'); + storedBook = new Book({ + ...bookToStore, + id: storedBookId, + }); + }); + + afterEach(async () => { + await deleteAll('books'); + }); + it('then returns true and the book has been effectively deleted', async () => { const isDeleted = await bookRepository.deleteById(storedBook.id!); expect(isDeleted).toBe(true); @@ -1108,10 +1304,6 @@ describe('Given an instance of book repository', () => { }); }); - afterEach(async () => { - await deleteAll('books'); - }); - afterAll(async () => { await closeMongoConnection(); }); diff --git a/test/util/mongo-server.ts b/test/util/mongo-server.ts index 87840c3..71f31e9 100644 --- a/test/util/mongo-server.ts +++ b/test/util/mongo-server.ts @@ -1,9 +1,9 @@ -import { MongoMemoryServer } from 'mongodb-memory-server'; +import { MongoMemoryReplSet } from 'mongodb-memory-server'; import mongoose from 'mongoose'; import { Entity } from '../../src'; const dbName = 'test'; -let mongoServer: MongoMemoryServer; +let mongoServer: MongoMemoryReplSet; type EntityWithOptionalDiscriminatorKey = Entity & { __t?: string }; @@ -23,6 +23,11 @@ export const insert = async ( .then((result) => result.insertedId.toString()); }; +export const findOne = async (filter: any, collection: string) => { + await setupConnection(); + return await mongoose.connection.db.collection(collection).findOne(filter); +}; + export const findById = async (id: string, collection: string) => { await setupConnection(); return await mongoose.connection.db @@ -43,12 +48,10 @@ export const closeMongoConnection = async () => { export const setupConnection = async () => { if (!mongoServer) { - mongoServer = await MongoMemoryServer.create({ - instance: { - dbName: dbName, - }, + mongoServer = await MongoMemoryReplSet.create({ + replSet: { dbName, count: 1 }, }); await mongoose.connect(mongoServer.getUri()); - await mongoose.connection.useDb(dbName); + mongoose.connection.useDb(dbName); } }; diff --git a/yarn.lock b/yarn.lock index 0064352..cada2b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2353,14 +2353,14 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001464: - version "1.0.30001488" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz#d19d7b6e913afae3e98f023db97c19e9ddc5e91f" - integrity sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ== + version "1.0.30001572" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz" + integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw== caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503: - version "1.0.30001513" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz#382fe5fbfb0f7abbaf8c55ca3ac71a0307a752e9" - integrity sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww== + version "1.0.30001572" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz" + integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw== chalk@5.3.0: version "5.3.0" From b4e22d90626d9dfc8fa1aec6ed9f93526119a5d9 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Thu, 4 Jan 2024 18:42:28 +0100 Subject: [PATCH 02/48] fix: add missing session and connection references Required to enable transaction at saveAll. --- src/mongoose.repository.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index 7683d8f..0539da4 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -160,7 +160,7 @@ export abstract class MongooseRepository> try { let document; if (!entity.id) { - document = await this.insert(entity as S, userId); + document = await this.insert(entity as S, userId, session); } else { document = await this.update( entity as PartialEntityWithId, @@ -198,6 +198,7 @@ export abstract class MongooseRepository> async (entity) => await this.save(entity, userId, session), ), ), + this.connection, ); } @@ -244,6 +245,7 @@ export abstract class MongooseRepository> private async insert( entity: S, userId?: string, + session?: ClientSession, ): Promise> { const entityClassName = entity['constructor']['name']; if (!this.typeMap.has(entityClassName)) { @@ -253,7 +255,7 @@ export abstract class MongooseRepository> } this.setDiscriminatorKeyOn(entity); const document = this.createDocumentAndSetUserId(entity, userId); - return (await document.save()) as HydratedDocument; + return (await document.save({ session })) as HydratedDocument; } private setDiscriminatorKeyOn( From 73283708f41a0fcc4baa5bff628436e57a5dfb35 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Thu, 4 Jan 2024 18:46:04 +0100 Subject: [PATCH 03/48] chore: switch to MongoDB replica set --- .../docker-compose.yml | 7 ++++++- .../mongo_scripts/dbstart.sh | 7 +++++++ .../mongo_scripts/rs-init.sh | 21 +++++++++++++++++++ .../nestjs-mongoose-book-manager/package.json | 2 +- .../src/app.module.ts | 9 +++++--- 5 files changed, 41 insertions(+), 5 deletions(-) create mode 100755 examples/nestjs-mongoose-book-manager/mongo_scripts/dbstart.sh create mode 100755 examples/nestjs-mongoose-book-manager/mongo_scripts/rs-init.sh diff --git a/examples/nestjs-mongoose-book-manager/docker-compose.yml b/examples/nestjs-mongoose-book-manager/docker-compose.yml index 020e35f..978054c 100644 --- a/examples/nestjs-mongoose-book-manager/docker-compose.yml +++ b/examples/nestjs-mongoose-book-manager/docker-compose.yml @@ -2,8 +2,13 @@ version: "3.3" services: mongodb: + container_name: mongodb image: mongo:latest environment: - MONGODB_DATABASE="book-repository" ports: - - "27016:27017" \ No newline at end of file + - "27016:27017" + volumes: + - ~/mongors/data1:/data/db + - ./mongo_scripts/rs-init.sh:/mongo_scripts/rs-init.sh + command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] \ No newline at end of file diff --git a/examples/nestjs-mongoose-book-manager/mongo_scripts/dbstart.sh b/examples/nestjs-mongoose-book-manager/mongo_scripts/dbstart.sh new file mode 100755 index 0000000..20112e4 --- /dev/null +++ b/examples/nestjs-mongoose-book-manager/mongo_scripts/dbstart.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +docker-compose up -d + +sleep 5 + +docker exec mongodb /mongo_scripts/rs-init.sh \ No newline at end of file diff --git a/examples/nestjs-mongoose-book-manager/mongo_scripts/rs-init.sh b/examples/nestjs-mongoose-book-manager/mongo_scripts/rs-init.sh new file mode 100755 index 0000000..d464e18 --- /dev/null +++ b/examples/nestjs-mongoose-book-manager/mongo_scripts/rs-init.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +echo "Starting replica set initialisation" + +mongosh < Date: Thu, 4 Jan 2024 18:46:49 +0100 Subject: [PATCH 04/48] feat: add saveAll endpoint --- .../src/book.controller.ts | 24 ++++++++++++- .../test/book.controller.test.ts | 36 ++++++++++++++++--- .../test/util/mongo-server.ts | 6 +++- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/src/book.controller.ts b/examples/nestjs-mongoose-book-manager/src/book.controller.ts index 0eeb0d3..b4f0259 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.controller.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.controller.ts @@ -9,11 +9,19 @@ import { Patch, Post, } from '@nestjs/common'; -import { AudioBook, Book, PaperBook } from './book'; import { Repository } from '../../../dist'; +import { AudioBook, Book, PaperBook } from './book'; type PartialBook = { id: string } & Partial; +function deserialiseAll(plainBooks: any[]): T[] { + const books: T[] = []; + for (const plainBook of plainBooks) { + books.push(deserialise(plainBook)); + } + return books; +} + function deserialise(plainBook: any): T { let book = null; if (plainBook.edition) { @@ -56,6 +64,20 @@ export class BookController { return this.save(book); } + @Post('/all') + async saveAll( + @Body({ + transform: (plainBooks) => deserialiseAll(plainBooks), + }) + books: Book[], + ): Promise { + try { + return await this.bookRepository.saveAll(books); + } catch (error) { + throw new BadRequestException(error); + } + } + @Delete(':id') async deleteById(@Param('id') id: string): Promise { return this.bookRepository.deleteById(id); diff --git a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts index 0c50eec..983c050 100644 --- a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts +++ b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts @@ -1,14 +1,15 @@ -import { Test } from '@nestjs/testing'; -import { AppModule } from '../src/app.module'; import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; import * as request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { AudioBook, PaperBook } from '../src/book'; import { closeMongoConnection, deleteAll, + findOne, insert, rootMongooseTestModule, } from './util/mongo-server'; -import { AudioBook, PaperBook } from '../src/book'; const timeout = 30000; @@ -102,7 +103,7 @@ describe('Given the book manager controller', () => { describe('that is invalid', () => { it('then returns a bad request HTTP status code', () => { return request(bookManager.getHttpServer()) - .patch('/books/') + .patch('/books') .send() .expect(HttpStatus.BAD_REQUEST); }); @@ -115,7 +116,7 @@ describe('Given the book manager controller', () => { edition: 4, }; return request(bookManager.getHttpServer()) - .patch('/books/') + .patch('/books') .send(paperBookToUpdate) .expect(HttpStatus.BAD_REQUEST); }); @@ -143,6 +144,31 @@ describe('Given the book manager controller', () => { }); }); + describe('when saving a list of books', () => { + describe('that includes an invalid book', () => { + it('then returns a bad request HTTP status code', () => { + const booksToStore = [ + { + title: 'The Sandman', + description: 'Fantastic fantasy audio book', + hostingPlatforms: ['Audible'], + } as AudioBook, + { + description: 'Invalid paper book description', + edition: 1, + } as PaperBook, + ]; + return request(bookManager.getHttpServer()) + .post('/books/all') + .send(booksToStore) + .then(async (result) => { + expect(result.status).toEqual(HttpStatus.BAD_REQUEST); + expect(await findOne({ title: 'The Sandman' }, 'books')).toBeNull(); + }); + }); + }); + }); + describe('when deleting a book', () => { describe('that is not stored', () => { it('then returns false', () => { diff --git a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts index 50d3e94..8307732 100644 --- a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts +++ b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts @@ -13,7 +13,6 @@ export const rootMongooseTestModule = ( MongooseModule.forRootAsync({ useFactory: async () => { mongoServer = await MongoMemoryReplSet.create({ - instanceOpts: [{ port: 27016 }], replSet: { dbName, count: 1 }, }); const mongoUri = mongoServer.getUri(); @@ -41,6 +40,11 @@ export const insert = async ( .then((result) => result.insertedId.toString()); }; +export const findOne = async (filter: any, collection: string) => { + await setupConnection(); + return await mongoose.connection.db.collection(collection).findOne(filter); +}; + export const deleteAll = async (collections: string[]) => { await setupConnection(); await Promise.all( From 68e5e377d8703e1a7916f989ed1a38e11453d249 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sat, 6 Jan 2024 19:16:02 +0100 Subject: [PATCH 05/48] fix: convert to async/await syntax for consistency purposes --- src/mongoose.repository.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index 0539da4..b03e5ee 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -121,15 +121,13 @@ export abstract class MongooseRepository> const offset = options?.pageable?.offset ?? 0; const pageNumber = options?.pageable?.pageNumber ?? 0; try { - return this.entityModel + const documents = await this.entityModel .find(options?.filters) .skip(pageNumber > 0 ? (pageNumber - 1) * offset : 0) .limit(offset) .sort(options?.sortBy) - .exec() - .then((documents) => - documents.map((document) => this.instantiateFrom(document) as S), - ); + .exec(); + return documents.map((document) => this.instantiateFrom(document) as S); } catch (error) { throw new IllegalArgumentException( 'The given optional parameters must be valid', @@ -141,12 +139,8 @@ export abstract class MongooseRepository> /** @inheritdoc */ async findById(id: string): Promise> { if (!id) throw new IllegalArgumentException('The given ID must be valid'); - return this.entityModel - .findById(id) - .exec() - .then((document) => - Optional.ofNullable(this.instantiateFrom(document) as S), - ); + const document = await this.entityModel.findById(id).exec(); + return Optional.ofNullable(this.instantiateFrom(document) as S); } /** @inheritdoc */ From fd6c70ba5c06289b69ba4ed4d9939f7005949608 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 07:23:21 +0000 Subject: [PATCH 06/48] chore(deps-dev): bump eslint from 8.55.0 to 8.56.0 Bumps [eslint](https://github.com/eslint/eslint) from 8.55.0 to 8.56.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.55.0...v8.56.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index cada2b8..8f58011 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1240,10 +1240,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.55.0": - version "8.55.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.55.0.tgz#b721d52060f369aa259cf97392403cb9ce892ec6" - integrity sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA== +"@eslint/js@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" + integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== "@humanwhocodes/config-array@^0.11.13": version "0.11.13" @@ -2926,14 +2926,14 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint@^8.0.1: - version "8.55.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.55.0.tgz#078cb7b847d66f2c254ea1794fa395bf8e7e03f8" - integrity sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA== + version "8.56.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" + integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.55.0" + "@eslint/js" "8.56.0" "@humanwhocodes/config-array" "^0.11.13" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" From 071ed5ad4353463e3b17b10985c3dbec14ffc4ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 07:21:48 +0000 Subject: [PATCH 07/48] chore(deps-dev): bump eslint-plugin-prettier from 5.0.0 to 5.1.2 Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.0.0 to 5.1.2. - [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases) - [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.0.0...v5.1.2) --- updated-dependencies: - dependency-name: eslint-plugin-prettier dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8f58011..5294844 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1570,7 +1570,7 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@pkgr/utils@^2.3.1": +"@pkgr/utils@^2.4.2": version "2.4.2" resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.2.tgz#9e638bbe9a6a6f165580dc943f138fd3309a2cbc" integrity sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw== @@ -2897,12 +2897,12 @@ eslint-plugin-no-only-or-skip-tests@^2.6.2: integrity sha512-Kmqekk+J1MUcRo4vzmz8fN/JmTU13HWL+pzBGSGFGbTQQrgz7DYzLTlt0fhdQWJ5hy9/e8994tdsKsOilIrOlw== eslint-plugin-prettier@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz#6887780ed95f7708340ec79acfdf60c35b9be57a" - integrity sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w== + version "5.1.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz#584c94d4bf31329b2d4cbeb10fd600d17d6de742" + integrity sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg== dependencies: prettier-linter-helpers "^1.0.0" - synckit "^0.8.5" + synckit "^0.8.6" eslint-scope@^5.1.1: version "5.1.1" @@ -5930,13 +5930,13 @@ svgo@^2.7.0: picocolors "^1.0.0" stable "^0.1.8" -synckit@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.5.tgz#b7f4358f9bb559437f9f167eb6bc46b3c9818fa3" - integrity sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q== +synckit@^0.8.6: + version "0.8.6" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.6.tgz#b69b7fbce3917c2673cbdc0d87fb324db4a5b409" + integrity sha512-laHF2savN6sMeHCjLRkheIU4wo3Zg9Ln5YOjOo7sZ5dVQW8yF5pPE5SIw1dsPhq3TRp1jisKRCdPhfs/1WMqDA== dependencies: - "@pkgr/utils" "^2.3.1" - tslib "^2.5.0" + "@pkgr/utils" "^2.4.2" + tslib "^2.6.2" tar-stream@^3.0.0: version "3.1.6" @@ -6046,7 +6046,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.0, tslib@^2.6.2: +tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== From 8b00c772847ea7c7c7dc137c6c69efac8e4cd27e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 07:40:02 +0000 Subject: [PATCH 08/48] chore(deps-dev): bump @types/supertest Bumps [@types/supertest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/supertest) from 2.0.12 to 6.0.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/supertest) --- updated-dependencies: - dependency-name: "@types/supertest" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .../nestjs-mongoose-book-manager/package.json | 2 +- .../nestjs-mongoose-book-manager/yarn.lock | 41 +++++++++++-------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/package.json b/examples/nestjs-mongoose-book-manager/package.json index b174a1b..bff9de0 100644 --- a/examples/nestjs-mongoose-book-manager/package.json +++ b/examples/nestjs-mongoose-book-manager/package.json @@ -41,7 +41,7 @@ "@types/express": "^4.17.13", "@types/jest": "29.5.1", "@types/node": "20.8.4", - "@types/supertest": "^2.0.11", + "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "eslint": "^8.52.0", diff --git a/examples/nestjs-mongoose-book-manager/yarn.lock b/examples/nestjs-mongoose-book-manager/yarn.lock index 6bc59d2..a138682 100644 --- a/examples/nestjs-mongoose-book-manager/yarn.lock +++ b/examples/nestjs-mongoose-book-manager/yarn.lock @@ -1009,10 +1009,10 @@ dependencies: "@types/node" "*" -"@types/cookiejar@*": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" - integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== "@types/eslint-scope@^3.7.3": version "3.7.4" @@ -1099,6 +1099,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -1157,20 +1162,22 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/superagent@*": - version "4.1.17" - resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.17.tgz#c8f0162b5d8a9c52d38b81398ef0650ef974b452" - integrity sha512-FFK/rRjNy24U6J1BvQkaNWu2ohOIF/kxRQXRsbT141YQODcOcZjzlcc4DGdI2SkTa0rhmF+X14zu6ICjCGIg+w== +"@types/superagent@^8.1.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.1.tgz#dbc620c5df3770b0c3092f947d6d5e808adae2bc" + integrity sha512-YQyEXA4PgCl7EVOoSAS3o0fyPFU6erv5mMixztQYe1bqbWmmn8c+IrqoxjQeZe4MgwXikgcaZPiI/DsbmOVlzA== dependencies: - "@types/cookiejar" "*" + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" "@types/node" "*" -"@types/supertest@^2.0.11": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" - integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== +"@types/supertest@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.2.tgz#2af1c466456aaf82c7c6106c6b5cbd73a5e86588" + integrity sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg== dependencies: - "@types/superagent" "*" + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" "@types/webidl-conversions@*": version "7.0.0" @@ -4033,8 +4040,10 @@ mongoose@^8.0.0: sift "16.0.1" "monguito@link:../..": - version "0.0.0" - uid "" + version "4.0.0" + dependencies: + mongoose "^8.0.0" + typescript-optional "^3.0.0-alpha.3" mpath@0.9.0: version "0.9.0" From 4e27109b85f80cffd28c97f52d898541600cdce1 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:25:04 +0100 Subject: [PATCH 09/48] chore: upgrade caniuse-lite dependency --- examples/nestjs-mongoose-book-manager/yarn.lock | 6 ++---- yarn.lock | 13 ++++--------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/yarn.lock b/examples/nestjs-mongoose-book-manager/yarn.lock index a138682..2bbe781 100644 --- a/examples/nestjs-mongoose-book-manager/yarn.lock +++ b/examples/nestjs-mongoose-book-manager/yarn.lock @@ -4040,10 +4040,8 @@ mongoose@^8.0.0: sift "16.0.1" "monguito@link:../..": - version "4.0.0" - dependencies: - mongoose "^8.0.0" - typescript-optional "^3.0.0-alpha.3" + version "0.0.0" + uid "" mpath@0.9.0: version "0.9.0" diff --git a/yarn.lock b/yarn.lock index 5294844..6b34105 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2352,15 +2352,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001464: - version "1.0.30001572" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz" - integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw== - -caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503: - version "1.0.30001572" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz" - integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001503: + version "1.0.30001576" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz" + integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg== chalk@5.3.0: version "5.3.0" From 7f83f17987295664cd0003d6a51e57c13462a2fd Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:34:03 +0100 Subject: [PATCH 10/48] chore: version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e62c53b..7f98db3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monguito", - "version": "4.0.0", + "version": "4.1.0", "description": "MongoDB Abstract Repository implementation for Node.js", "author": { "name": "Josu Martinez", From 8e9eaca0521029064eb7aa098df9017d8773cd0a Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Wed, 10 Jan 2024 18:36:16 +0100 Subject: [PATCH 11/48] refactor: place saveAll in new class hiearchy Required to avoid breaking changes in client code that uses monguito with standalone Mongo. --- src/index.ts | 4 + src/mongoose.repository.ts | 21 +-- src/mongoose.transactional-repository.ts | 41 +++++ src/repository.ts | 14 -- src/transactional-repository.ts | 22 +++ test/repository/book.repository.test.ts | 121 -------------- .../book.transactional-repository.test.ts | 151 ++++++++++++++++++ .../book.transactional-repository.ts | 13 ++ test/util/mongo-server.ts | 1 - 9 files changed, 233 insertions(+), 155 deletions(-) create mode 100644 src/mongoose.transactional-repository.ts create mode 100644 src/transactional-repository.ts create mode 100644 test/repository/book.transactional-repository.test.ts create mode 100644 test/repository/book.transactional-repository.ts diff --git a/src/index.ts b/src/index.ts index ff220d7..6ec1bc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,9 @@ import { TypeData, TypeMap, } from './mongoose.repository'; +import { MongooseTransactionalRepository } from './mongoose.transactional-repository'; import { PartialEntityWithId, Repository } from './repository'; +import { TransactionalRepository } from './transactional-repository'; import { Auditable, AuditableClass, isAuditable } from './util/audit'; import { Entity } from './util/entity'; import { @@ -25,8 +27,10 @@ export { Entity, IllegalArgumentException, MongooseRepository, + MongooseTransactionalRepository, PartialEntityWithId, Repository, + TransactionalRepository, TypeData, TypeMap, UndefinedConstructorException, diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index b03e5ee..a1279a6 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -16,7 +16,6 @@ import { ValidationException, } from './util/exceptions'; import { SearchOptions } from './util/search-options'; -import { runInTransaction } from './util/transaction'; /** * Models a domain object instance constructor. @@ -86,9 +85,9 @@ export abstract class MongooseRepository> protected readonly entityModel: Model; /** - * Sets up the underlying configuration to enable Mongoose operation execution. + * Sets up the underlying configuration to enable database operation execution. * @param {TypeMap} typeMap a map of domain object types supported by this repository. - * @param {Connection=} connection (optional) a Mongoose connection to an instance of MongoDB. + * @param {Connection=} connection (optional) a connection to an instance of MongoDB. */ protected constructor( typeMap: TypeMap, @@ -180,22 +179,6 @@ export abstract class MongooseRepository> } } - /** @inheritdoc */ - async saveAll( - entities: S[] | PartialEntityWithId[], - userId?: string, - ): Promise { - return await runInTransaction( - async (session: ClientSession) => - await Promise.all( - entities.map( - async (entity) => await this.save(entity, userId, session), - ), - ), - this.connection, - ); - } - /** * Instantiates a persistable domain object from the given Mongoose Document. * @param {HydratedDocument | null} document the given Mongoose Document. diff --git a/src/mongoose.transactional-repository.ts b/src/mongoose.transactional-repository.ts new file mode 100644 index 0000000..033188f --- /dev/null +++ b/src/mongoose.transactional-repository.ts @@ -0,0 +1,41 @@ +import { ClientSession, Connection, UpdateQuery } from 'mongoose'; +import { MongooseRepository, TypeMap } from './mongoose.repository'; +import { PartialEntityWithId } from './repository'; +import { TransactionalRepository } from './transactional-repository'; +import { Entity } from './util/entity'; +import { runInTransaction } from './util/transaction'; + +/** + * Abstract Mongoose-based implementation of the {@link TransactionalRepository} interface. + */ +export abstract class MongooseTransactionalRepository< + T extends Entity & UpdateQuery, + > + extends MongooseRepository + implements TransactionalRepository +{ + /** + * Sets up the underlying configuration to enable database operation execution. + * @param {TypeMap} typeMap a map of domain object types supported by this repository. + * @param {Connection=} connection (optional) a connection to an instance of MongoDB. + */ + protected constructor(typeMap: TypeMap, connection?: Connection) { + super(typeMap, connection); + } + + /** @inheritdoc */ + async saveAll( + entities: S[] | PartialEntityWithId[], + userId?: string, + ): Promise { + return await runInTransaction( + async (session: ClientSession) => + await Promise.all( + entities.map( + async (entity) => await this.save(entity, userId, session), + ), + ), + this.connection, + ); + } +} diff --git a/src/repository.ts b/src/repository.ts index 7c26eda..c4796b8 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -45,20 +45,6 @@ export interface Repository { userId?: string, ) => Promise; - /** - * Saves (insert or update) a list of entities. - * @param {S[] | PartialEntityWithId[]} entities the list of entities to save. - * @param {string} userId (optional) the ID of the user executing the action. - * @returns {Promise} the list of saved entities. - * @throws {IllegalArgumentException} if any of the given entities is `undefined` or `null` or - * specifies an `id` not matching any existing entity. - * @throws {ValidationException} if any of the given entities specifies a field with some invalid value. - */ - saveAll: ( - entities: S[] | PartialEntityWithId[], - userId?: string, - ) => Promise; - /** * Deletes an entity by ID. * @param {string} id the ID of the entity. diff --git a/src/transactional-repository.ts b/src/transactional-repository.ts new file mode 100644 index 0000000..5bfebfb --- /dev/null +++ b/src/transactional-repository.ts @@ -0,0 +1,22 @@ +import { PartialEntityWithId, Repository } from './repository'; +import { Entity } from './util/entity'; + +/** + * Specifies a list of common database CRUD operations that must execute in a database transaction. + */ +export interface TransactionalRepository + extends Repository { + /** + * Saves (insert or update) a list of entities. + * @param {S[] | PartialEntityWithId[]} entities the list of entities to save. + * @param {string} userId (optional) the ID of the user executing the action. + * @returns {Promise} the list of saved entities. + * @throws {IllegalArgumentException} if any of the given entities is `undefined` or `null` or + * specifies an `id` not matching any existing entity. + * @throws {ValidationException} if any of the given entities specifies a field with some invalid value. + */ + saveAll: ( + entities: S[] | PartialEntityWithId[], + userId?: string, + ) => Promise; +} diff --git a/test/repository/book.repository.test.ts b/test/repository/book.repository.test.ts index 14f9c28..3e26830 100644 --- a/test/repository/book.repository.test.ts +++ b/test/repository/book.repository.test.ts @@ -14,7 +14,6 @@ import { closeMongoConnection, deleteAll, findById, - findOne, insert, setupConnection, } from '../util/mongo-server'; @@ -1136,126 +1135,6 @@ describe('Given an instance of book repository', () => { }); }); - describe('when saving a list of books', () => { - describe('that is empty', () => { - it('then returns an empty list of books', async () => { - const books = await bookRepository.saveAll([]); - - expect(books).toEqual([]); - }); - }); - - describe('that includes a book that has not been registered as a Mongoose discriminator', () => { - it('throws an exception', async () => { - const booksToInsert = [ - bookFixture({ isbn: '1942788340' }), - electronicBookFixture({ isbn: '1942788341' }), - ]; - await expect( - bookRepository.saveAll(booksToInsert), - ).rejects.toThrowError(IllegalArgumentException); - expect(await findOne({}, 'books')).toBeNull(); - }); - }); - - describe('that includes books that have been registered as a Mongoose discriminator', () => { - // describe('and one book is undefined', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and one book is null', () => { - // it('throws an exception', async () => {}); - // }); - - describe('and all books are new', () => { - // describe('and all books are of the same type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => {}); - // }); - - describe('and each book is of a different type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - describe('and all books specify valid field values', () => { - afterEach(async () => { - await deleteAll('books'); - }); - - it('then inserts the books', async () => { - const booksToInsert = [ - bookFixture({ isbn: '1942788342' }), - paperBookFixture({ isbn: '1942788343' }), - ]; - const savedBooks = await bookRepository.saveAll(booksToInsert); - expect(savedBooks.length).toBe(2); - }); - }); - }); - }); - - // describe('and none of the books is new', () => { - // describe('and all books are of the same type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => {}); - // }); - - // describe('and each book is of a different type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => {}); - // }); - // }); - - // describe('and one book is new while another is not new', () => { - // describe('and all books are of the same type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => { - // describe('and the book to insert has a partial content', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and the book to insert has a complete content', () => { - // describe('and the book to update has a partial content', () => {}); - - // describe('and the book to update has a complete content', () => {}); - // }); - // }); - // }); - - // describe('and each book is of a different type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => { - // describe('and the book to insert has a partial content', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and the book to insert has a complete content', () => { - // describe('and the book to update has a partial content', () => {}); - - // describe('and the book to update has a complete content', () => {}); - // }); - // }); - // }); - // }); - }); - }); - describe('when deleting a book', () => { describe('by an undefined ID', () => { it('then throws an exception', async () => { diff --git a/test/repository/book.transactional-repository.test.ts b/test/repository/book.transactional-repository.test.ts new file mode 100644 index 0000000..f3fb77f --- /dev/null +++ b/test/repository/book.transactional-repository.test.ts @@ -0,0 +1,151 @@ +import { TransactionalRepository } from '../../src/transactional-repository'; +import { IllegalArgumentException } from '../../src/util/exceptions'; +import { Book } from '../domain/book'; +import { + bookFixture, + electronicBookFixture, + paperBookFixture, +} from '../domain/book.fixtures'; +import { + closeMongoConnection, + deleteAll, + findOne, + setupConnection, +} from '../util/mongo-server'; +import { MongooseBookTransactionalRepository } from './book.transactional-repository'; + +describe('Given an instance of book repository', () => { + let bookRepository: TransactionalRepository; + + beforeAll(async () => { + await setupConnection(); + bookRepository = new MongooseBookTransactionalRepository(); + + // FIXME: the following line is oddly required to ensure the right operation of transactions. Figure out what is wrong with the current replica set setup logic. + await findOne({}, 'books'); + }); + + describe('when saving a list of books', () => { + describe('that is empty', () => { + it('then returns an empty list of books', async () => { + const books = await bookRepository.saveAll([]); + + expect(books).toEqual([]); + }); + }); + + describe('that includes a book that has not been registered as a Mongoose discriminator', () => { + it('throws an exception', async () => { + const booksToInsert = [ + bookFixture({ isbn: '1942788340' }), + electronicBookFixture({ isbn: '1942788341' }), + ]; + await expect( + bookRepository.saveAll(booksToInsert), + ).rejects.toThrowError(IllegalArgumentException); + expect(await findOne({}, 'books')).toBeNull(); + }); + }); + + describe('that includes books that have been registered as a Mongoose discriminator', () => { + // describe('and one book is undefined', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and one book is null', () => { + // it('throws an exception', async () => {}); + // }); + + describe('and all books are new', () => { + // describe('and all books are of the same type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => {}); + // }); + + describe('and each book is of a different type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + describe('and all books specify valid field values', () => { + afterEach(async () => { + await deleteAll('books'); + }); + + it('then inserts the books', async () => { + const booksToInsert = [ + bookFixture({ isbn: '1942788342' }), + paperBookFixture({ isbn: '1942788343' }), + ]; + const savedBooks = await bookRepository.saveAll(booksToInsert); + expect(savedBooks.length).toBe(2); + }); + }); + }); + }); + + // describe('and none of the books is new', () => { + // describe('and all books are of the same type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => {}); + // }); + + // describe('and each book is of a different type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => {}); + // }); + // }); + + // describe('and one book is new while another is not new', () => { + // describe('and all books are of the same type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => { + // describe('and the book to insert has a partial content', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and the book to insert has a complete content', () => { + // describe('and the book to update has a partial content', () => {}); + + // describe('and the book to update has a complete content', () => {}); + // }); + // }); + // }); + + // describe('and each book is of a different type', () => { + // describe('and some field values of one book are invalid', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and all books specify valid field values', () => { + // describe('and the book to insert has a partial content', () => { + // it('throws an exception', async () => {}); + // }); + + // describe('and the book to insert has a complete content', () => { + // describe('and the book to update has a partial content', () => {}); + + // describe('and the book to update has a complete content', () => {}); + // }); + // }); + // }); + // }); + }); + }); + + afterAll(async () => { + await closeMongoConnection(); + }); +}); diff --git a/test/repository/book.transactional-repository.ts b/test/repository/book.transactional-repository.ts new file mode 100644 index 0000000..537c010 --- /dev/null +++ b/test/repository/book.transactional-repository.ts @@ -0,0 +1,13 @@ +import { MongooseTransactionalRepository } from '../../src'; +import { AudioBook, Book, PaperBook } from '../domain/book'; +import { AudioBookSchema, BookSchema, PaperBookSchema } from './book.schema'; + +export class MongooseBookTransactionalRepository extends MongooseTransactionalRepository { + constructor() { + super({ + Default: { type: Book, schema: BookSchema }, + PaperBook: { type: PaperBook, schema: PaperBookSchema }, + AudioBook: { type: AudioBook, schema: AudioBookSchema }, + }); + } +} diff --git a/test/util/mongo-server.ts b/test/util/mongo-server.ts index 71f31e9..63758e3 100644 --- a/test/util/mongo-server.ts +++ b/test/util/mongo-server.ts @@ -38,7 +38,6 @@ export const findById = async (id: string, collection: string) => { export const deleteAll = async (collection: string) => { await setupConnection(); await mongoose.connection.db.collection(collection).deleteMany({}); - return; }; export const closeMongoConnection = async () => { From 0a38f70a2e4f09ca1ebea62b555ddd146a363ce0 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sun, 14 Jan 2024 21:42:37 +0100 Subject: [PATCH 12/48] test: complete saveAll tests Also fixed saveAll entities argument type and Mongoose transaction session creation --- src/mongoose.transactional-repository.ts | 2 +- src/transactional-repository.ts | 2 +- src/util/transaction.ts | 2 +- test/repository/book.repository.test.ts | 166 +++--- .../book.transactional-repository.test.ts | 481 ++++++++++++++---- test/util/mongo-server.ts | 3 +- 6 files changed, 477 insertions(+), 179 deletions(-) diff --git a/src/mongoose.transactional-repository.ts b/src/mongoose.transactional-repository.ts index 033188f..164c45f 100644 --- a/src/mongoose.transactional-repository.ts +++ b/src/mongoose.transactional-repository.ts @@ -25,7 +25,7 @@ export abstract class MongooseTransactionalRepository< /** @inheritdoc */ async saveAll( - entities: S[] | PartialEntityWithId[], + entities: (S | PartialEntityWithId)[], userId?: string, ): Promise { return await runInTransaction( diff --git a/src/transactional-repository.ts b/src/transactional-repository.ts index 5bfebfb..8e943ef 100644 --- a/src/transactional-repository.ts +++ b/src/transactional-repository.ts @@ -16,7 +16,7 @@ export interface TransactionalRepository * @throws {ValidationException} if any of the given entities specifies a field with some invalid value. */ saveAll: ( - entities: S[] | PartialEntityWithId[], + entities: (S | PartialEntityWithId)[], userId?: string, ) => Promise; } diff --git a/src/util/transaction.ts b/src/util/transaction.ts index 90a812c..b26d668 100644 --- a/src/util/transaction.ts +++ b/src/util/transaction.ts @@ -20,7 +20,7 @@ export async function runInTransaction( if (connection) { session = await connection.startSession(); } else { - session = await mongoose.startSession(); + session = await mongoose.connection.startSession(); } session.startTransaction(); try { diff --git a/test/repository/book.repository.test.ts b/test/repository/book.repository.test.ts index 3e26830..87c3c31 100644 --- a/test/repository/book.repository.test.ts +++ b/test/repository/book.repository.test.ts @@ -32,7 +32,7 @@ describe('Given an instance of book repository', () => { describe('when searching a book by ID', () => { describe('by an undefined ID', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { await expect( bookRepository.findById(undefined as unknown as string), ).rejects.toThrowError(IllegalArgumentException); @@ -40,7 +40,7 @@ describe('Given an instance of book repository', () => { }); describe('by a null ID', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { await expect( bookRepository.findById(null as unknown as string), ).rejects.toThrowError(IllegalArgumentException); @@ -48,7 +48,7 @@ describe('Given an instance of book repository', () => { }); describe('by the ID of a nonexistent book', () => { - it('then retrieves an empty book', async () => { + it('retrieves an empty book', async () => { const book = await bookRepository.findById('000000000000000000000001'); expect(book).toEqual(Optional.empty()); }); @@ -72,7 +72,7 @@ describe('Given an instance of book repository', () => { await deleteAll('books'); }); - it('then retrieves the book', async () => { + it('retrieves the book', async () => { const book = await bookRepository.findById(storedPaperBook.id!); expect(book.isPresent()).toBe(true); expect(book.get()).toEqual(storedPaperBook); @@ -82,7 +82,7 @@ describe('Given an instance of book repository', () => { describe('when searching a book by a custom field value', () => { describe('and the search value is undefined', () => { - it('then throws an error', async () => { + it('throws an error', async () => { await expect( bookRepository.findByIsbn(undefined as unknown as string), ).rejects.toThrowError(); @@ -90,7 +90,7 @@ describe('Given an instance of book repository', () => { }); describe('and the search value is null', () => { - it('then throws an error', async () => { + it('throws an error', async () => { await expect( bookRepository.findByIsbn(null as unknown as string), ).rejects.toThrowError(); @@ -98,7 +98,7 @@ describe('Given an instance of book repository', () => { }); describe('and there is no book matching the given search value', () => { - it('then returns an empty book', async () => { + it('returns an empty book', async () => { const book = await bookRepository.findByIsbn('0000000000'); expect(book).toEqual(Optional.empty()); }); @@ -118,7 +118,7 @@ describe('Given an instance of book repository', () => { await deleteAll('books'); }); - it('then returns a book matching the given search value', async () => { + it('returns a book matching the given search value', async () => { const book = await bookRepository.findByIsbn(storedBook.isbn); expect(book.isPresent()).toBe(true); expect(book.get()).toEqual(storedBook); @@ -163,7 +163,7 @@ describe('Given an instance of book repository', () => { }); describe('and not providing any optional parameter', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const books = await bookRepository.findAll(); expect(books.length).toBe(3); expect(books).toEqual([storedBook, storedPaperBook, storedAudioBook]); @@ -172,7 +172,7 @@ describe('Given an instance of book repository', () => { describe('and providing a value for the filter parameter', () => { describe('and such a field does not refer to an existing field in any Book type', () => { - it('then retrieves an empty list of books', async () => { + it('retrieves an empty list of books', async () => { const filters = { fruit: 'Banana' }; const books = await bookRepository.findAll({ filters }); expect(books.length).toBe(0); @@ -180,7 +180,7 @@ describe('Given an instance of book repository', () => { }); describe('and such a value refers to an existing field in some Book type', () => { - it('then retrieves a list with all books matching the filter', async () => { + it('retrieves a list with all books matching the filter', async () => { const filters = { __t: 'PaperBook' }; const books = await bookRepository.findAll({ filters }); expect(books.length).toBe(1); @@ -191,7 +191,7 @@ describe('Given an instance of book repository', () => { describe('and providing a value for the sort parameter', () => { describe('and such a value is invalid', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const sortBy = { title: 2 }; await expect(bookRepository.findAll({ sortBy })).rejects.toThrowError( IllegalArgumentException, @@ -200,7 +200,7 @@ describe('Given an instance of book repository', () => { }); describe('and such a value is valid', () => { - it('then retrieves an ordered list with books', async () => { + it('retrieves an ordered list with books', async () => { const sortBy = { title: -1 }; const books = await bookRepository.findAll({ sortBy }); expect(books.length).toBe(3); @@ -212,7 +212,7 @@ describe('Given an instance of book repository', () => { describe('and providing a value for the pagination parameter', () => { describe('and the page number is undefined', () => { describe('and the offset is undefined', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: undefined as unknown as number, offset: undefined as unknown as number, @@ -228,7 +228,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is null', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: undefined as unknown as number, offset: null as unknown as number, @@ -244,7 +244,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is a negative number', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: undefined as unknown as number, offset: -1, @@ -256,7 +256,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is zero', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: undefined as unknown as number, offset: 0, @@ -272,7 +272,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is one', () => { - it('then retrieves a list one book', async () => { + it('retrieves a list one book', async () => { const pageable = { pageNumber: undefined as unknown as number, offset: 1, @@ -284,7 +284,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is equals to the amount of all of the stored books', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: undefined as unknown as number, offset: 3, @@ -300,7 +300,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is bigger than the amount of all of the stored books', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: undefined as unknown as number, offset: 4, @@ -318,7 +318,7 @@ describe('Given an instance of book repository', () => { describe('and the page number is null', () => { describe('and the offset is undefined', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: null as unknown as number, offset: undefined as unknown as number, @@ -334,7 +334,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is null', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: null as unknown as number, offset: null as unknown as number, @@ -350,7 +350,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is a negative number', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: null as unknown as number, offset: -1, @@ -362,7 +362,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is zero', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: null as unknown as number, offset: 0, @@ -378,7 +378,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is one', () => { - it('then retrieves a list one book', async () => { + it('retrieves a list one book', async () => { const pageable = { pageNumber: null as unknown as number, offset: 1, @@ -390,7 +390,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is equals to the amount of all of the stored books', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: null as unknown as number, offset: 3, @@ -406,7 +406,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is bigger than the amount of all of the stored books', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: null as unknown as number, offset: 4, @@ -424,7 +424,7 @@ describe('Given an instance of book repository', () => { describe('and the page number is a negative number', () => { describe('and the offset is undefined', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: -1, offset: undefined as unknown as number, @@ -436,7 +436,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is null', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: -1, offset: null as unknown as number, @@ -448,7 +448,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is a negative number', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: -1, offset: -1 }; await expect( bookRepository.findAll({ pageable }), @@ -457,7 +457,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is zero', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: -1, offset: 0 }; await expect( bookRepository.findAll({ pageable }), @@ -466,7 +466,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is one', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: -1, offset: 1 }; await expect( bookRepository.findAll({ pageable }), @@ -475,7 +475,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is equals to the amount of all of the stored books', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: -1, offset: 3 }; await expect( bookRepository.findAll({ pageable }), @@ -484,7 +484,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is bigger than the amount of all of the stored books', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: -1, offset: 4 }; await expect( bookRepository.findAll({ pageable }), @@ -495,7 +495,7 @@ describe('Given an instance of book repository', () => { describe('and the page number is zero', () => { describe('and the offset is undefined', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 0, offset: undefined as unknown as number, @@ -511,7 +511,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is null', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 0, offset: null as unknown as number, @@ -527,7 +527,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is a negative number', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: 0, offset: -1 }; await expect( bookRepository.findAll({ pageable }), @@ -536,7 +536,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is zero', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 0, offset: 0 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(3); @@ -549,7 +549,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is one', () => { - it('then retrieves a list one book', async () => { + it('retrieves a list one book', async () => { const pageable = { pageNumber: 0, offset: 1 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(1); @@ -558,7 +558,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is equals to the amount of all of the stored books', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 0, offset: 3 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(3); @@ -571,7 +571,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is bigger than the amount of all of the stored books', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 0, offset: 4 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(3); @@ -586,7 +586,7 @@ describe('Given an instance of book repository', () => { describe('and the page number is one', () => { describe('and the offset is undefined', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 1, offset: undefined as unknown as number, @@ -602,7 +602,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is null', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 1, offset: null as unknown as number, @@ -618,7 +618,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is a negative number', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: 1, offset: -1 }; await expect( bookRepository.findAll({ pageable }), @@ -627,7 +627,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is zero', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 1, offset: 0 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(3); @@ -640,7 +640,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is one', () => { - it('then retrieves a list with one book', async () => { + it('retrieves a list with one book', async () => { const pageable = { pageNumber: 1, offset: 1 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(1); @@ -649,7 +649,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is equals to the amount of all of the stored books', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 1, offset: 3 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(3); @@ -662,7 +662,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is bigger than the amount of all of the stored books', () => { - it('then retrieves a list with all books', async () => { + it('retrieves a list with all books', async () => { const pageable = { pageNumber: 1, offset: 4 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(3); @@ -677,7 +677,7 @@ describe('Given an instance of book repository', () => { describe('and the page number is equals to the amount of all of the stored books', () => { describe('and the offset is undefined', () => { - it('then retrieves a list of all books', async () => { + it('retrieves a list of all books', async () => { const pageable = { pageNumber: 3, offset: undefined as unknown as number, @@ -693,7 +693,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is null', () => { - it('then retrieves a list of all books', async () => { + it('retrieves a list of all books', async () => { const pageable = { pageNumber: 3, offset: null as unknown as number, @@ -709,7 +709,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is a negative number', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: 3, offset: -1 }; await expect( bookRepository.findAll({ pageable }), @@ -718,7 +718,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is zero', () => { - it('then retrieves a list of all books', async () => { + it('retrieves a list of all books', async () => { const pageable = { pageNumber: 3, offset: 0 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(3); @@ -731,7 +731,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is one', () => { - it('then retrieves a list with one book', async () => { + it('retrieves a list with one book', async () => { const pageable = { pageNumber: 3, offset: 1 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(1); @@ -740,7 +740,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is equals to the amount of all of the stored books', () => { - it('then retrieves an empty list of books', async () => { + it('retrieves an empty list of books', async () => { const pageable = { pageNumber: 3, offset: 3 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(0); @@ -748,7 +748,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is bigger than the amount of all of the stored books', () => { - it('then retrieves an empty list of books', async () => { + it('retrieves an empty list of books', async () => { const pageable = { pageNumber: 3, offset: 4 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(0); @@ -758,7 +758,7 @@ describe('Given an instance of book repository', () => { describe('and the page number is bigger than the amount of all of the stored books', () => { describe('and the offset is undefined', () => { - it('then retrieves a list of all books', async () => { + it('retrieves a list of all books', async () => { const pageable = { pageNumber: 4, offset: undefined as unknown as number, @@ -774,7 +774,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is null', () => { - it('then retrieves a list of all books', async () => { + it('retrieves a list of all books', async () => { const pageable = { pageNumber: 4, offset: null as unknown as number, @@ -790,7 +790,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is a negative number', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const pageable = { pageNumber: 4, offset: -1 }; await expect( bookRepository.findAll({ pageable }), @@ -799,7 +799,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is zero', () => { - it('then retrieves a list of all books', async () => { + it('retrieves a list of all books', async () => { const pageable = { pageNumber: 4, offset: 0 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(3); @@ -812,7 +812,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is one', () => { - it('then retrieves an empty list of books', async () => { + it('retrieves an empty list of books', async () => { const pageable = { pageNumber: 4, offset: 1 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(0); @@ -820,7 +820,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is equals to the amount of all of the stored books', () => { - it('then retrieves an empty list of books', async () => { + it('retrieves an empty list of books', async () => { const pageable = { pageNumber: 4, offset: 3 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(0); @@ -828,7 +828,7 @@ describe('Given an instance of book repository', () => { }); describe('and the offset is bigger than the amount of all of the stored books', () => { - it('then retrieves an empty list of books', async () => { + it('retrieves an empty list of books', async () => { const pageable = { pageNumber: 4, offset: 4 }; const books = await bookRepository.findAll({ pageable }); expect(books.length).toBe(0); @@ -838,7 +838,7 @@ describe('Given an instance of book repository', () => { }); describe('and providing a valid value for all optional parameters', () => { - it('then retrieves an ordered list with books matching the filter', async () => { + it('retrieves an ordered list with books matching the filter', async () => { const filters = { __t: ['PaperBook', 'AudioBook'] }; const sortBy = { title: -1 }; const pageable = { pageNumber: 1, offset: 1 }; @@ -854,7 +854,7 @@ describe('Given an instance of book repository', () => { }); describe('when saving a book', () => { - describe('that has not been registered as a Mongoose discriminator', () => { + describe('which type is not registered in the repository', () => { it('throws an exception', async () => { const bookToInsert = electronicBookFixture(); await expect(bookRepository.save(bookToInsert)).rejects.toThrowError( @@ -863,9 +863,9 @@ describe('Given an instance of book repository', () => { }); }); - describe('that has been registered as a Mongoose discriminator', () => { + describe('which type is registered in the repository', () => { describe('that is undefined', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { await expect( bookRepository.save(undefined as unknown as Book), ).rejects.toThrowError('The given entity must be valid'); @@ -873,7 +873,7 @@ describe('Given an instance of book repository', () => { }); describe('that is null', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { await expect( bookRepository.save(null as unknown as Book), ).rejects.toThrowError('The given entity must be valid'); @@ -883,7 +883,7 @@ describe('Given an instance of book repository', () => { describe('that is new', () => { describe('and that is of supertype Book', () => { describe('and specifies an ID', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const bookToInsert = bookFixture( { title: 'Modern Software Engineering', @@ -901,7 +901,7 @@ describe('Given an instance of book repository', () => { describe('and does not specify an ID', () => { describe('and some field values are invalid', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const bookToInsert = bookFixture({ title: 'Modern Software Engineering', description: 'Build Better Software Faster', @@ -915,7 +915,7 @@ describe('Given an instance of book repository', () => { }); describe('and all field values are valid', () => { - it('then inserts the book', async () => { + it('inserts the book', async () => { const bookToInsert = bookFixture({ title: 'Modern Software Engineering', description: 'Build Better Software Faster', @@ -933,7 +933,7 @@ describe('Given an instance of book repository', () => { describe('and that is of a subtype of Book', () => { describe('and some field values are invalid', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const bookToInsert = paperBookFixture({ title: 'Implementing Domain-Driven Design', description: 'Describes Domain-Driven Design in depth', @@ -947,7 +947,7 @@ describe('Given an instance of book repository', () => { }); describe('and all field values are valid', () => { - it('then inserts the book', async () => { + it('inserts the book', async () => { const bookToInsert = paperBookFixture({ title: 'Implementing Domain-Driven Design', description: 'Describes Domain-Driven Design in depth', @@ -981,7 +981,7 @@ describe('Given an instance of book repository', () => { describe('and that specifies partial contents of the supertype', () => { describe('and some field values are invalid', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const bookToUpdate = { id: storedBook.id, description: @@ -996,7 +996,7 @@ describe('Given an instance of book repository', () => { }); describe('and all field values are valid', () => { - it('then updates the book', async () => { + it('updates the book', async () => { const bookToUpdate = { id: storedBook.id, description: @@ -1011,7 +1011,7 @@ describe('Given an instance of book repository', () => { }); }); describe('and that specifies all the contents of the supertype', () => { - it('then updates the book', async () => { + it('updates the book', async () => { const bookToUpdate = bookFixture( { title: 'Continuous Delivery', @@ -1060,7 +1060,7 @@ describe('Given an instance of book repository', () => { describe('and that specifies partial contents of the subtype', () => { describe('and some field values are invalid', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const bookToUpdate = { id: storedAudioBook.id, hostingPlatforms: ['Spotify'], @@ -1074,7 +1074,7 @@ describe('Given an instance of book repository', () => { }); describe('and all field values are valid', () => { - it('then updates the book', async () => { + it('updates the book', async () => { const bookToUpdate = { id: storedAudioBook.id, hostingPlatforms: ['Spotify'], @@ -1093,7 +1093,7 @@ describe('Given an instance of book repository', () => { describe('and that specifies all the contents of the subtype', () => { describe('and some field values are invalid', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { const bookToUpdate = audioBookFixture( { title: 'The Pragmatic Programmer', @@ -1110,7 +1110,7 @@ describe('Given an instance of book repository', () => { }); describe('and all field values are valid', () => { - it('then updates the book', async () => { + it('updates the book', async () => { const bookToUpdate = audioBookFixture( { title: 'The Pragmatic Programmer', @@ -1137,7 +1137,7 @@ describe('Given an instance of book repository', () => { describe('when deleting a book', () => { describe('by an undefined ID', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { await expect( bookRepository.deleteById(undefined as unknown as string), ).rejects.toThrowError(IllegalArgumentException); @@ -1145,7 +1145,7 @@ describe('Given an instance of book repository', () => { }); describe('by a null ID', () => { - it('then throws an exception', async () => { + it('throws an exception', async () => { await expect( bookRepository.deleteById(undefined as unknown as string), ).rejects.toThrowError(IllegalArgumentException); @@ -1153,7 +1153,7 @@ describe('Given an instance of book repository', () => { }); describe('by the ID of a nonexistent book', () => { - it('then returns false', async () => { + it('returns false', async () => { const isDeleted = await bookRepository.deleteById( '00007032a61c4eda79230000', ); @@ -1175,7 +1175,7 @@ describe('Given an instance of book repository', () => { await deleteAll('books'); }); - it('then returns true and the book has been effectively deleted', async () => { + it('returns true and the book has been effectively deleted', async () => { const isDeleted = await bookRepository.deleteById(storedBook.id!); expect(isDeleted).toBe(true); expect(await findById(storedBook.id!, 'books')).toBe(null); diff --git a/test/repository/book.transactional-repository.test.ts b/test/repository/book.transactional-repository.test.ts index f3fb77f..0c92996 100644 --- a/test/repository/book.transactional-repository.test.ts +++ b/test/repository/book.transactional-repository.test.ts @@ -1,6 +1,10 @@ +import { PartialEntityWithId } from '../../src'; import { TransactionalRepository } from '../../src/transactional-repository'; -import { IllegalArgumentException } from '../../src/util/exceptions'; -import { Book } from '../domain/book'; +import { + IllegalArgumentException, + ValidationException, +} from '../../src/util/exceptions'; +import { Book, PaperBook } from '../domain/book'; import { bookFixture, electronicBookFixture, @@ -10,138 +14,433 @@ import { closeMongoConnection, deleteAll, findOne, + insert, setupConnection, } from '../util/mongo-server'; import { MongooseBookTransactionalRepository } from './book.transactional-repository'; +const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + describe('Given an instance of book repository', () => { let bookRepository: TransactionalRepository; beforeAll(async () => { await setupConnection(); bookRepository = new MongooseBookTransactionalRepository(); - - // FIXME: the following line is oddly required to ensure the right operation of transactions. Figure out what is wrong with the current replica set setup logic. - await findOne({}, 'books'); + // Wait until the repository is properly connected to Mongoose's connection + await sleep(50); }); describe('when saving a list of books', () => { describe('that is empty', () => { - it('then returns an empty list of books', async () => { + it('returns an empty list of books', async () => { const books = await bookRepository.saveAll([]); expect(books).toEqual([]); }); }); - describe('that includes a book that has not been registered as a Mongoose discriminator', () => { + describe('that includes a book that is undefined', () => { it('throws an exception', async () => { - const booksToInsert = [ + const booksToStore = [ bookFixture({ isbn: '1942788340' }), - electronicBookFixture({ isbn: '1942788341' }), + undefined as unknown as Book, ]; - await expect( - bookRepository.saveAll(booksToInsert), - ).rejects.toThrowError(IllegalArgumentException); + await expect(bookRepository.saveAll(booksToStore)).rejects.toThrowError( + IllegalArgumentException, + ); expect(await findOne({}, 'books')).toBeNull(); }); }); - describe('that includes books that have been registered as a Mongoose discriminator', () => { - // describe('and one book is undefined', () => { - // it('throws an exception', async () => {}); - // }); + describe('that includes a book that is null', () => { + it('throws an exception', async () => { + const booksToStore = [ + bookFixture({ isbn: '1942788340' }), + null as unknown as Book, + ]; + await expect(bookRepository.saveAll(booksToStore)).rejects.toThrowError( + IllegalArgumentException, + ); + expect(await findOne({}, 'books')).toBeNull(); + }); + }); - // describe('and one book is null', () => { - // it('throws an exception', async () => {}); - // }); + describe('that includes a book which type is not registered in the repository', () => { + it('throws an exception', async () => { + const booksToStore = [ + bookFixture({ isbn: '1942788340' }), + electronicBookFixture({ isbn: '1942788341' }), + ]; + await expect(bookRepository.saveAll(booksToStore)).rejects.toThrowError( + IllegalArgumentException, + ); + expect(await findOne({}, 'books')).toBeNull(); + }); + }); - describe('and all books are new', () => { - // describe('and all books are of the same type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); + describe('that includes books which type are registered in the repository', () => { + describe('and all of the books are new', () => { + describe('and all of the books are of the same type', () => { + describe('and some field values of one book are invalid', () => { + it('throws an exception', async () => { + const booksToStore = [ + bookFixture({ isbn: '1942788340' }), + bookFixture({ isbn: undefined }), + ]; + await expect( + bookRepository.saveAll(booksToStore), + ).rejects.toThrowError(ValidationException); + expect(await findOne({}, 'books')).toBeNull(); + }); + }); - // describe('and all books specify valid field values', () => {}); - // }); + describe('and all of the books specify valid field values', () => { + it('inserts the books', async () => { + const booksToStore = [ + bookFixture({ isbn: '1942788342' }), + bookFixture({ isbn: '1942788343' }), + ]; + const savedBooks = await bookRepository.saveAll(booksToStore); + expect(savedBooks.length).toBe(2); + }); + }); + }); describe('and each book is of a different type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - describe('and all books specify valid field values', () => { - afterEach(async () => { - await deleteAll('books'); + describe('and some field values of one book are invalid', () => { + it('throws an exception', async () => { + const booksToStore = [ + bookFixture({ isbn: '1942788340' }), + paperBookFixture({ isbn: undefined }), + ]; + await expect( + bookRepository.saveAll(booksToStore), + ).rejects.toThrowError(ValidationException); + expect(await findOne({}, 'books')).toBeNull(); }); + }); - it('then inserts the books', async () => { - const booksToInsert = [ + describe('and all of the books specify valid field values', () => { + it('inserts the books', async () => { + const booksToStore = [ bookFixture({ isbn: '1942788342' }), paperBookFixture({ isbn: '1942788343' }), ]; - const savedBooks = await bookRepository.saveAll(booksToInsert); + const savedBooks = await bookRepository.saveAll(booksToStore); expect(savedBooks.length).toBe(2); }); }); }); }); - // describe('and none of the books is new', () => { - // describe('and all books are of the same type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => {}); - // }); - - // describe('and each book is of a different type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => {}); - // }); - // }); - - // describe('and one book is new while another is not new', () => { - // describe('and all books are of the same type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => { - // describe('and the book to insert has a partial content', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and the book to insert has a complete content', () => { - // describe('and the book to update has a partial content', () => {}); - - // describe('and the book to update has a complete content', () => {}); - // }); - // }); - // }); - - // describe('and each book is of a different type', () => { - // describe('and some field values of one book are invalid', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and all books specify valid field values', () => { - // describe('and the book to insert has a partial content', () => { - // it('throws an exception', async () => {}); - // }); - - // describe('and the book to insert has a complete content', () => { - // describe('and the book to update has a partial content', () => {}); - - // describe('and the book to update has a complete content', () => {}); - // }); - // }); - // }); - // }); + describe('and all of the books already exist', () => { + describe('and all of the books are of the same type', () => { + let storedPaperBook1: PaperBook, storedPaperBook2: PaperBook; + + beforeEach(async () => { + let paperBookToStore = paperBookFixture({ isbn: '1942788342' }); + let storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook1 = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + + paperBookToStore = paperBookFixture({ isbn: '1942788343' }); + storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook2 = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + }); + + describe('and some field values of one book are invalid', () => { + it('throws an exception', async () => { + const booksToStore = [ + paperBookFixture( + { + isbn: '1942788342', + title: 'New title', + }, + storedPaperBook1.id, + ), + paperBookFixture({ isbn: undefined }, storedPaperBook2.id), + ]; + await expect( + bookRepository.saveAll(booksToStore), + ).rejects.toThrowError(ValidationException); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks).toEqual([storedPaperBook1, storedPaperBook2]); + }); + }); + + describe('and all of the books specify valid field values', () => { + it('updates the books', async () => { + const booksToStore = [ + paperBookFixture( + { + isbn: '1942788342', + title: 'New title', + }, + storedPaperBook1.id, + ), + paperBookFixture( + { + isbn: '1942788343', + title: 'New title', + }, + storedPaperBook2.id, + ), + ]; + const savedBooks = await bookRepository.saveAll(booksToStore); + expect(savedBooks).toEqual(booksToStore); + }); + }); + }); + + describe('and each book is of a different type', () => { + let storedBook: Book, storedPaperBook: PaperBook; + + beforeEach(async () => { + const bookToStore = bookFixture({ isbn: '1942788342' }); + const storedBookId = await insert(bookToStore, 'books'); + storedBook = new Book({ + ...bookToStore, + id: storedBookId, + }); + + const paperBookToStore = paperBookFixture({ isbn: '1942788343' }); + const storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + }); + + describe('and some field values of one book are invalid', () => { + it('throws an exception', async () => { + const booksToStore = [ + bookFixture( + { + isbn: '1942788342', + title: 'New title', + }, + storedBook.id, + ), + paperBookFixture({ isbn: undefined }, storedPaperBook.id), + ]; + await expect( + bookRepository.saveAll(booksToStore), + ).rejects.toThrowError(ValidationException); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks).toEqual([storedBook, storedPaperBook]); + }); + }); + + describe('and all of the books specify valid field values', () => { + it('updates the books', async () => { + const booksToStore = [ + bookFixture( + { + isbn: '1942788342', + title: 'New title', + }, + storedBook.id, + ), + paperBookFixture( + { + isbn: '1942788343', + title: 'New title', + }, + storedPaperBook.id, + ), + ]; + const savedBooks = await bookRepository.saveAll(booksToStore); + expect(savedBooks).toEqual(booksToStore); + }); + }); + }); + }); + + describe('and one book is new while another already exists', () => { + let storedPaperBook: PaperBook; + + beforeEach(async () => { + const paperBookToStore = paperBookFixture({ isbn: '1942788342' }); + const storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + }); + + describe('and all of the books are of the same type', () => { + describe('and some field values of one book are invalid', () => { + it('throws an exception', async () => { + const booksToStore = [ + paperBookFixture( + { isbn: '1942788342', title: 'New title' }, + storedPaperBook.id, + ), + paperBookFixture({ isbn: undefined }), + ]; + await expect( + bookRepository.saveAll(booksToStore), + ).rejects.toThrowError(ValidationException); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks).toEqual([storedPaperBook]); + }); + }); + + describe('and all of the books specify valid field values', () => { + describe('and the book to update has partial content', () => { + it('updates the books', async () => { + const newBook = paperBookFixture({ isbn: '1942788343' }); + const existingBook = { + id: storedPaperBook.id, + title: 'New title', + } as PartialEntityWithId; + const savedBooks = await bookRepository.saveAll([ + newBook, + existingBook, + ]); + expect(savedBooks.length).toBe(2); + expect(savedBooks[0].isbn).toEqual(newBook.isbn); + expect(savedBooks[0].title).toEqual(newBook.title); + expect(savedBooks[0].description).toEqual(newBook.description); + expect(savedBooks[0].edition).toEqual(newBook.edition); + expect(savedBooks[1].id).toEqual(existingBook.id); + expect(savedBooks[1].isbn).toEqual(storedPaperBook.isbn); + expect(savedBooks[1].title).toEqual(existingBook.title); + expect(savedBooks[1].description).toEqual( + storedPaperBook.description, + ); + expect(savedBooks[1].edition).toEqual(storedPaperBook.edition); + }); + }); + + describe('and the book to update has complete content', () => { + it('updates the books', async () => { + const newBook = paperBookFixture({ isbn: '1942788343' }); + const existingBook = paperBookFixture( + { title: 'New title' }, + storedPaperBook.id, + ); + const savedBooks = await bookRepository.saveAll([ + newBook, + existingBook, + ]); + expect(savedBooks.length).toBe(2); + expect(savedBooks[0].isbn).toEqual(newBook.isbn); + expect(savedBooks[0].title).toEqual(newBook.title); + expect(savedBooks[0].description).toEqual(newBook.description); + expect(savedBooks[0].edition).toEqual(newBook.edition); + expect(savedBooks[1].id).toEqual(existingBook.id); + expect(savedBooks[1].isbn).toEqual(existingBook.isbn); + expect(savedBooks[1].title).toEqual(existingBook.title); + expect(savedBooks[1].description).toEqual( + existingBook.description, + ); + expect(savedBooks[1].edition).toEqual(existingBook.edition); + }); + }); + }); + }); + + describe('and each book is of a different type', () => { + describe('and some field values of one book are invalid', () => { + it('throws an exception', async () => { + const booksToStore = [ + bookFixture({ isbn: '1942788343' }), + paperBookFixture({ isbn: undefined }, storedPaperBook.id), + ]; + await expect( + bookRepository.saveAll(booksToStore), + ).rejects.toThrowError(ValidationException); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks).toEqual([storedPaperBook]); + }); + }); + + describe('and all of the books specify valid field values', () => { + describe('and the book to update has a partial content', () => { + it('updates the books', async () => { + const newBook = bookFixture({ isbn: '1942788343' }); + const existingBook = { + id: storedPaperBook.id, + title: 'New title', + } as PartialEntityWithId; + const savedBooks: (Book | PaperBook)[] = + await bookRepository.saveAll([newBook, existingBook]); + expect(savedBooks.length).toBe(2); + expect(savedBooks[0].isbn).toEqual(newBook.isbn); + expect(savedBooks[0].title).toEqual(newBook.title); + expect(savedBooks[0].description).toEqual(newBook.description); + expect(savedBooks[1].id).toEqual(existingBook.id); + expect(savedBooks[1].isbn).toEqual(storedPaperBook.isbn); + expect(savedBooks[1].title).toEqual(existingBook.title); + expect(savedBooks[1].description).toEqual( + storedPaperBook.description, + ); + expect((savedBooks[1] as PaperBook).edition).toEqual( + storedPaperBook.edition, + ); + }); + }); + + describe('and the book to update has a complete content', () => { + it('updates the books', async () => { + const newBook = bookFixture({ isbn: '1942788343' }); + const existingBook = paperBookFixture( + { title: 'New title' }, + storedPaperBook.id, + ); + const savedBooks: (Book | PaperBook)[] = + await bookRepository.saveAll([newBook, existingBook]); + expect(savedBooks.length).toBe(2); + expect(savedBooks[0].isbn).toEqual(newBook.isbn); + expect(savedBooks[0].title).toEqual(newBook.title); + expect(savedBooks[0].description).toEqual(newBook.description); + expect(savedBooks[1].id).toEqual(existingBook.id); + expect(savedBooks[1].isbn).toEqual(existingBook.isbn); + expect(savedBooks[1].title).toEqual(existingBook.title); + expect(savedBooks[1].description).toEqual( + existingBook.description, + ); + expect((savedBooks[1] as PaperBook).edition).toEqual( + existingBook.edition, + ); + }); + }); + }); + }); + }); + }); + + afterEach(async () => { + await deleteAll('books'); }); }); diff --git a/test/util/mongo-server.ts b/test/util/mongo-server.ts index 63758e3..88a9484 100644 --- a/test/util/mongo-server.ts +++ b/test/util/mongo-server.ts @@ -50,7 +50,6 @@ export const setupConnection = async () => { mongoServer = await MongoMemoryReplSet.create({ replSet: { dbName, count: 1 }, }); - await mongoose.connect(mongoServer.getUri()); - mongoose.connection.useDb(dbName); + await mongoose.connect(mongoServer.getUri(), { dbName }); } }; From 3c2d25420a7fe8505d5c1e29f655476064d626c3 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 15 Jan 2024 01:01:01 +0100 Subject: [PATCH 13/48] test: split Mongo standalone vs replica set config --- test/repository/book.repository.test.ts | 3 +- .../book.transactional-repository.test.ts | 3 +- test/util/mongo-server.ts | 29 ++++++++++++------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/test/repository/book.repository.test.ts b/test/repository/book.repository.test.ts index 87c3c31..f3ffc8b 100644 --- a/test/repository/book.repository.test.ts +++ b/test/repository/book.repository.test.ts @@ -11,6 +11,7 @@ import { paperBookFixture, } from '../domain/book.fixtures'; import { + MongoServerType, closeMongoConnection, deleteAll, findById, @@ -26,7 +27,7 @@ describe('Given an instance of book repository', () => { let storedAudioBook: AudioBook; beforeAll(async () => { - setupConnection(); + await setupConnection(MongoServerType.STANDALONE); bookRepository = new MongooseBookRepository(); }); diff --git a/test/repository/book.transactional-repository.test.ts b/test/repository/book.transactional-repository.test.ts index 0c92996..f9d16f3 100644 --- a/test/repository/book.transactional-repository.test.ts +++ b/test/repository/book.transactional-repository.test.ts @@ -11,6 +11,7 @@ import { paperBookFixture, } from '../domain/book.fixtures'; import { + MongoServerType, closeMongoConnection, deleteAll, findOne, @@ -27,7 +28,7 @@ describe('Given an instance of book repository', () => { let bookRepository: TransactionalRepository; beforeAll(async () => { - await setupConnection(); + await setupConnection(MongoServerType.REPLICA_SET); bookRepository = new MongooseBookTransactionalRepository(); // Wait until the repository is properly connected to Mongoose's connection await sleep(50); diff --git a/test/util/mongo-server.ts b/test/util/mongo-server.ts index 88a9484..f01ec40 100644 --- a/test/util/mongo-server.ts +++ b/test/util/mongo-server.ts @@ -1,9 +1,14 @@ -import { MongoMemoryReplSet } from 'mongodb-memory-server'; +import { MongoMemoryReplSet, MongoMemoryServer } from 'mongodb-memory-server'; import mongoose from 'mongoose'; import { Entity } from '../../src'; +export enum MongoServerType { + STANDALONE, + REPLICA_SET, +} + const dbName = 'test'; -let mongoServer: MongoMemoryReplSet; +let mongoServer: MongoMemoryServer | MongoMemoryReplSet; type EntityWithOptionalDiscriminatorKey = Entity & { __t?: string }; @@ -12,7 +17,6 @@ export const insert = async ( collection: string, discriminatorKey?: string, ) => { - await setupConnection(); if (discriminatorKey) { entity['__t'] = discriminatorKey; } @@ -24,19 +28,16 @@ export const insert = async ( }; export const findOne = async (filter: any, collection: string) => { - await setupConnection(); return await mongoose.connection.db.collection(collection).findOne(filter); }; export const findById = async (id: string, collection: string) => { - await setupConnection(); return await mongoose.connection.db .collection(collection) .findOne({ id: id }); }; export const deleteAll = async (collection: string) => { - await setupConnection(); await mongoose.connection.db.collection(collection).deleteMany({}); }; @@ -45,11 +46,19 @@ export const closeMongoConnection = async () => { await mongoServer?.stop(); }; -export const setupConnection = async () => { +export const setupConnection = async ( + mongoServerType: MongoServerType = MongoServerType.STANDALONE, +) => { if (!mongoServer) { - mongoServer = await MongoMemoryReplSet.create({ - replSet: { dbName, count: 1 }, - }); + if (mongoServerType === MongoServerType.STANDALONE) { + mongoServer = await MongoMemoryServer.create({ + instance: { dbName }, + }); + } else { + mongoServer = await MongoMemoryReplSet.create({ + replSet: { dbName, count: 1 }, + }); + } await mongoose.connect(mongoServer.getUri(), { dbName }); } }; From 4263b90df4a2ccdaf3eb028734b94dabdd6dfeb7 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sun, 21 Jan 2024 20:28:14 +0100 Subject: [PATCH 14/48] fix: re-call callback under transient transaction error --- src/util/transaction.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/util/transaction.ts b/src/util/transaction.ts index b26d668..e6f6d06 100644 --- a/src/util/transaction.ts +++ b/src/util/transaction.ts @@ -5,6 +5,10 @@ import mongoose, { ClientSession, Connection } from 'mongoose'; */ export type DbCallback = (session: ClientSession) => Promise; +export type TransactionOptions = { + retries: number; +}; + /** * Runs the provided callback function within a transaction and commits the changes to the database * iff it has run successfully. @@ -15,6 +19,8 @@ export type DbCallback = (session: ClientSession) => Promise; export async function runInTransaction( callback: DbCallback, connection?: Connection, + options?: TransactionOptions, + attempt = 0, ): Promise { let session: ClientSession; if (connection) { @@ -29,8 +35,25 @@ export async function runInTransaction( return result; } catch (error) { await session.abortTransaction(); + if ( + isTransientTransactionError(error) && + attempt < (options?.retries ?? 3) + ) { + return runInTransaction(callback, connection, options, ++attempt); + } throw error; } finally { session.endSession(); } } + +/** + * Determines whether the given error is a transient transaction error or not. + * Transient transaction errors can be safely retried. + * + * @param error the given error. + * @returns `true` if the given error is a transient transaction error, `false otherwise`. + */ +function isTransientTransactionError(error: any): boolean { + return error.message.includes('does not match any in-progress transactions'); +} From 396a7be4f604c54d02f32337175e5ded216ac7b6 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 22 Jan 2024 00:07:27 +0100 Subject: [PATCH 15/48] refactor: refactor runInTransaction --- src/mongoose.transactional-repository.ts | 2 +- src/util/transaction.ts | 36 +++++++++++++++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/mongoose.transactional-repository.ts b/src/mongoose.transactional-repository.ts index 164c45f..db0fc23 100644 --- a/src/mongoose.transactional-repository.ts +++ b/src/mongoose.transactional-repository.ts @@ -35,7 +35,7 @@ export abstract class MongooseTransactionalRepository< async (entity) => await this.save(entity, userId, session), ), ), - this.connection, + { connection: this.connection }, ); } } diff --git a/src/util/transaction.ts b/src/util/transaction.ts index e6f6d06..f4c9a7c 100644 --- a/src/util/transaction.ts +++ b/src/util/transaction.ts @@ -5,8 +5,14 @@ import mongoose, { ClientSession, Connection } from 'mongoose'; */ export type DbCallback = (session: ClientSession) => Promise; +/** + * Specifies some transaction options, specifically the Mongoose connection object to create a transaction + * session from and the maximum amount of times that the callback function is to be retried in the case of + * any MongoDB transient transaction error. + */ export type TransactionOptions = { - retries: number; + connection?: Connection; + retries?: number; }; /** @@ -14,20 +20,31 @@ export type TransactionOptions = { * iff it has run successfully. * * @param {DbCallback} callback a callback function that writes to and reads from the database using a session. - * @param {Connection=} connection (optional) a Mongoose connection to create the session from. + * @param {TransactionOptions=} options (optional) some options about the transaction. */ export async function runInTransaction( callback: DbCallback, - connection?: Connection, options?: TransactionOptions, - attempt = 0, ): Promise { - let session: ClientSession; + return await innerRunIntransaction(callback, 0, options); +} + +async function startSession(connection?: Connection): Promise { if (connection) { - session = await connection.startSession(); + return await connection.startSession(); } else { - session = await mongoose.connection.startSession(); + return await mongoose.connection.startSession(); } +} + +const DEFAULT_MAX_RETRIES = 3; + +async function innerRunIntransaction( + callback: DbCallback, + attempt: number, + options?: TransactionOptions, +): Promise { + const session = await startSession(options?.connection); session.startTransaction(); try { const result = await callback(session); @@ -37,9 +54,9 @@ export async function runInTransaction( await session.abortTransaction(); if ( isTransientTransactionError(error) && - attempt < (options?.retries ?? 3) + attempt < (options?.retries ?? DEFAULT_MAX_RETRIES) ) { - return runInTransaction(callback, connection, options, ++attempt); + return innerRunIntransaction(callback, ++attempt, options); } throw error; } finally { @@ -50,7 +67,6 @@ export async function runInTransaction( /** * Determines whether the given error is a transient transaction error or not. * Transient transaction errors can be safely retried. - * * @param error the given error. * @returns `true` if the given error is a transient transaction error, `false otherwise`. */ From b3669dac013a6351e636d722285eeeccab644a48 Mon Sep 17 00:00:00 2001 From: Josu Martinez Date: Wed, 24 Jan 2024 00:52:02 +0100 Subject: [PATCH 16/48] fix: enrich type definition --- src/mongoose.repository.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index a1279a6..b64b1bf 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -51,12 +51,12 @@ class InnerTypeMap { return index !== -1 ? this.data[index] : undefined; } - getSupertypeData(): TypeData { - return this.get('Default')!; + getSupertypeData(): TypeData | undefined { + return this.get('Default'); } - getSupertypeName(): string { - return this.getSupertypeData().type.name; + getSupertypeName(): string | undefined { + return this.getSupertypeData()?.type.name; } getSubtypesData(): TypeData[] { @@ -202,6 +202,10 @@ export abstract class MongooseRepository> private createEntityModel(connection?: Connection) { let entityModel; const supertypeData = this.typeMap.getSupertypeData(); + if (!supertypeData) + throw new UndefinedConstructorException( + 'No super type constructor is registered', + ); if (connection) { entityModel = connection.model( supertypeData.type.name, From 608b1749fe54a0cf7839c1fb40de610dcc6a0a4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jan 2024 02:42:43 +0000 Subject: [PATCH 17/48] chore(deps): bump follow-redirects from 1.15.3 to 1.15.4 Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6b34105..c9b7d0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3247,9 +3247,9 @@ flatted@^3.1.0: integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== follow-redirects@^1.15.3: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + version "1.15.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" + integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== for-each@^0.3.3: version "0.3.3" From 6ff5c9184241cf7dcf1550ccfa70fcb867267358 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jan 2024 02:42:33 +0000 Subject: [PATCH 18/48] chore(deps): bump follow-redirects Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- examples/nestjs-mongoose-book-manager/yarn.lock | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/yarn.lock b/examples/nestjs-mongoose-book-manager/yarn.lock index 2bbe781..55a39fa 100644 --- a/examples/nestjs-mongoose-book-manager/yarn.lock +++ b/examples/nestjs-mongoose-book-manager/yarn.lock @@ -2725,9 +2725,9 @@ flatted@^3.1.0: integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== follow-redirects@^1.15.3: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + version "1.15.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" + integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== foreground-child@^3.1.0: version "3.1.1" @@ -4040,8 +4040,10 @@ mongoose@^8.0.0: sift "16.0.1" "monguito@link:../..": - version "0.0.0" - uid "" + version "4.1.0" + dependencies: + mongoose "^8.0.0" + typescript-optional "^3.0.0-alpha.3" mpath@0.9.0: version "0.9.0" From 81c3090077ff518e9cf1d3b0b10d0a92ed34bc65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 07:14:29 +0000 Subject: [PATCH 19/48] chore(deps-dev): bump prettier from 3.1.0 to 3.2.2 Bumps [prettier](https://github.com/prettier/prettier) from 3.1.0 to 3.2.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.1.0...3.2.2) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c9b7d0f..3e1c096 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5284,9 +5284,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e" - integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw== + version "3.2.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.2.tgz#96e580f7ca9c96090ad054616c0c4597e2844b65" + integrity sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A== pretty-bytes@^3.0.0: version "3.0.1" From 56976dcc433f9b08fbdf0dfef74e84a58a4601d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 07:16:00 +0000 Subject: [PATCH 20/48] chore(deps-dev): bump @nestjs/cli Bumps [@nestjs/cli](https://github.com/nestjs/nest-cli) from 10.2.1 to 10.3.0. - [Release notes](https://github.com/nestjs/nest-cli/releases) - [Changelog](https://github.com/nestjs/nest-cli/blob/master/.release-it.json) - [Commits](https://github.com/nestjs/nest-cli/compare/10.2.1...10.3.0) --- updated-dependencies: - dependency-name: "@nestjs/cli" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .../nestjs-mongoose-book-manager/package.json | 2 +- .../nestjs-mongoose-book-manager/yarn.lock | 302 ++++++++++-------- 2 files changed, 178 insertions(+), 126 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/package.json b/examples/nestjs-mongoose-book-manager/package.json index bff9de0..f279a58 100644 --- a/examples/nestjs-mongoose-book-manager/package.json +++ b/examples/nestjs-mongoose-book-manager/package.json @@ -35,7 +35,7 @@ "typescript-optional": "^3.0.0-alpha.3" }, "devDependencies": { - "@nestjs/cli": "^10.2.1", + "@nestjs/cli": "^10.3.0", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^9.0.0", "@types/express": "^4.17.13", diff --git a/examples/nestjs-mongoose-book-manager/yarn.lock b/examples/nestjs-mongoose-book-manager/yarn.lock index 55a39fa..0e0290b 100644 --- a/examples/nestjs-mongoose-book-manager/yarn.lock +++ b/examples/nestjs-mongoose-book-manager/yarn.lock @@ -26,27 +26,27 @@ rxjs "7.8.1" source-map "0.7.4" -"@angular-devkit/core@16.2.8": - version "16.2.8" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.8.tgz#db74f3063e7fd573be7dafd022e8dc10e43140c0" - integrity sha512-PTGozYvh1Bin5lB15PwcXa26Ayd17bWGLS3H8Rs0s+04mUDvfNofmweaX1LgumWWy3nCUTDuwHxX10M3G0wE2g== +"@angular-devkit/core@17.0.9": + version "17.0.9" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.0.9.tgz#f79ff77fc38e8af1af4694acfb5b480339f073e1" + integrity sha512-r5jqwpWOgowqe9KSDqJ3iSbmsEt2XPjSvRG4DSI2T9s31bReoMtreo8b7wkRa2B3hbcDnstFbn8q27VvJDqRaQ== dependencies: ajv "8.12.0" ajv-formats "2.1.1" jsonc-parser "3.2.0" - picomatch "2.3.1" + picomatch "3.0.1" rxjs "7.8.1" source-map "0.7.4" -"@angular-devkit/schematics-cli@16.2.8": - version "16.2.8" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-16.2.8.tgz#5945f391d316724d7b49d578932dcb5a25a73649" - integrity sha512-EXURJCzWTVYCipiTT4vxQQOrF63asOUDbeOy3OtiSh7EwIUvxm3BPG6hquJqngEnI/N6bA75NJ1fBhU6Hrh7eA== +"@angular-devkit/schematics-cli@17.0.9": + version "17.0.9" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-17.0.9.tgz#6ee0ac81b37cab50e60b49a0541508552a75ab7f" + integrity sha512-tznzzB26sy8jVUlV9HhXcbFYZcIIFMAiDMOuyLko2LZFjfoqW+OPvwa1mwAQwvVVSQZVAKvdndFhzwyl/axwFQ== dependencies: - "@angular-devkit/core" "16.2.8" - "@angular-devkit/schematics" "16.2.8" + "@angular-devkit/core" "17.0.9" + "@angular-devkit/schematics" "17.0.9" ansi-colors "4.1.3" - inquirer "8.2.4" + inquirer "9.2.11" symbol-observable "4.0.0" yargs-parser "21.1.1" @@ -61,14 +61,14 @@ ora "5.4.1" rxjs "7.8.1" -"@angular-devkit/schematics@16.2.8": - version "16.2.8" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.8.tgz#cc11cf6d00cd9131adbede9a99f3a617aedd5bc4" - integrity sha512-MBiKZOlR9/YMdflALr7/7w/BGAfo/BGTrlkqsIB6rDWV1dYiCgxI+033HsiNssLS6RQyCFx/e7JA2aBBzu9zEg== +"@angular-devkit/schematics@17.0.9": + version "17.0.9" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.0.9.tgz#8370d21cf0ac0f5f99b7a27fcf626cc5cd682c95" + integrity sha512-5ti7g45F2KjDJS0DbgnOGI1GyKxGpn4XsKTYJFJrSAWj6VpuvPy/DINRrXNuRVo09VPEkqA+IW7QwaG9icptQg== dependencies: - "@angular-devkit/core" "16.2.8" + "@angular-devkit/core" "17.0.9" jsonc-parser "3.2.0" - magic-string "0.30.1" + magic-string "0.30.5" ora "5.4.1" rxjs "7.8.1" @@ -774,6 +774,13 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@ljharb/through@^2.3.9": + version "2.3.11" + resolved "https://registry.yarnpkg.com/@ljharb/through/-/through-2.3.11.tgz#783600ff12c06f21a76cc26e33abd0b1595092f9" + integrity sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w== + dependencies: + call-bind "^1.0.2" + "@lukeed/csprng@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" @@ -786,14 +793,14 @@ dependencies: sparse-bitfield "^3.0.3" -"@nestjs/cli@^10.2.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.2.1.tgz#a1d32c28e188f0fb4c3f54235c55745de4c6dd7f" - integrity sha512-CAJAQwmxFZfB3RTvqz/eaXXWpyU+mZ4QSqfBYzjneTsPgF+uyOAW3yQpaLNn9Dfcv39R9UxSuAhayv6yuFd+Jg== +"@nestjs/cli@^10.3.0": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.3.0.tgz#5f9ef49a60baf4b39cb87e4b74240f7c9339e923" + integrity sha512-37h+wSDItY0NE/x3a/M9yb2cXzfsD4qoE26rHgFn592XXLelDN12wdnfn7dTIaiRZT7WOCdQ+BYP9mQikR4AsA== dependencies: - "@angular-devkit/core" "16.2.8" - "@angular-devkit/schematics" "16.2.8" - "@angular-devkit/schematics-cli" "16.2.8" + "@angular-devkit/core" "17.0.9" + "@angular-devkit/schematics" "17.0.9" + "@angular-devkit/schematics-cli" "17.0.9" "@nestjs/schematics" "^10.0.1" chalk "4.1.2" chokidar "3.5.3" @@ -804,14 +811,13 @@ inquirer "8.2.6" node-emoji "1.11.0" ora "5.4.1" - os-name "4.0.1" rimraf "4.4.1" shelljs "0.8.5" source-map-support "0.5.21" tree-kill "1.2.2" tsconfig-paths "4.2.0" tsconfig-paths-webpack-plugin "4.1.0" - typescript "5.2.2" + typescript "5.3.3" webpack "5.89.0" webpack-node-externals "3.0.0" @@ -1501,7 +1507,7 @@ ansi-colors@4.1.3: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== -ansi-escapes@^4.2.1: +ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== @@ -1842,6 +1848,15 @@ call-bind@^1.0.0: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1879,6 +1894,11 @@ chalk@^2.0.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -1950,6 +1970,11 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2184,6 +2209,15 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-lazy-prop@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" @@ -2276,13 +2310,6 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - enhanced-resolve@^5.15.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -2336,6 +2363,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + eslint-config-prettier@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f" @@ -2472,21 +2504,6 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^4.0.2: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -2570,7 +2587,7 @@ express@4.18.2: utils-merge "1.0.1" vary "~1.1.2" -external-editor@^3.0.3: +external-editor@^3.0.3, external-editor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== @@ -2659,6 +2676,14 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" +figures@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-5.0.0.tgz#126cd055052dea699f8a54e8c9450e6ecfc44d5f" + integrity sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg== + dependencies: + escape-string-regexp "^5.0.0" + is-unicode-supported "^1.2.0" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2813,6 +2838,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2833,18 +2863,21 @@ get-intrinsic@^1.0.2: has-proto "^1.0.1" has-symbols "^1.0.3" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stream@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -2926,6 +2959,13 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -2951,6 +2991,13 @@ has-own-prop@^2.0.0: resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af" integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" @@ -2968,6 +3015,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hexoid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" @@ -2997,11 +3051,6 @@ https-proxy-agent@^7.0.2: agent-base "^7.0.2" debug "4" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -3063,27 +3112,6 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inquirer@8.2.4: - version "8.2.4" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" - integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.1" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^5.4.1" - run-async "^2.4.0" - rxjs "^7.5.5" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - wrap-ansi "^7.0.0" - inquirer@8.2.6: version "8.2.6" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" @@ -3105,6 +3133,27 @@ inquirer@8.2.6: through "^2.3.6" wrap-ansi "^6.0.1" +inquirer@9.2.11: + version "9.2.11" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.2.11.tgz#e9003755c233a414fceda1891c23bd622cad4a95" + integrity sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g== + dependencies: + "@ljharb/through" "^2.3.9" + ansi-escapes "^4.3.2" + chalk "^5.3.0" + cli-cursor "^3.1.0" + cli-width "^4.1.0" + external-editor "^3.1.0" + figures "^5.0.0" + lodash "^4.17.21" + mute-stream "1.0.0" + ora "^5.4.1" + run-async "^3.0.0" + rxjs "^7.8.1" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wrap-ansi "^6.2.0" + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -3208,6 +3257,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -3809,11 +3863,6 @@ lru-cache@^9.1.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== -macos-release@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.1.tgz#bccac4a8f7b93163a8d163b8ebf385b3c5f55bf9" - integrity sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A== - magic-string@0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529" @@ -3821,10 +3870,10 @@ magic-string@0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -magic-string@0.30.1: - version "0.30.1" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.1.tgz#ce5cd4b0a81a5d032bd69aab4522299b2166284d" - integrity sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA== +magic-string@0.30.5: + version "0.30.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" + integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" @@ -4090,6 +4139,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mute-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" + integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -4151,7 +4205,7 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.0, npm-run-path@^4.0.1: +npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -4182,7 +4236,7 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -4240,14 +4294,6 @@ ora@5.4.1, ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -os-name@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/os-name/-/os-name-4.0.1.tgz#32cee7823de85a8897647ba4d76db46bf845e555" - integrity sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw== - dependencies: - macos-release "^2.5.0" - windows-release "^4.0.0" - os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -4374,7 +4420,12 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@2.3.1, picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-3.0.1.tgz#817033161def55ec9638567a2f3bbc876b3e7516" + integrity sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -4443,14 +4494,6 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - punycode@^2.1.0, punycode@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -4648,6 +4691,11 @@ run-async@^2.4.0: resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== +run-async@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-3.0.0.tgz#42a432f6d76c689522058984384df28be379daad" + integrity sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -4655,7 +4703,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@7.8.1, rxjs@^7.2.0, rxjs@^7.5.5: +rxjs@7.8.1, rxjs@^7.2.0, rxjs@^7.5.5, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -4743,6 +4791,17 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +set-function-length@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1" + integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w== + dependencies: + define-data-property "^1.1.1" + function-bind "^1.1.2" + get-intrinsic "^1.2.2" + gopd "^1.0.1" + has-property-descriptors "^1.0.1" + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -5239,10 +5298,10 @@ typescript-optional@^3.0.0-alpha.3: resolved "https://registry.yarnpkg.com/typescript-optional/-/typescript-optional-3.0.0-alpha.3.tgz#2f5317275f66318c6c521423b3e313feab0e8f79" integrity sha512-X2JbUQA+WK0P8gwiickO6s8yZnX/ufov6zx4hbvdYVqHFTz8fAYoh+8JMKxVzQuh2/aMUvF9KSNqXi4p6pNxuA== -typescript@5.2.2, typescript@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" - integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +typescript@5.3.3, typescript@^5.2.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== uid@2.0.2: version "2.0.2" @@ -5410,13 +5469,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -windows-release@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-4.0.0.tgz#4725ec70217d1bf6e02c7772413b29cdde9ec377" - integrity sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg== - dependencies: - execa "^4.0.2" - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -5426,7 +5478,7 @@ windows-release@^4.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^6.0.1: +wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== From 9f9865928b0498cc13b100b264891e347123567b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 07:14:40 +0000 Subject: [PATCH 21/48] chore(deps-dev): bump @types/node from 20.10.0 to 20.11.1 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.10.0 to 20.11.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3e1c096..8f65a79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1783,9 +1783,9 @@ integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== "@types/node@*", "@types/node@^20.5.0": - version "20.10.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.0.tgz#16ddf9c0a72b832ec4fcce35b8249cf149214617" - integrity sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ== + version "20.11.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.1.tgz#6a93f94abeda166f688d3d2aca18012afbe5f850" + integrity sha512-DsXojJUES2M+FE8CpptJTKpg+r54moV9ZEncPstni1WHFmTcCzeFLnMFfyhCVS8XNOy/OQG+8lVxRLRrVHmV5A== dependencies: undici-types "~5.26.4" From 5aaef0a66d72d1ad3fdba18a22966087e21f9242 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Jan 2024 18:56:32 +0000 Subject: [PATCH 22/48] chore(deps-dev): bump @nestjs/schematics Bumps [@nestjs/schematics](https://github.com/nestjs/schematics) from 10.0.1 to 10.1.0. - [Release notes](https://github.com/nestjs/schematics/releases) - [Changelog](https://github.com/nestjs/schematics/blob/master/.release-it.json) - [Commits](https://github.com/nestjs/schematics/compare/10.0.1...10.1.0) --- updated-dependencies: - dependency-name: "@nestjs/schematics" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .../nestjs-mongoose-book-manager/package.json | 2 +- .../nestjs-mongoose-book-manager/yarn.lock | 44 ++++--------------- 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/package.json b/examples/nestjs-mongoose-book-manager/package.json index f279a58..690fc0d 100644 --- a/examples/nestjs-mongoose-book-manager/package.json +++ b/examples/nestjs-mongoose-book-manager/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@nestjs/cli": "^10.3.0", - "@nestjs/schematics": "^10.0.1", + "@nestjs/schematics": "^10.1.0", "@nestjs/testing": "^9.0.0", "@types/express": "^4.17.13", "@types/jest": "29.5.1", diff --git a/examples/nestjs-mongoose-book-manager/yarn.lock b/examples/nestjs-mongoose-book-manager/yarn.lock index 0e0290b..66009a0 100644 --- a/examples/nestjs-mongoose-book-manager/yarn.lock +++ b/examples/nestjs-mongoose-book-manager/yarn.lock @@ -15,17 +15,6 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@angular-devkit/core@16.1.0": - version "16.1.0" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.1.0.tgz#cb56b19e88fc936fb0b26c5ae62591f1e8906961" - integrity sha512-mrWpuDvttmhrCGcLc68RIXKtTzUhkBTsE5ZZFZNO1+FSC+vO/ZpyCpPd6C+6coM68NfXYjHlms5XF6KbxeGn/Q== - dependencies: - ajv "8.12.0" - ajv-formats "2.1.1" - jsonc-parser "3.2.0" - rxjs "7.8.1" - source-map "0.7.4" - "@angular-devkit/core@17.0.9": version "17.0.9" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.0.9.tgz#f79ff77fc38e8af1af4694acfb5b480339f073e1" @@ -50,17 +39,6 @@ symbol-observable "4.0.0" yargs-parser "21.1.1" -"@angular-devkit/schematics@16.1.0": - version "16.1.0" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.1.0.tgz#a0235ba402c9dfb38b7dedbf59bbcda667e8ec2f" - integrity sha512-LM35PH9DT3eQRSZgrkk2bx1ZQjjVh8BCByTlr37/c+FnF9mNbeBsa1YkxrlsN/CwO+045OwEwRHnkM9Zcx0U/A== - dependencies: - "@angular-devkit/core" "16.1.0" - jsonc-parser "3.2.0" - magic-string "0.30.0" - ora "5.4.1" - rxjs "7.8.1" - "@angular-devkit/schematics@17.0.9": version "17.0.9" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.0.9.tgz#8370d21cf0ac0f5f99b7a27fcf626cc5cd682c95" @@ -753,7 +731,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.15": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.15": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== @@ -863,13 +841,13 @@ multer "1.4.4-lts.1" tslib "2.5.3" -"@nestjs/schematics@^10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-10.0.1.tgz#c44f763186de621ec8b3974891bbb27d3c6df04b" - integrity sha512-buxpYtSwOmWyf0nUJWJCkCkYITwbOfIEKHTnGS7sDbcfaajrOFXb5pPAGD2E1CUb3C1+NkQIURPKzs0IouZTQg== +"@nestjs/schematics@^10.0.1", "@nestjs/schematics@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-10.1.0.tgz#bf9be846bafad0f45f0ea5ed80aaaf971bb90873" + integrity sha512-HQWvD3F7O0Sv3qHS2jineWxPLmBTLlyjT6VdSw2EAIXulitmV+ErxB3TCVQQORlNkl5p5cwRYWyBaOblDbNFIQ== dependencies: - "@angular-devkit/core" "16.1.0" - "@angular-devkit/schematics" "16.1.0" + "@angular-devkit/core" "17.0.9" + "@angular-devkit/schematics" "17.0.9" comment-json "4.2.3" jsonc-parser "3.2.0" pluralize "8.0.0" @@ -3863,13 +3841,6 @@ lru-cache@^9.1.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== -magic-string@0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529" - integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - magic-string@0.30.5: version "0.30.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" @@ -5470,6 +5441,7 @@ which@^2.0.1: isexe "^2.0.0" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 55ef33c8ea7d0a072bf449670cdaf608ccb97c88 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 22 Jan 2024 00:14:31 +0100 Subject: [PATCH 23/48] chore: version bump --- examples/nestjs-mongoose-book-manager/yarn.lock | 6 ++---- package.json | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/yarn.lock b/examples/nestjs-mongoose-book-manager/yarn.lock index 66009a0..3d112bb 100644 --- a/examples/nestjs-mongoose-book-manager/yarn.lock +++ b/examples/nestjs-mongoose-book-manager/yarn.lock @@ -4060,10 +4060,8 @@ mongoose@^8.0.0: sift "16.0.1" "monguito@link:../..": - version "4.1.0" - dependencies: - mongoose "^8.0.0" - typescript-optional "^3.0.0-alpha.3" + version "0.0.0" + uid "" mpath@0.9.0: version "0.9.0" diff --git a/package.json b/package.json index 7f98db3..b3b3d8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monguito", - "version": "4.1.0", + "version": "4.1.1", "description": "MongoDB Abstract Repository implementation for Node.js", "author": { "name": "Josu Martinez", From 5a382fd09aaf41ba6108801267790d57182717a4 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 1 Jan 2024 18:59:42 +0100 Subject: [PATCH 24/48] feat: start saveAll development Created saveAll test case list draft. Also added Mongo transaction handling (required update from standalone Mongo to replica set for testing purposes) and fixtures to aid in testing. --- examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts index 8307732..05bbb48 100644 --- a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts +++ b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts @@ -13,6 +13,7 @@ export const rootMongooseTestModule = ( MongooseModule.forRootAsync({ useFactory: async () => { mongoServer = await MongoMemoryReplSet.create({ + instanceOpts: [{ port: 27016 }], replSet: { dbName, count: 1 }, }); const mongoUri = mongoServer.getUri(); From 38a8954b56ec5ab47330f09bbc2435bc53d0fc9e Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:00:22 +0100 Subject: [PATCH 25/48] fix: switch to transactional book repository --- .../src/book.controller.ts | 4 ++-- .../src/book.repository.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/src/book.controller.ts b/examples/nestjs-mongoose-book-manager/src/book.controller.ts index b4f0259..3a8d65a 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.controller.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.controller.ts @@ -9,7 +9,7 @@ import { Patch, Post, } from '@nestjs/common'; -import { Repository } from '../../../dist'; +import { TransactionalRepository } from '../../../dist'; import { AudioBook, Book, PaperBook } from './book'; type PartialBook = { id: string } & Partial; @@ -38,7 +38,7 @@ function deserialise(plainBook: any): T { export class BookController { constructor( @Inject('BOOK_REPOSITORY') - private readonly bookRepository: Repository, + private readonly bookRepository: TransactionalRepository, ) {} @Get() diff --git a/examples/nestjs-mongoose-book-manager/src/book.repository.ts b/examples/nestjs-mongoose-book-manager/src/book.repository.ts index e596a40..adfd2f8 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.repository.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.repository.ts @@ -1,18 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { InjectConnection } from '@nestjs/mongoose'; +import { Connection } from 'mongoose'; import { IllegalArgumentException, - MongooseRepository, - Repository, + MongooseTransactionalRepository, + TransactionalRepository, } from 'monguito'; import { AudioBook, Book, PaperBook } from './book'; import { AudioBookSchema, BookSchema, PaperBookSchema } from './book.schemas'; -import { Injectable } from '@nestjs/common'; -import { Connection } from 'mongoose'; -import { InjectConnection } from '@nestjs/mongoose'; @Injectable() export class MongooseBookRepository - extends MongooseRepository - implements Repository + extends MongooseTransactionalRepository + implements TransactionalRepository { constructor(@InjectConnection() connection: Connection) { super( From a8a89f72364dbd3735d31d92c011c5928d50f93f Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:00:10 +0100 Subject: [PATCH 26/48] fix: remove non-required types for next release Do not release runInTransaction and its associated types this time. --- src/index.ts | 3 --- src/util/transaction.ts | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6ec1bc3..7d6cecc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,6 @@ import { ValidationException, } from './util/exceptions'; import { AuditableSchema, BaseSchema, extendSchema } from './util/schema'; -import { DbCallback, runInTransaction } from './util/transaction'; export { Auditable, @@ -23,7 +22,6 @@ export { AuditableSchema, BaseSchema, Constructor, - DbCallback, Entity, IllegalArgumentException, MongooseRepository, @@ -37,5 +35,4 @@ export { ValidationException, extendSchema, isAuditable, - runInTransaction, }; diff --git a/src/util/transaction.ts b/src/util/transaction.ts index f4c9a7c..d2461cc 100644 --- a/src/util/transaction.ts +++ b/src/util/transaction.ts @@ -3,14 +3,14 @@ import mongoose, { ClientSession, Connection } from 'mongoose'; /** * Models a callback function that writes to and reads from the database using a session. */ -export type DbCallback = (session: ClientSession) => Promise; +type DbCallback = (session: ClientSession) => Promise; /** * Specifies some transaction options, specifically the Mongoose connection object to create a transaction * session from and the maximum amount of times that the callback function is to be retried in the case of * any MongoDB transient transaction error. */ -export type TransactionOptions = { +type TransactionOptions = { connection?: Connection; retries?: number; }; @@ -26,7 +26,7 @@ export async function runInTransaction( callback: DbCallback, options?: TransactionOptions, ): Promise { - return await innerRunIntransaction(callback, 0, options); + return await recursiveRunIntransaction(callback, 0, options); } async function startSession(connection?: Connection): Promise { @@ -39,7 +39,7 @@ async function startSession(connection?: Connection): Promise { const DEFAULT_MAX_RETRIES = 3; -async function innerRunIntransaction( +async function recursiveRunIntransaction( callback: DbCallback, attempt: number, options?: TransactionOptions, @@ -56,7 +56,7 @@ async function innerRunIntransaction( isTransientTransactionError(error) && attempt < (options?.retries ?? DEFAULT_MAX_RETRIES) ) { - return innerRunIntransaction(callback, ++attempt, options); + return recursiveRunIntransaction(callback, ++attempt, options); } throw error; } finally { From bcbe030cf61d135a9314371d9b92af3a69c98b83 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:19:05 +0100 Subject: [PATCH 27/48] refactor: wrap session within new OperationOptions type --- src/mongoose.repository.ts | 12 ++++++++---- src/mongoose.transactional-repository.ts | 2 +- src/repository.ts | 13 ++++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index b64b1bf..b65077f 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -7,7 +7,11 @@ import mongoose, { UpdateQuery, } from 'mongoose'; import { Optional } from 'typescript-optional'; -import { PartialEntityWithId, Repository } from './repository'; +import { + OperationOptions, + PartialEntityWithId, + Repository, +} from './repository'; import { isAuditable } from './util/audit'; import { Entity } from './util/entity'; import { @@ -146,19 +150,19 @@ export abstract class MongooseRepository> async save( entity: S | PartialEntityWithId, userId?: string, - session?: ClientSession, + options?: OperationOptions, ): Promise { if (!entity) throw new IllegalArgumentException('The given entity must be valid'); try { let document; if (!entity.id) { - document = await this.insert(entity as S, userId, session); + document = await this.insert(entity as S, userId, options?.session); } else { document = await this.update( entity as PartialEntityWithId, userId, - session, + options?.session, ); } if (document) return this.instantiateFrom(document) as S; diff --git a/src/mongoose.transactional-repository.ts b/src/mongoose.transactional-repository.ts index db0fc23..bdc9f64 100644 --- a/src/mongoose.transactional-repository.ts +++ b/src/mongoose.transactional-repository.ts @@ -32,7 +32,7 @@ export abstract class MongooseTransactionalRepository< async (session: ClientSession) => await Promise.all( entities.map( - async (entity) => await this.save(entity, userId, session), + async (entity) => await this.save(entity, userId, { session }), ), ), { connection: this.connection }, diff --git a/src/repository.ts b/src/repository.ts index c4796b8..702f14b 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -1,3 +1,4 @@ +import { ClientSession } from 'mongoose'; import { Optional } from 'typescript-optional'; import { Entity } from './util/entity'; import { SearchOptions } from './util/search-options'; @@ -11,6 +12,14 @@ export type PartialEntityWithId = { __t?: string; } & Partial; +/** + * Specifies some operation options e.g., a Mongoose session required in operations to run within a transaction. + */ +export type OperationOptions = { + userId?: string; + session?: ClientSession; +}; + /** * Specifies a list of common database CRUD operations. */ @@ -34,7 +43,8 @@ export interface Repository { /** * Saves (insert or update) an entity. * @param {S | PartialEntityWithId} entity the entity to save. - * @param {string} userId (optional) the ID of the user executing the action. + * @param {string=} userId (optional) the ID of the user executing the action. + * @param {OperationOptions} OperationOptions (optional) operation options. * @returns {Promise} the saved entity. * @throws {IllegalArgumentException} if the given entity is `undefined` or `null` or * specifies an `id` not matching any existing entity. @@ -43,6 +53,7 @@ export interface Repository { save: ( entity: S | PartialEntityWithId, userId?: string, + options?: OperationOptions, ) => Promise; /** From a743ca51f494b69663ea9bc5b9ae73f6557276ed Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Fri, 26 Jan 2024 00:05:59 +0100 Subject: [PATCH 28/48] refactor: rename transactional references to atomic --- .../nestjs-mongoose-book-manager/src/book.controller.ts | 4 ++-- .../nestjs-mongoose-book-manager/src/book.repository.ts | 8 ++++---- src/{transactional-repository.ts => atomic-repository.ts} | 3 +-- src/index.ts | 8 ++++---- ...tional-repository.ts => mongoose.atomic-repository.ts} | 8 ++++---- test/repository/auditable.book.repository.test.ts | 4 ++-- test/repository/auditable.book.repository.ts | 2 +- ...-repository.test.ts => book.atomic-repository.test.ts} | 8 ++++---- ...nsactional-repository.ts => book.atomic-repository.ts} | 4 ++-- 9 files changed, 24 insertions(+), 25 deletions(-) rename src/{transactional-repository.ts => atomic-repository.ts} (91%) rename src/{mongoose.transactional-repository.ts => mongoose.atomic-repository.ts} (81%) rename test/repository/{book.transactional-repository.test.ts => book.atomic-repository.test.ts} (98%) rename test/repository/{book.transactional-repository.ts => book.atomic-repository.ts} (68%) diff --git a/examples/nestjs-mongoose-book-manager/src/book.controller.ts b/examples/nestjs-mongoose-book-manager/src/book.controller.ts index 3a8d65a..ce51453 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.controller.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.controller.ts @@ -9,7 +9,7 @@ import { Patch, Post, } from '@nestjs/common'; -import { TransactionalRepository } from '../../../dist'; +import { AtomicRepository } from '../../../dist'; import { AudioBook, Book, PaperBook } from './book'; type PartialBook = { id: string } & Partial; @@ -38,7 +38,7 @@ function deserialise(plainBook: any): T { export class BookController { constructor( @Inject('BOOK_REPOSITORY') - private readonly bookRepository: TransactionalRepository, + private readonly bookRepository: AtomicRepository, ) {} @Get() diff --git a/examples/nestjs-mongoose-book-manager/src/book.repository.ts b/examples/nestjs-mongoose-book-manager/src/book.repository.ts index adfd2f8..2938005 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.repository.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.repository.ts @@ -2,17 +2,17 @@ import { Injectable } from '@nestjs/common'; import { InjectConnection } from '@nestjs/mongoose'; import { Connection } from 'mongoose'; import { + AtomicRepository, IllegalArgumentException, - MongooseTransactionalRepository, - TransactionalRepository, + MongooseAtomicRepository, } from 'monguito'; import { AudioBook, Book, PaperBook } from './book'; import { AudioBookSchema, BookSchema, PaperBookSchema } from './book.schemas'; @Injectable() export class MongooseBookRepository - extends MongooseTransactionalRepository - implements TransactionalRepository + extends MongooseAtomicRepository + implements AtomicRepository { constructor(@InjectConnection() connection: Connection) { super( diff --git a/src/transactional-repository.ts b/src/atomic-repository.ts similarity index 91% rename from src/transactional-repository.ts rename to src/atomic-repository.ts index 8e943ef..664d3d2 100644 --- a/src/transactional-repository.ts +++ b/src/atomic-repository.ts @@ -4,8 +4,7 @@ import { Entity } from './util/entity'; /** * Specifies a list of common database CRUD operations that must execute in a database transaction. */ -export interface TransactionalRepository - extends Repository { +export interface AtomicRepository extends Repository { /** * Saves (insert or update) a list of entities. * @param {S[] | PartialEntityWithId[]} entities the list of entities to save. diff --git a/src/index.ts b/src/index.ts index 7d6cecc..b5af11a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ +import { AtomicRepository } from './atomic-repository'; +import { MongooseAtomicRepository } from './mongoose.atomic-repository'; import { Constructor, MongooseRepository, TypeData, TypeMap, } from './mongoose.repository'; -import { MongooseTransactionalRepository } from './mongoose.transactional-repository'; import { PartialEntityWithId, Repository } from './repository'; -import { TransactionalRepository } from './transactional-repository'; import { Auditable, AuditableClass, isAuditable } from './util/audit'; import { Entity } from './util/entity'; import { @@ -17,6 +17,7 @@ import { import { AuditableSchema, BaseSchema, extendSchema } from './util/schema'; export { + AtomicRepository, Auditable, AuditableClass, AuditableSchema, @@ -24,11 +25,10 @@ export { Constructor, Entity, IllegalArgumentException, + MongooseAtomicRepository, MongooseRepository, - MongooseTransactionalRepository, PartialEntityWithId, Repository, - TransactionalRepository, TypeData, TypeMap, UndefinedConstructorException, diff --git a/src/mongoose.transactional-repository.ts b/src/mongoose.atomic-repository.ts similarity index 81% rename from src/mongoose.transactional-repository.ts rename to src/mongoose.atomic-repository.ts index bdc9f64..be82c9b 100644 --- a/src/mongoose.transactional-repository.ts +++ b/src/mongoose.atomic-repository.ts @@ -1,18 +1,18 @@ import { ClientSession, Connection, UpdateQuery } from 'mongoose'; +import { AtomicRepository } from './atomic-repository'; import { MongooseRepository, TypeMap } from './mongoose.repository'; import { PartialEntityWithId } from './repository'; -import { TransactionalRepository } from './transactional-repository'; import { Entity } from './util/entity'; import { runInTransaction } from './util/transaction'; /** - * Abstract Mongoose-based implementation of the {@link TransactionalRepository} interface. + * Abstract Mongoose-based implementation of the {@link AtomicRepository} interface. */ -export abstract class MongooseTransactionalRepository< +export abstract class MongooseAtomicRepository< T extends Entity & UpdateQuery, > extends MongooseRepository - implements TransactionalRepository + implements AtomicRepository { /** * Sets up the underlying configuration to enable database operation execution. diff --git a/test/repository/auditable.book.repository.test.ts b/test/repository/auditable.book.repository.test.ts index c4ae58f..04dc777 100644 --- a/test/repository/auditable.book.repository.test.ts +++ b/test/repository/auditable.book.repository.test.ts @@ -5,7 +5,7 @@ import { deleteAll, setupConnection, } from '../util/mongo-server'; -import { AuditableMongooseBookRepository } from './auditable.book.repository'; +import { MongooseAuditableBookRepository } from './auditable.book.repository'; describe('Given an instance of auditable book repository and a user ID', () => { let bookRepository: Repository; @@ -13,7 +13,7 @@ describe('Given an instance of auditable book repository and a user ID', () => { beforeAll(async () => { setupConnection(); - bookRepository = new AuditableMongooseBookRepository(); + bookRepository = new MongooseAuditableBookRepository(); }); describe('when creating an auditable book', () => { diff --git a/test/repository/auditable.book.repository.ts b/test/repository/auditable.book.repository.ts index 9e64ec7..9054ac9 100644 --- a/test/repository/auditable.book.repository.ts +++ b/test/repository/auditable.book.repository.ts @@ -5,7 +5,7 @@ import { AuditablePaperBookSchema, } from './auditable.book.schema'; -export class AuditableMongooseBookRepository extends MongooseRepository { +export class MongooseAuditableBookRepository extends MongooseRepository { constructor() { super({ Default: { type: AuditableBook, schema: AuditableBookSchema }, diff --git a/test/repository/book.transactional-repository.test.ts b/test/repository/book.atomic-repository.test.ts similarity index 98% rename from test/repository/book.transactional-repository.test.ts rename to test/repository/book.atomic-repository.test.ts index f9d16f3..ebb75d0 100644 --- a/test/repository/book.transactional-repository.test.ts +++ b/test/repository/book.atomic-repository.test.ts @@ -1,5 +1,5 @@ import { PartialEntityWithId } from '../../src'; -import { TransactionalRepository } from '../../src/transactional-repository'; +import { AtomicRepository } from '../../src/atomic-repository'; import { IllegalArgumentException, ValidationException, @@ -18,18 +18,18 @@ import { insert, setupConnection, } from '../util/mongo-server'; -import { MongooseBookTransactionalRepository } from './book.transactional-repository'; +import { MongooseBookAtomicRepository } from './book.atomic-repository'; const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; describe('Given an instance of book repository', () => { - let bookRepository: TransactionalRepository; + let bookRepository: AtomicRepository; beforeAll(async () => { await setupConnection(MongoServerType.REPLICA_SET); - bookRepository = new MongooseBookTransactionalRepository(); + bookRepository = new MongooseBookAtomicRepository(); // Wait until the repository is properly connected to Mongoose's connection await sleep(50); }); diff --git a/test/repository/book.transactional-repository.ts b/test/repository/book.atomic-repository.ts similarity index 68% rename from test/repository/book.transactional-repository.ts rename to test/repository/book.atomic-repository.ts index 537c010..eff71bd 100644 --- a/test/repository/book.transactional-repository.ts +++ b/test/repository/book.atomic-repository.ts @@ -1,8 +1,8 @@ -import { MongooseTransactionalRepository } from '../../src'; +import { MongooseAtomicRepository } from '../../src/mongoose.atomic-repository'; import { AudioBook, Book, PaperBook } from '../domain/book'; import { AudioBookSchema, BookSchema, PaperBookSchema } from './book.schema'; -export class MongooseBookTransactionalRepository extends MongooseTransactionalRepository { +export class MongooseBookAtomicRepository extends MongooseAtomicRepository { constructor() { super({ Default: { type: Book, schema: BookSchema }, From c9335ba48dd418033c867775cb8406a1f44e7d7d Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:55:11 +0100 Subject: [PATCH 29/48] feat: add deleteAll operation --- src/atomic-repository.ts | 17 ++++ src/mongoose.atomic-repository.ts | 16 +++- .../repository/book.atomic-repository.test.ts | 85 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/atomic-repository.ts b/src/atomic-repository.ts index 664d3d2..5070d96 100644 --- a/src/atomic-repository.ts +++ b/src/atomic-repository.ts @@ -1,6 +1,14 @@ import { PartialEntityWithId, Repository } from './repository'; import { Entity } from './util/entity'; +/** + * Specifies options for the `deleteAll` operation. + * - `filter`: a MongoDB query object to select the entities to be deleted + */ +export interface DeleteOptions { + filter?: any; +} + /** * Specifies a list of common database CRUD operations that must execute in a database transaction. */ @@ -18,4 +26,13 @@ export interface AtomicRepository extends Repository { entities: (S | PartialEntityWithId)[], userId?: string, ) => Promise; + + /** + * Deletes all the entities that match the given filter, if any. No filter specification will + * result in the deletion of all entities. + * @param {DeleteOptions=} options (optional) deletion options. + * @returns {number} the number of deleted entities. + * @see {@link DeleteOptions} + */ + deleteAll: (options?: DeleteOptions) => Promise; } diff --git a/src/mongoose.atomic-repository.ts b/src/mongoose.atomic-repository.ts index be82c9b..9fa0301 100644 --- a/src/mongoose.atomic-repository.ts +++ b/src/mongoose.atomic-repository.ts @@ -1,8 +1,9 @@ import { ClientSession, Connection, UpdateQuery } from 'mongoose'; -import { AtomicRepository } from './atomic-repository'; +import { AtomicRepository, DeleteOptions } from './atomic-repository'; import { MongooseRepository, TypeMap } from './mongoose.repository'; import { PartialEntityWithId } from './repository'; import { Entity } from './util/entity'; +import { IllegalArgumentException } from './util/exceptions'; import { runInTransaction } from './util/transaction'; /** @@ -38,4 +39,17 @@ export abstract class MongooseAtomicRepository< { connection: this.connection }, ); } + + /** @inheritdoc */ + async deleteAll(options?: DeleteOptions): Promise { + if (options?.filter === null) { + throw new IllegalArgumentException('Null filters are disallowed'); + } + return await runInTransaction( + async (session: ClientSession) => + (await this.entityModel.deleteMany(options?.filter, { session })) + .deletedCount, + { connection: this.connection }, + ); + } } diff --git a/test/repository/book.atomic-repository.test.ts b/test/repository/book.atomic-repository.test.ts index ebb75d0..3a5480f 100644 --- a/test/repository/book.atomic-repository.test.ts +++ b/test/repository/book.atomic-repository.test.ts @@ -445,6 +445,91 @@ describe('Given an instance of book repository', () => { }); }); + describe('when deleting a list of books', () => { + let storedBook: Book, storedPaperBook: PaperBook; + + beforeEach(async () => { + const bookToStore = bookFixture({ isbn: '1942788342' }); + const storedBookId = await insert(bookToStore, 'books'); + storedBook = new Book({ + ...bookToStore, + id: storedBookId, + }); + + const paperBookToStore = paperBookFixture({ isbn: '1942788343' }); + const storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + }); + + describe('that does not include any filter', () => { + it('deletes all books', async () => { + const deletedBooks = await bookRepository.deleteAll(); + expect(deletedBooks).toBe(2); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks.length).toBe(0); + }); + }); + + describe('that includes a null filter', () => { + it('throws an exception', async () => { + await expect( + bookRepository.deleteAll({ filter: null as unknown as object }), + ).rejects.toThrowError(IllegalArgumentException); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks).toEqual([storedBook, storedPaperBook]); + }); + }); + + describe('that includes a filter matching no book', () => { + it('does not delete any book', async () => { + const deletedBooks = await bookRepository.deleteAll({ + filter: { hostingPlatforms: ['Audible'] }, + }); + expect(deletedBooks).toBe(0); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks).toEqual([storedBook, storedPaperBook]); + }); + }); + + describe('that includes a filter matching some books', () => { + it('only deletes the matching books', async () => { + const deletedBooks = await bookRepository.deleteAll({ + filter: { isbn: '1942788343' }, + }); + expect(deletedBooks).toBe(1); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks).toEqual([storedBook]); + }); + }); + + describe('that includes a filter matching all books', () => { + it('deletes all books', async () => { + const deletedBooks = await bookRepository.deleteAll({ + filter: { isbn: ['1942788342', '1942788343'] }, + }); + expect(deletedBooks).toBe(2); + + const storedBooks = await bookRepository.findAll(); + expect(storedBooks.length).toBe(0); + }); + }); + + afterEach(async () => { + await deleteAll('books'); + }); + }); + afterAll(async () => { await closeMongoConnection(); }); From ac0d2f067a25a20ffb68a4b2a491ed0c94a9762d Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 29 Jan 2024 01:07:52 +0100 Subject: [PATCH 30/48] test: fix replica set config --- .../test/book.controller.test.ts | 4 ++++ .../test/util/mongo-server.ts | 17 +++++------------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts index 983c050..a63d088 100644 --- a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts +++ b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts @@ -9,6 +9,7 @@ import { findOne, insert, rootMongooseTestModule, + setupConnection, } from './util/mongo-server'; const timeout = 30000; @@ -22,6 +23,8 @@ describe('Given the book manager controller', () => { imports: [rootMongooseTestModule(), AppModule], }).compile(); + await setupConnection(); + bookManager = appModule.createNestApplication(); await bookManager.init(); }, timeout); @@ -198,6 +201,7 @@ describe('Given the book manager controller', () => { }); afterAll(async () => { + await bookManager.close(); await closeMongoConnection(); }, timeout); }); diff --git a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts index 05bbb48..17ead89 100644 --- a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts +++ b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts @@ -3,18 +3,15 @@ import { MongoMemoryReplSet } from 'mongodb-memory-server'; import mongoose from 'mongoose'; import { Entity } from '../../../../src'; +const dbName = 'test'; let mongoServer: MongoMemoryReplSet; -let dbName: string; -export const rootMongooseTestModule = ( - options: MongooseModuleOptions = {}, - dbName = 'book-repository', -) => +export const rootMongooseTestModule = (options: MongooseModuleOptions = {}) => MongooseModule.forRootAsync({ useFactory: async () => { mongoServer = await MongoMemoryReplSet.create({ instanceOpts: [{ port: 27016 }], - replSet: { dbName, count: 1 }, + replSet: { name: 'rs0', dbName, count: 1 }, }); const mongoUri = mongoServer.getUri(); return { @@ -31,7 +28,6 @@ export const insert = async ( collection: string, discriminatorKey?: string, ) => { - await setupConnection(); if (discriminatorKey) { entity['__t'] = discriminatorKey; } @@ -42,22 +38,19 @@ export const insert = async ( }; export const findOne = async (filter: any, collection: string) => { - await setupConnection(); return await mongoose.connection.db.collection(collection).findOne(filter); }; export const deleteAll = async (collections: string[]) => { - await setupConnection(); await Promise.all( collections.map((c) => mongoose.connection.db.collection(c).deleteMany({})), ); return; }; -const setupConnection = async () => { +export const setupConnection = async () => { if (!mongoServer) return; - await mongoose.connect(mongoServer.getUri()); - mongoose.connection.useDb(dbName); + await mongoose.connect(mongoServer.getUri(), { dbName }); }; export const closeMongoConnection = async () => { From 672b43f351310a2e9366c50cab28ace3b3251c30 Mon Sep 17 00:00:00 2001 From: Josu Martinez Date: Tue, 30 Jan 2024 00:07:53 +0100 Subject: [PATCH 31/48] test: split Mongo standalone vs replica set tests --- .../test/book.atomic-controller.test.ts | 82 +++++++++++++++++++ .../test/book.controller.test.ts | 30 +------ .../test/util/mongo-server.ts | 24 +++++- 3 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts diff --git a/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts new file mode 100644 index 0000000..4e6c323 --- /dev/null +++ b/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts @@ -0,0 +1,82 @@ +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import * as request from 'supertest'; +import { AppModule } from '../src/app.module'; +import { AudioBook, PaperBook } from '../src/book'; +import { + closeMongoConnection, + deleteAll, + findOne, + insert, + rootMongooseReplicaSetMongoTestModule, + setupConnection, +} from './util/mongo-server'; + +const timeout = 30000; + +describe('Given the book manager controller', () => { + let bookManager: INestApplication; + let storedPaperBook: PaperBook; + + beforeAll(async () => { + const appModule = await Test.createTestingModule({ + imports: [rootMongooseReplicaSetMongoTestModule(), AppModule], + }).compile(); + + await setupConnection(); + + bookManager = appModule.createNestApplication(); + await bookManager.init(); + }, timeout); + + beforeEach(async () => { + const paperBookToStore = new PaperBook({ + title: 'Effective Java', + description: 'Great book on the Java programming language', + edition: 3, + }); + const storedPaperBookId = await insert( + paperBookToStore, + 'books', + PaperBook.name, + ); + storedPaperBook = new PaperBook({ + ...paperBookToStore, + id: storedPaperBookId, + }); + }); + + describe('when saving a list of books', () => { + describe('that includes an invalid book', () => { + it('then returns a bad request HTTP status code', () => { + const booksToStore = [ + { + title: 'The Sandman', + description: 'Fantastic fantasy audio book', + hostingPlatforms: ['Audible'], + } as AudioBook, + { + description: 'Invalid paper book description', + edition: 1, + } as PaperBook, + ]; + return request(bookManager.getHttpServer()) + .post('/books/all') + .send(booksToStore) + .then(async (result) => { + expect(result.status).toEqual(HttpStatus.BAD_REQUEST); + expect(await findOne({ title: 'The Sandman' }, 'books')).toBeNull(); + }); + }); + }); + }); + + afterEach(async () => { + await deleteAll(['books']); + }); + + afterAll(async () => { + await bookManager.close(); + await closeMongoConnection(); + }, timeout); +}); diff --git a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts index a63d088..5bc1aef 100644 --- a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts +++ b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts @@ -6,9 +6,8 @@ import { AudioBook, PaperBook } from '../src/book'; import { closeMongoConnection, deleteAll, - findOne, insert, - rootMongooseTestModule, + rootMongooseStandaloneMongoTestModule, setupConnection, } from './util/mongo-server'; @@ -20,7 +19,7 @@ describe('Given the book manager controller', () => { beforeAll(async () => { const appModule = await Test.createTestingModule({ - imports: [rootMongooseTestModule(), AppModule], + imports: [rootMongooseStandaloneMongoTestModule(), AppModule], }).compile(); await setupConnection(); @@ -147,31 +146,6 @@ describe('Given the book manager controller', () => { }); }); - describe('when saving a list of books', () => { - describe('that includes an invalid book', () => { - it('then returns a bad request HTTP status code', () => { - const booksToStore = [ - { - title: 'The Sandman', - description: 'Fantastic fantasy audio book', - hostingPlatforms: ['Audible'], - } as AudioBook, - { - description: 'Invalid paper book description', - edition: 1, - } as PaperBook, - ]; - return request(bookManager.getHttpServer()) - .post('/books/all') - .send(booksToStore) - .then(async (result) => { - expect(result.status).toEqual(HttpStatus.BAD_REQUEST); - expect(await findOne({ title: 'The Sandman' }, 'books')).toBeNull(); - }); - }); - }); - }); - describe('when deleting a book', () => { describe('that is not stored', () => { it('then returns false', () => { diff --git a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts index 17ead89..2e90679 100644 --- a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts +++ b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts @@ -1,12 +1,30 @@ import { MongooseModule, MongooseModuleOptions } from '@nestjs/mongoose'; -import { MongoMemoryReplSet } from 'mongodb-memory-server'; +import { MongoMemoryReplSet, MongoMemoryServer } from 'mongodb-memory-server'; import mongoose from 'mongoose'; import { Entity } from '../../../../src'; const dbName = 'test'; -let mongoServer: MongoMemoryReplSet; +let mongoServer: MongoMemoryServer | MongoMemoryReplSet; -export const rootMongooseTestModule = (options: MongooseModuleOptions = {}) => +export const rootMongooseStandaloneMongoTestModule = ( + options: MongooseModuleOptions = {}, +) => + MongooseModule.forRootAsync({ + useFactory: async () => { + mongoServer = await MongoMemoryServer.create({ + instance: { dbName, port: 27016 }, + }); + const mongoUri = mongoServer.getUri(); + return { + uri: mongoUri, + ...options, + }; + }, + }); + +export const rootMongooseReplicaSetMongoTestModule = ( + options: MongooseModuleOptions = {}, +) => MongooseModule.forRootAsync({ useFactory: async () => { mongoServer = await MongoMemoryReplSet.create({ From 6a04609f2c1c74b7e6b849a8dd1a89fd67c3c285 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:15:58 +0100 Subject: [PATCH 32/48] test: add happy path for atomic saveAll Also fixed a couple of errors in an attempt to run both Mongo standalone and replica set based tests concurrently. --- .../src/book.controller.ts | 2 +- .../test/book.atomic-controller.test.ts | 58 ++++++++++++++++--- .../test/book.controller.test.ts | 18 +++--- .../test/util/mongo-server.ts | 2 +- test/util/mongo-server.ts | 4 +- 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/src/book.controller.ts b/examples/nestjs-mongoose-book-manager/src/book.controller.ts index ce51453..c6ec445 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.controller.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.controller.ts @@ -17,7 +17,7 @@ type PartialBook = { id: string } & Partial; function deserialiseAll(plainBooks: any[]): T[] { const books: T[] = []; for (const plainBook of plainBooks) { - books.push(deserialise(plainBook)); + books.push('id' in plainBook ? plainBook : deserialise(plainBook)); } return books; } diff --git a/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts index 4e6c323..bdc544f 100644 --- a/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts +++ b/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts @@ -33,7 +33,7 @@ describe('Given the book manager controller', () => { const paperBookToStore = new PaperBook({ title: 'Effective Java', description: 'Great book on the Java programming language', - edition: 3, + edition: 2, }); const storedPaperBookId = await insert( paperBookToStore, @@ -48,16 +48,15 @@ describe('Given the book manager controller', () => { describe('when saving a list of books', () => { describe('that includes an invalid book', () => { - it('then returns a bad request HTTP status code', () => { + it('returns a bad request HTTP status code', async () => { const booksToStore = [ { - title: 'The Sandman', - description: 'Fantastic fantasy audio book', - hostingPlatforms: ['Audible'], - } as AudioBook, + id: storedPaperBook.id, + edition: 3, + } as Partial, { - description: 'Invalid paper book description', - edition: 1, + description: 'Paper book to insert with no title (thus, invalid)', + edition: 3, } as PaperBook, ]; return request(bookManager.getHttpServer()) @@ -65,10 +64,51 @@ describe('Given the book manager controller', () => { .send(booksToStore) .then(async (result) => { expect(result.status).toEqual(HttpStatus.BAD_REQUEST); - expect(await findOne({ title: 'The Sandman' }, 'books')).toBeNull(); + expect(await findOne({ title: 'Accelerate' }, 'books')).toBeNull(); + const updatedPaperBook = await findOne( + { title: 'Effective Java' }, + 'books', + ); + expect(updatedPaperBook).toBeDefined(); + expect(updatedPaperBook!.edition).toBe(2); + }); + }); + }); + + describe('that includes valid books', () => { + it('returns the created books', async () => { + const booksToStore = [ + { + title: 'Accelerate', + description: 'Building High Performing Technology Organizations', + hostingPlatforms: ['Audible'], + } as AudioBook, + { + id: storedPaperBook.id, + edition: 3, + } as Partial, + ]; + return request(bookManager.getHttpServer()) + .post('/books/all') + .send(booksToStore) + .then(async (result) => { + expect(result.status).toEqual(HttpStatus.CREATED); + expect( + await findOne({ title: 'Accelerate' }, 'books'), + ).toBeDefined(); + const updatedPaperBook = await findOne( + { title: 'Effective Java' }, + 'books', + ); + expect(updatedPaperBook).toBeDefined(); + expect(updatedPaperBook!.edition).toBe(3); }); }); }); + + afterEach(async () => { + await deleteAll(['books']); + }); }); afterEach(async () => { diff --git a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts index 5bc1aef..9995f2a 100644 --- a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts +++ b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts @@ -46,7 +46,7 @@ describe('Given the book manager controller', () => { }); describe('when finding all books', () => { - it('then retrieves all the existent books', () => { + it('retrieves all the existent books', () => { return request(bookManager.getHttpServer()) .get('/books') .expect(HttpStatus.OK) @@ -58,7 +58,7 @@ describe('Given the book manager controller', () => { describe('when creating a new book', () => { describe('that is invalid', () => { - it('then returns a bad request HTTP status code', () => { + it('returns a bad request HTTP status code', () => { return request(bookManager.getHttpServer()) .post('/books') .send() @@ -67,7 +67,7 @@ describe('Given the book manager controller', () => { }); describe('that is specifies an ID', () => { - it('then returns a bad request HTTP status code', () => { + it('returns a bad request HTTP status code', () => { const audioBookToStore = { id: '000000000000000000000000', title: 'The Sandman', @@ -82,7 +82,7 @@ describe('Given the book manager controller', () => { }); describe('that is valid', () => { - it('then returns the created book', () => { + it('returns the created book', () => { const audioBookToStore = { title: 'The Sandman', description: 'Fantastic fantasy audio book', @@ -103,7 +103,7 @@ describe('Given the book manager controller', () => { describe('when updating a book', () => { describe('that is invalid', () => { - it('then returns a bad request HTTP status code', () => { + it('returns a bad request HTTP status code', () => { return request(bookManager.getHttpServer()) .patch('/books') .send() @@ -112,7 +112,7 @@ describe('Given the book manager controller', () => { }); describe('that is not stored', () => { - it('then returns a bad request HTTP status code', () => { + it('returns a bad request HTTP status code', () => { const paperBookToUpdate = { id: '000000000000000000000000', edition: 4, @@ -125,7 +125,7 @@ describe('Given the book manager controller', () => { }); describe('that is stored', () => { - it('then returns the created book', () => { + it('returns the updated book', () => { const paperBookToUpdate = { id: storedPaperBook.id, edition: 4, @@ -148,7 +148,7 @@ describe('Given the book manager controller', () => { describe('when deleting a book', () => { describe('that is not stored', () => { - it('then returns false', () => { + it('returns false', () => { return request(bookManager.getHttpServer()) .delete('/books/000000000000000000000000') .expect(HttpStatus.OK) @@ -159,7 +159,7 @@ describe('Given the book manager controller', () => { }); describe('that is stored', () => { - it('then returns true', () => { + it('returns true', () => { return request(bookManager.getHttpServer()) .delete(`/books/${storedPaperBook.id}`) .expect(HttpStatus.OK) diff --git a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts index 2e90679..51dd626 100644 --- a/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts +++ b/examples/nestjs-mongoose-book-manager/test/util/mongo-server.ts @@ -12,7 +12,7 @@ export const rootMongooseStandaloneMongoTestModule = ( MongooseModule.forRootAsync({ useFactory: async () => { mongoServer = await MongoMemoryServer.create({ - instance: { dbName, port: 27016 }, + instance: { dbName, port: 27017 }, }); const mongoUri = mongoServer.getUri(); return { diff --git a/test/util/mongo-server.ts b/test/util/mongo-server.ts index f01ec40..db6a22e 100644 --- a/test/util/mongo-server.ts +++ b/test/util/mongo-server.ts @@ -32,9 +32,7 @@ export const findOne = async (filter: any, collection: string) => { }; export const findById = async (id: string, collection: string) => { - return await mongoose.connection.db - .collection(collection) - .findOne({ id: id }); + return await mongoose.connection.db.collection(collection).findOne({ id }); }; export const deleteAll = async (collection: string) => { From c2ef0e1d8ffd625187e6f2d5d55d0c3ddc6fc20f Mon Sep 17 00:00:00 2001 From: Josu Martinez Date: Tue, 30 Jan 2024 23:23:40 +0100 Subject: [PATCH 33/48] test: add test for deleteAll --- .../src/book.controller.ts | 7 ++++++- .../test/book.atomic-controller.test.ts | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/src/book.controller.ts b/examples/nestjs-mongoose-book-manager/src/book.controller.ts index c6ec445..405d41a 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.controller.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.controller.ts @@ -69,7 +69,7 @@ export class BookController { @Body({ transform: (plainBooks) => deserialiseAll(plainBooks), }) - books: Book[], + books: (Book | PartialBook)[], ): Promise { try { return await this.bookRepository.saveAll(books); @@ -83,6 +83,11 @@ export class BookController { return this.bookRepository.deleteById(id); } + @Delete() + async deleteAll(): Promise { + return this.bookRepository.deleteAll(); + } + private async save(book: Book | PartialBook): Promise { try { return await this.bookRepository.save(book); diff --git a/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts index bdc544f..71101f9 100644 --- a/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts +++ b/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts @@ -105,9 +105,19 @@ describe('Given the book manager controller', () => { }); }); }); + }); - afterEach(async () => { - await deleteAll(['books']); + describe('when deleting all books', () => { + it('deletes all books', async () => { + return request(bookManager.getHttpServer()) + .delete('/books') + .then(async (result) => { + expect(result.status).toEqual(HttpStatus.OK); + expect(result.text).toBe('1'); + expect( + await findOne({ title: 'Effective Java' }, 'books'), + ).toBeNull(); + }); }); }); From c1956eac145b38c5d6d753e52a032d5cb6b6cd1a Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sat, 3 Feb 2024 23:30:00 +0100 Subject: [PATCH 34/48] docs: add doccumentation on transactional operations --- README.md | 68 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index aa21b6c..345bed3 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ `monguito` is a lightweight and type-safe [MongoDB](https://www.mongodb.com/) handling library for [Node.js](https://nodejs.org/) applications that implements both the Abstract [Repository](https://www.martinfowler.com/eaaCatalog/repository.html) and the [Polymorphic](https://www.mongodb.com/developer/products/mongodb/polymorphic-pattern/) patterns. -It allows developers to define any custom MongoDB repository in a fast, easy, and structured manner, releasing them from having to write basic CRUD operations, while decoupling domain from persistence logic. Despite its small size, it includes several optional features such as seamless audit data handling support. +It allows you (dear developer) to define any custom MongoDB repository in a fast, easy, and structured manner, releasing you from having to write all the boilerplate code for basic CRUD operations, while enabling you to decouple domain and persistence logic. Moreover, despite its small size, it includes several optional features such as seamless audit data handling support. -Last but not least, `monguito` wraps [Mongoose](https://mongoosejs.com/), a very popular and solid MongoDB ODM for Node.js applications. Furthermore, it leverages Mongoose [schemas](https://mongoosejs.com/docs/guide.html) to enable developers focus on their own persistance models, leaving everything else to the library. +Last but not least, `monguito` wraps [Mongoose](https://mongoosejs.com/), a very popular and solid MongoDB ODM for Node.js applications. `monguito` enables you to use any Mongoose feature such as [aggregation pipelines](https://mongoosejs.com/docs/api/aggregate.html) or [middleware functions](https://mongoosejs.com/docs/middleware.html). Furthermore, it leverages Mongoose [schemas](https://mongoosejs.com/docs/guide.html) to enable developers focus on their own persistance models, leaving everything else to the library. # Getting Started @@ -101,14 +101,17 @@ and `PaperBook` and `AudioBook` as subtypes. Code complexity to support polymorp at `MongooseRepository`; all that is required is that `MongooseRepository` receives a map describing the domain model. Each map entry key relates to a domain object type, and the related entry value is a reference to the constructor and the database [schema](https://mongoosejs.com/docs/guide.html) of such domain object. The `Default` key is mandatory and -relates to the supertype, while the rest of the keys relate to the subtypes. Beware that subtype keys are named after -the type name. If it so happens that you do not have any subtype in your domain model, no problem! Just specify the -domain object that your custom repository is to handle as the sole map key-value, and you are done. +relates to the supertype, while the rest of the keys relate to the subtypes. -Regarding schemas: We believe that writing your own database schemas is a good practice, as opposed of using decorators -at your domain model. This is mainly to avoid marrying the underlying infrastructure, thus enabling you to easily get -rid of this repository logic if something better comes in. It also allows you to have more control on the persistence -properties of your domain objects. After all, database definition is a thing that Mongoose is really rock-solid about. +Beware that subtype keys are named after the type name. If it so happens that you do not have any subtype in your domain +model, no problem! Just specify the domain object that your custom repository is to handle as the sole map key-value, +and you are done. + +## Transactional Operations + +You may expect all CRUD operations to be [atomic](). However, not all Mongoose functions are natively atomic e.g., `Model.updateMany`. Similarly, `monguito` includes some operations that require some transaction handling logic to ensure atomicity. This is the case of `saveAll` and `deleteAll`. Furthermore, transactional logic does not work in MongoDB standalone instances; transactions can only be performed on MongoDB instances that are running as part of a larger cluster, which could be either a sharded database cluster or a replica set. + +All considered, `monguito` specifies `saveAll` and `deleteAll` in `TransactionalRepository`, an interface that extends `Repository`, and implements them in `MongooseTransactionalRepository`, a class that inherits `MongooseRepository`. All you need to do to ensure that both behave as atomic operations is to connect to a MongoDB cluster instead of a standalone instance. Jump to the [Examples](#examples) section to see examples of `monguito` with MongoDB replica set. # Supported Database Operations @@ -134,6 +137,19 @@ interface Repository { object type (e.g., `PaperBook` or `AudioBook`). This way, you can be sure that the resulting values of the CRUD operations are of the type you expect. +Besides, this is the specification for `TransactionalRepository`, the interface that includes all the operations specified at `Repository` as well as some operations requiring some transactional logic to ensure atomicity. As mentioned earlier, `MongooseTransactionalRepository` is the class that implements `TransactionalRepository`. + +```typescript +export interface AtomicRepository extends Repository { + saveAll: ( + entities: (S | PartialEntityWithId)[], + userId?: string, + ) => Promise; + + deleteAll: (filters?: any) => Promise; +} +``` + ### `findById` Returns an [`Optional`](https://github.com/bromne/typescript-optional#readme) entity matching the given `id`. @@ -150,8 +166,7 @@ Check it out! Returns an array including all the persisted entities, or an empty array otherwise. This operation accepts some additional and non-required search `options`: -- `filters`: a [MongoDB entity field-based query](https://www.mongodb.com/docs/manual/tutorial/query-documents/) - to filter results +- `filters`: a [MongoDB query](https://www.mongodb.com/docs/manual/tutorial/query-documents/) to filter results - `sortBy`: a [MongoDB sort criteria](https://www.mongodb.com/docs/manual/reference/method/cursor.sort/#mongodb-method-cursor.sort) to return results in some sorted order - `pageable`: pagination data (i.e., `pageNumber` and `offset`) required to return a particular set of results. @@ -159,9 +174,7 @@ additional and non-required search `options`: ### `save` -Persists the given entity by either inserting or updating it and returns the persisted entity. It the entity does -not specify an `id`, this function inserts the entity. Otherwise, this function expects the entity to exist in the -collection; if it does, the function updates it or throwns exception if it does not. +Persists the given entity by either inserting or updating it and returns the persisted entity. It the entity specifies an `id` field, this function updates it, assuming that it exists in the collection. Otherwise, this operation results in an exception being thrown. On the contrary, if the entity does not specify an `id` field, it inserts it into the collection. Trying to persist a new entity that includes a developer specified `id` is considered a _system invariant violation_; only Mongoose is able to produce MongoDB identifiers to prevent `id` collisions and undesired entity updates. @@ -169,9 +182,23 @@ only Mongoose is able to produce MongoDB identifiers to prevent `id` collisions Finally, this function specifies an optional `userId` argument to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details). +### `saveAll` + +Persists the given list of entities by either inserting or updating them and returns the persisted entities. As with the `save` operation, `saveAll` inserts or updates each entity of the list based on the existence of the `id` field. + +> [!WARNING] +> This operation is only guaranteed to be atomic when executed against a MongoDB cluster. + ### `deleteById` -Deletes an entity matching the given `id` if it exists. When it does, the function returns `true`. Otherwise, it returns `false`. +Deletes an entity which `id` field value that matches the given `id`. When it does, the function returns `true`. Otherwise, it returns `false`. + +### `deleteAll` + +Deletes all the entities that match the MongoDB query specified within the `options` parameter. This operation returns the total amount of deleted entities. + +> [!WARNING] +> This operation is only guaranteed to be atomic when executed against a MongoDB cluster. # Examples @@ -187,8 +214,8 @@ recommend reading the following section. If you are to inject your newly created repository into an application that uses a Node.js-based framework (e.g., [NestJS](https://nestjs.com/) or [Express](https://expressjs.com/)) then you may want to do some extra effort and -follow the [Dependency Inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) and _depend on -abstractions, not implementations_. To do so, you simply need to add one extra artefact to your code: +follow the [Dependency Inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) to _depend on +abstractions, not implementations_. Simply need to add one extra artefact to your code: ```typescript interface BookRepository extends Repository { @@ -272,7 +299,12 @@ inheritance-based programming language, and we strongly believe that you are ent will, with no dependencies to other libraries. But all that being said, you may decide not to use it at all, and that would be just fine. All you need to do is ensure that your domain objects specify an optional `id` field. -## Utilities to Define Your Custom Schemas +## Define Your Custom Schemas + +We believe that writing your own database schemas is a good practice, as opposed of using decorators +at your domain model. This is mainly to avoid marrying the underlying infrastructure, thus enabling you to easily get +rid of this repository logic if something better comes in. It also allows you to have more control on the persistence +properties of your domain objects. After all, database definition is a thing that Mongoose is really rock-solid about. The `extendSchema` function eases the specification of the Mongoose schemas of your domain model and let `monguito` to handle the required implementation details. This function is specially convenient when defining schemas for From 86779c498f4da90ea71f451fe87ffb8a68467861 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sat, 3 Feb 2024 23:47:17 +0100 Subject: [PATCH 35/48] docs: update documentation on transactional operations --- README.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 345bed3..c975e5d 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,14 @@ and you are done. ## Transactional Operations -You may expect all CRUD operations to be [atomic](). However, not all Mongoose functions are natively atomic e.g., `Model.updateMany`. Similarly, `monguito` includes some operations that require some transaction handling logic to ensure atomicity. This is the case of `saveAll` and `deleteAll`. Furthermore, transactional logic does not work in MongoDB standalone instances; transactions can only be performed on MongoDB instances that are running as part of a larger cluster, which could be either a sharded database cluster or a replica set. +You may expect all CRUD operations to be [atomic](). However, not all Mongoose functions are natively atomic e.g., `Model.updateMany`. Similarly, `monguito` includes some operations that require some transaction handling logic to ensure atomicity. This is the case of `saveAll` and `deleteAll`. Furthermore, transactional logic does not work in MongoDB standalone instances; transactions can only be performed on MongoDB instances that are running as part of a larger cluster, which could be either a sharded database cluster or a replica set. Using a MongoDB cluster in your production environment is [the official recommendation](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/), by the way. -All considered, `monguito` specifies `saveAll` and `deleteAll` in `TransactionalRepository`, an interface that extends `Repository`, and implements them in `MongooseTransactionalRepository`, a class that inherits `MongooseRepository`. All you need to do to ensure that both behave as atomic operations is to connect to a MongoDB cluster instead of a standalone instance. Jump to the [Examples](#examples) section to see examples of `monguito` with MongoDB replica set. +All considered, `monguito` specifies `saveAll` and `deleteAll` in `TransactionalRepository`, an interface that extends `Repository`, and implements them in `MongooseTransactionalRepository`, a class that inherits `MongooseRepository`. All you need to do to ensure that both behave as atomic operations is to connect to a MongoDB cluster instead of a standalone instance. For further information on how to use `monguito` to access MongoDB replica set jump to the [examples](#examples) section. # Supported Database Operations +## Basic CRUD Operations + Let's have a look to `Repository`, the generic interface implemented by `MongooseRepository`. Keep in mind that the current semantics for these operations are those provided at `MongooseRepository`. If you want any of these operations to behave differently then you must override it at your custom repository implementation. @@ -137,19 +139,6 @@ interface Repository { object type (e.g., `PaperBook` or `AudioBook`). This way, you can be sure that the resulting values of the CRUD operations are of the type you expect. -Besides, this is the specification for `TransactionalRepository`, the interface that includes all the operations specified at `Repository` as well as some operations requiring some transactional logic to ensure atomicity. As mentioned earlier, `MongooseTransactionalRepository` is the class that implements `TransactionalRepository`. - -```typescript -export interface AtomicRepository extends Repository { - saveAll: ( - entities: (S | PartialEntityWithId)[], - userId?: string, - ) => Promise; - - deleteAll: (filters?: any) => Promise; -} -``` - ### `findById` Returns an [`Optional`](https://github.com/bromne/typescript-optional#readme) entity matching the given `id`. @@ -182,29 +171,40 @@ only Mongoose is able to produce MongoDB identifiers to prevent `id` collisions Finally, this function specifies an optional `userId` argument to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details). -### `saveAll` +### `deleteById` -Persists the given list of entities by either inserting or updating them and returns the persisted entities. As with the `save` operation, `saveAll` inserts or updates each entity of the list based on the existence of the `id` field. +Deletes an entity which `id` field value that matches the given `id`. When it does, the function returns `true`. Otherwise, it returns `false`. + +## Transactional CRUD Operations + +Besides, this is the specification for `TransactionalRepository`, the interface that includes all the operations specified at `Repository` as well as some operations requiring some transactional logic to ensure atomicity. As mentioned earlier, `MongooseTransactionalRepository` is the class that implements `TransactionalRepository`. + +```typescript +export interface AtomicRepository extends Repository { + saveAll: ( + entities: (S | PartialEntityWithId)[], + userId?: string, + ) => Promise; + + deleteAll: (filters?: any) => Promise; +} +``` > [!WARNING] -> This operation is only guaranteed to be atomic when executed against a MongoDB cluster. +> The following operations are only guaranteed to be atomic when executed against a MongoDB cluster. -### `deleteById` +### `saveAll` -Deletes an entity which `id` field value that matches the given `id`. When it does, the function returns `true`. Otherwise, it returns `false`. +Persists the given list of entities by either inserting or updating them and returns the persisted entities. As with the `save` operation, `saveAll` inserts or updates each entity of the list based on the existence of the `id` field. ### `deleteAll` Deletes all the entities that match the MongoDB query specified within the `options` parameter. This operation returns the total amount of deleted entities. -> [!WARNING] -> This operation is only guaranteed to be atomic when executed against a MongoDB cluster. - # Examples -You may find an example of how to instantiate and use a repository that performs CRUD operations over instances -of `Book` and its aforementioned subtypes under [`book.repository.test.ts`](test/book.repository.test.ts). This is a -complete set of unit test cases used to validate this project. +You may find an example of how to instantiate and use a repository that performs basic CRUD operations over instances +of `Book` and its aforementioned subtypes under [`book.repository.test.ts`](test/book.repository.test.ts). You may also find an example on `monguito`'s transactional CRUD operations on [`book.transactional-repository.test.ts`](test/book.transactional-repository.test.ts). Moreover, if you are interested in knowing how to inject and use a custom repository in a NestJS application, visit [`nestjs-mongoose-book-manager`](examples/nestjs-mongoose-book-manager). But before jumping to that link, we From 4ad8a7d97403e8cafe2e04980660ac04b4b773bb Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:44:33 +0100 Subject: [PATCH 36/48] docs: update documentation on transactional operations --- README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c975e5d..1c58436 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ const books: Book[] = bookRepository.findAll(); No more leaking of the persistence logic into your domain/application logic! 🀩 -## Polymorphic Domain Model Specification +# Polymorphic Domain Model Specification `MongooseBookRepository` handles database operations over a _polymorphic_ domain model that defines `Book` as supertype and `PaperBook` and `AudioBook` as subtypes. Code complexity to support polymorphic domain models is hidden @@ -107,18 +107,14 @@ Beware that subtype keys are named after the type name. If it so happens that yo model, no problem! Just specify the domain object that your custom repository is to handle as the sole map key-value, and you are done. -## Transactional Operations - -You may expect all CRUD operations to be [atomic](). However, not all Mongoose functions are natively atomic e.g., `Model.updateMany`. Similarly, `monguito` includes some operations that require some transaction handling logic to ensure atomicity. This is the case of `saveAll` and `deleteAll`. Furthermore, transactional logic does not work in MongoDB standalone instances; transactions can only be performed on MongoDB instances that are running as part of a larger cluster, which could be either a sharded database cluster or a replica set. Using a MongoDB cluster in your production environment is [the official recommendation](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/), by the way. - -All considered, `monguito` specifies `saveAll` and `deleteAll` in `TransactionalRepository`, an interface that extends `Repository`, and implements them in `MongooseTransactionalRepository`, a class that inherits `MongooseRepository`. All you need to do to ensure that both behave as atomic operations is to connect to a MongoDB cluster instead of a standalone instance. For further information on how to use `monguito` to access MongoDB replica set jump to the [examples](#examples) section. - # Supported Database Operations +We support two kinds of CRUD operations: _basic_ and _transactional_. Both kinds specify [atomic]() operations; however, while the former kind of operations are inherently atomic, the latter kind of operations require some transactional logic to ensure atomicity. Moreover, while basic CRUD operations may be safely executed on a MongoDB standalone instance, transactional CRUD operations are only atomic when run as part of a larger cluster e.g., a sharded cluster or a replica set. Using a MongoDB cluster in your production environment is, by the way, [the official recommendation](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/). + ## Basic CRUD Operations Let's have a look to `Repository`, the generic interface implemented by `MongooseRepository`. Keep in mind that the current -semantics for these operations are those provided at `MongooseRepository`. If you want any of these operations to behave +semantics for these operations are those provided at `MongooseRepository`; if you want any of these operations to behave differently then you must override it at your custom repository implementation. ```typescript @@ -177,7 +173,7 @@ Deletes an entity which `id` field value that matches the given `id`. When it do ## Transactional CRUD Operations -Besides, this is the specification for `TransactionalRepository`, the interface that includes all the operations specified at `Repository` as well as some operations requiring some transactional logic to ensure atomicity. As mentioned earlier, `MongooseTransactionalRepository` is the class that implements `TransactionalRepository`. +Let's now see the specification for `TransactionalRepository`, an interface that defines transactional CRUD operations. This interface also is an extension of `Repository` for convenience purposes, as you most likely want to also invoke basic CRUD operations using a single repository instance. Futhermore, `MongooseTransactionalRepository` is the class that implements `TransactionalRepository`. ```typescript export interface AtomicRepository extends Repository { @@ -191,7 +187,7 @@ export interface AtomicRepository extends Repository { ``` > [!WARNING] -> The following operations are only guaranteed to be atomic when executed against a MongoDB cluster. +> The following operations are only guaranteed to be atomic when executed on a MongoDB cluster. ### `saveAll` From 6e4bca3d8cef10e7a9d90c74ddb8044397122e88 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sun, 4 Feb 2024 12:01:16 +0100 Subject: [PATCH 37/48] refactor: rename atomic references to transactional Since atomicity is the desired property of transactional repository operations. --- README.md | 3 ++- .../src/book.controller.ts | 4 ++-- .../src/book.repository.ts | 8 ++++---- ....test.ts => book.transactional-controller.test.ts} | 0 src/index.ts | 8 ++++---- ...sitory.ts => mongoose.transactional-repository.ts} | 11 +++++++---- ...omic-repository.ts => transactional-repository.ts} | 3 ++- ....test.ts => book.transactional-repository.test.ts} | 9 ++++----- ...repository.ts => book.transactional-repository.ts} | 4 ++-- 9 files changed, 27 insertions(+), 23 deletions(-) rename examples/nestjs-mongoose-book-manager/test/{book.atomic-controller.test.ts => book.transactional-controller.test.ts} (100%) rename src/{mongoose.atomic-repository.ts => mongoose.transactional-repository.ts} (85%) rename src/{atomic-repository.ts => transactional-repository.ts} (94%) rename test/repository/{book.atomic-repository.test.ts => book.transactional-repository.test.ts} (98%) rename test/repository/{book.atomic-repository.ts => book.transactional-repository.ts} (68%) diff --git a/README.md b/README.md index 1c58436..f74c251 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,8 @@ Deletes an entity which `id` field value that matches the given `id`. When it do Let's now see the specification for `TransactionalRepository`, an interface that defines transactional CRUD operations. This interface also is an extension of `Repository` for convenience purposes, as you most likely want to also invoke basic CRUD operations using a single repository instance. Futhermore, `MongooseTransactionalRepository` is the class that implements `TransactionalRepository`. ```typescript -export interface AtomicRepository extends Repository { +export interface TransactionalRepository + extends Repository { saveAll: ( entities: (S | PartialEntityWithId)[], userId?: string, diff --git a/examples/nestjs-mongoose-book-manager/src/book.controller.ts b/examples/nestjs-mongoose-book-manager/src/book.controller.ts index 405d41a..2c24212 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.controller.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.controller.ts @@ -9,7 +9,7 @@ import { Patch, Post, } from '@nestjs/common'; -import { AtomicRepository } from '../../../dist'; +import { TransactionalRepository } from '../../../dist'; import { AudioBook, Book, PaperBook } from './book'; type PartialBook = { id: string } & Partial; @@ -38,7 +38,7 @@ function deserialise(plainBook: any): T { export class BookController { constructor( @Inject('BOOK_REPOSITORY') - private readonly bookRepository: AtomicRepository, + private readonly bookRepository: TransactionalRepository, ) {} @Get() diff --git a/examples/nestjs-mongoose-book-manager/src/book.repository.ts b/examples/nestjs-mongoose-book-manager/src/book.repository.ts index 2938005..adfd2f8 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.repository.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.repository.ts @@ -2,17 +2,17 @@ import { Injectable } from '@nestjs/common'; import { InjectConnection } from '@nestjs/mongoose'; import { Connection } from 'mongoose'; import { - AtomicRepository, IllegalArgumentException, - MongooseAtomicRepository, + MongooseTransactionalRepository, + TransactionalRepository, } from 'monguito'; import { AudioBook, Book, PaperBook } from './book'; import { AudioBookSchema, BookSchema, PaperBookSchema } from './book.schemas'; @Injectable() export class MongooseBookRepository - extends MongooseAtomicRepository - implements AtomicRepository + extends MongooseTransactionalRepository + implements TransactionalRepository { constructor(@InjectConnection() connection: Connection) { super( diff --git a/examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.transactional-controller.test.ts similarity index 100% rename from examples/nestjs-mongoose-book-manager/test/book.atomic-controller.test.ts rename to examples/nestjs-mongoose-book-manager/test/book.transactional-controller.test.ts diff --git a/src/index.ts b/src/index.ts index b5af11a..7d6cecc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ -import { AtomicRepository } from './atomic-repository'; -import { MongooseAtomicRepository } from './mongoose.atomic-repository'; import { Constructor, MongooseRepository, TypeData, TypeMap, } from './mongoose.repository'; +import { MongooseTransactionalRepository } from './mongoose.transactional-repository'; import { PartialEntityWithId, Repository } from './repository'; +import { TransactionalRepository } from './transactional-repository'; import { Auditable, AuditableClass, isAuditable } from './util/audit'; import { Entity } from './util/entity'; import { @@ -17,7 +17,6 @@ import { import { AuditableSchema, BaseSchema, extendSchema } from './util/schema'; export { - AtomicRepository, Auditable, AuditableClass, AuditableSchema, @@ -25,10 +24,11 @@ export { Constructor, Entity, IllegalArgumentException, - MongooseAtomicRepository, MongooseRepository, + MongooseTransactionalRepository, PartialEntityWithId, Repository, + TransactionalRepository, TypeData, TypeMap, UndefinedConstructorException, diff --git a/src/mongoose.atomic-repository.ts b/src/mongoose.transactional-repository.ts similarity index 85% rename from src/mongoose.atomic-repository.ts rename to src/mongoose.transactional-repository.ts index 9fa0301..53013dc 100644 --- a/src/mongoose.atomic-repository.ts +++ b/src/mongoose.transactional-repository.ts @@ -1,19 +1,22 @@ import { ClientSession, Connection, UpdateQuery } from 'mongoose'; -import { AtomicRepository, DeleteOptions } from './atomic-repository'; import { MongooseRepository, TypeMap } from './mongoose.repository'; import { PartialEntityWithId } from './repository'; +import { + DeleteOptions, + TransactionalRepository, +} from './transactional-repository'; import { Entity } from './util/entity'; import { IllegalArgumentException } from './util/exceptions'; import { runInTransaction } from './util/transaction'; /** - * Abstract Mongoose-based implementation of the {@link AtomicRepository} interface. + * Abstract Mongoose-based implementation of the {@link TransactionalRepository} interface. */ -export abstract class MongooseAtomicRepository< +export abstract class MongooseTransactionalRepository< T extends Entity & UpdateQuery, > extends MongooseRepository - implements AtomicRepository + implements TransactionalRepository { /** * Sets up the underlying configuration to enable database operation execution. diff --git a/src/atomic-repository.ts b/src/transactional-repository.ts similarity index 94% rename from src/atomic-repository.ts rename to src/transactional-repository.ts index 5070d96..e6681f1 100644 --- a/src/atomic-repository.ts +++ b/src/transactional-repository.ts @@ -12,7 +12,8 @@ export interface DeleteOptions { /** * Specifies a list of common database CRUD operations that must execute in a database transaction. */ -export interface AtomicRepository extends Repository { +export interface TransactionalRepository + extends Repository { /** * Saves (insert or update) a list of entities. * @param {S[] | PartialEntityWithId[]} entities the list of entities to save. diff --git a/test/repository/book.atomic-repository.test.ts b/test/repository/book.transactional-repository.test.ts similarity index 98% rename from test/repository/book.atomic-repository.test.ts rename to test/repository/book.transactional-repository.test.ts index 3a5480f..17d3d12 100644 --- a/test/repository/book.atomic-repository.test.ts +++ b/test/repository/book.transactional-repository.test.ts @@ -1,5 +1,4 @@ -import { PartialEntityWithId } from '../../src'; -import { AtomicRepository } from '../../src/atomic-repository'; +import { PartialEntityWithId, TransactionalRepository } from '../../src'; import { IllegalArgumentException, ValidationException, @@ -18,18 +17,18 @@ import { insert, setupConnection, } from '../util/mongo-server'; -import { MongooseBookAtomicRepository } from './book.atomic-repository'; +import { MongooseBookTransactionalRepository } from './book.transactional-repository'; const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; describe('Given an instance of book repository', () => { - let bookRepository: AtomicRepository; + let bookRepository: TransactionalRepository; beforeAll(async () => { await setupConnection(MongoServerType.REPLICA_SET); - bookRepository = new MongooseBookAtomicRepository(); + bookRepository = new MongooseBookTransactionalRepository(); // Wait until the repository is properly connected to Mongoose's connection await sleep(50); }); diff --git a/test/repository/book.atomic-repository.ts b/test/repository/book.transactional-repository.ts similarity index 68% rename from test/repository/book.atomic-repository.ts rename to test/repository/book.transactional-repository.ts index eff71bd..537c010 100644 --- a/test/repository/book.atomic-repository.ts +++ b/test/repository/book.transactional-repository.ts @@ -1,8 +1,8 @@ -import { MongooseAtomicRepository } from '../../src/mongoose.atomic-repository'; +import { MongooseTransactionalRepository } from '../../src'; import { AudioBook, Book, PaperBook } from '../domain/book'; import { AudioBookSchema, BookSchema, PaperBookSchema } from './book.schema'; -export class MongooseBookAtomicRepository extends MongooseAtomicRepository { +export class MongooseBookTransactionalRepository extends MongooseTransactionalRepository { constructor() { super({ Default: { type: Book, schema: BookSchema }, From ccb9d0cf0eb838fd72c4ff745f9d7f7f169fcfd6 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 5 Feb 2024 00:21:50 +0100 Subject: [PATCH 38/48] fix: rename DeleteOptions.filter to filters --- src/mongoose.transactional-repository.ts | 4 ++-- src/transactional-repository.ts | 2 +- test/repository/book.transactional-repository.test.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mongoose.transactional-repository.ts b/src/mongoose.transactional-repository.ts index 53013dc..ecb8f61 100644 --- a/src/mongoose.transactional-repository.ts +++ b/src/mongoose.transactional-repository.ts @@ -45,12 +45,12 @@ export abstract class MongooseTransactionalRepository< /** @inheritdoc */ async deleteAll(options?: DeleteOptions): Promise { - if (options?.filter === null) { + if (options?.filters === null) { throw new IllegalArgumentException('Null filters are disallowed'); } return await runInTransaction( async (session: ClientSession) => - (await this.entityModel.deleteMany(options?.filter, { session })) + (await this.entityModel.deleteMany(options?.filters, { session })) .deletedCount, { connection: this.connection }, ); diff --git a/src/transactional-repository.ts b/src/transactional-repository.ts index e6681f1..9eac700 100644 --- a/src/transactional-repository.ts +++ b/src/transactional-repository.ts @@ -6,7 +6,7 @@ import { Entity } from './util/entity'; * - `filter`: a MongoDB query object to select the entities to be deleted */ export interface DeleteOptions { - filter?: any; + filters?: any; } /** diff --git a/test/repository/book.transactional-repository.test.ts b/test/repository/book.transactional-repository.test.ts index 17d3d12..ee0d4a6 100644 --- a/test/repository/book.transactional-repository.test.ts +++ b/test/repository/book.transactional-repository.test.ts @@ -480,7 +480,7 @@ describe('Given an instance of book repository', () => { describe('that includes a null filter', () => { it('throws an exception', async () => { await expect( - bookRepository.deleteAll({ filter: null as unknown as object }), + bookRepository.deleteAll({ filters: null as unknown as object }), ).rejects.toThrowError(IllegalArgumentException); const storedBooks = await bookRepository.findAll(); @@ -491,7 +491,7 @@ describe('Given an instance of book repository', () => { describe('that includes a filter matching no book', () => { it('does not delete any book', async () => { const deletedBooks = await bookRepository.deleteAll({ - filter: { hostingPlatforms: ['Audible'] }, + filters: { hostingPlatforms: ['Audible'] }, }); expect(deletedBooks).toBe(0); @@ -503,7 +503,7 @@ describe('Given an instance of book repository', () => { describe('that includes a filter matching some books', () => { it('only deletes the matching books', async () => { const deletedBooks = await bookRepository.deleteAll({ - filter: { isbn: '1942788343' }, + filters: { isbn: '1942788343' }, }); expect(deletedBooks).toBe(1); @@ -515,7 +515,7 @@ describe('Given an instance of book repository', () => { describe('that includes a filter matching all books', () => { it('deletes all books', async () => { const deletedBooks = await bookRepository.deleteAll({ - filter: { isbn: ['1942788342', '1942788343'] }, + filters: { isbn: ['1942788342', '1942788343'] }, }); expect(deletedBooks).toBe(2); From 07b5e404eee9e05238fd8e8615360dacff2c5264 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:09:55 +0100 Subject: [PATCH 39/48] docs: update NestJS example with new monguito transactional operations --- .../nestjs-mongoose-book-manager/README.md | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/README.md b/examples/nestjs-mongoose-book-manager/README.md index 10e10cf..0d60436 100644 --- a/examples/nestjs-mongoose-book-manager/README.md +++ b/examples/nestjs-mongoose-book-manager/README.md @@ -1,12 +1,9 @@ -This is an example of how to use `monguito` in a NestJS application that uses MongoDB. It is a dummy book -manager that exposes three simple endpoints i.e., create, update, and delete a book, as well as list all -books. A book may be of type `Book` or any of its subtypes i.e., `PaperBook` and `AudioBook`. +This is an example of how to use `monguito` in a NestJS application that uses a MongoDB replica set instance with +a single node. It is a dummy book manager that exposes an endpoint for each CRUD operation offered by `monguito`. +A book may be of type `Book` or any of its subtypes i.e., `PaperBook` and `AudioBook`. -> **Warning** -> -> Some basic knowledge on [NestJS](https://docs.nestjs.com/) is assumed, as well as that you have read the main -> documentation of [monguito](../../README.md). The goal of this documentation is not to provide a comprehensive -> guide on `monguito` usage. Thus, you may want to check the [sample application code](./src) as you go reading. +> [!WARNING] +> Some basic knowledge on [NestJS](https://docs.nestjs.com/) is assumed, as well as that you have read the main documentation of [monguito](../../README.md). The goal of this documentation is not to provide a comprehensive guide on `monguito` usage. Thus, you may want to check the [sample application code](./src) as you go reading. # Main Contents @@ -28,7 +25,7 @@ $ yarn install ## Run The application requires a running instance of MongoDB. It includes a `docker-compose.yml` file that will fire up a -MongoDB instance, assuming that Docker Desktop is running. +MongoDB replica set instance, assuming that Docker Desktop is running. ```bash # run the NestJS application as well as the MongoDB Docker container @@ -109,13 +106,13 @@ superclass includes an `id` field in its definition. `MongooseBookRepository` is a Mongoose-based book repository implementation class. Since it does not include any additional database operation, there is no need to create a custom repository interface for it. In this case, we can -directly implement the `Repository` interface. The definition of `MongooseBookRepository` is as follows: +directly implement the `TransactionalRepository` interface. The definition of `MongooseBookRepository` is as follows: ```typescript @Injectable() export class MongooseBookRepository - extends MongooseRepository - implements Repository + extends MongooseTransactionalRepository + implements TransactionalRepository { constructor(@InjectConnection() connection: Connection) { super( @@ -143,12 +140,12 @@ store all of your entities in collections of the same database or different data databases, you may need to specify a NestJS provider for each of them. NestJS providers are discussed later in this document. -This implementation of `MongooseBookRepository` overrides the `deleteById` operation defined at `MongooseRepository`, -also modifying its semantics; while `MongooseRepository.deleteById()` performs hard book -deletion, `MongooseBookRepository.deleteById()` performs soft book deletion. You may realise that this operation updates -the value of the book field `isDeleted` to `true`. In order to achieve it, `Book` must include this field in its -definition. You may find the full definition of the book domain model used by this sample -application [here](src/book.ts). +This implementation of `MongooseBookRepository` overrides the `deleteById` operation defined at `MongooseRepository` +(i.e., `MongooseTransactionalRepository`'s extension), also modifying its semantics; while `MongooseRepository.deleteById()` +performs hard book deletion, `MongooseBookRepository.deleteById()` performs soft book deletion. You may realise that +this operation updates the value of the book field `isDeleted` to `true`. In order to achieve it, `Book` must include +this field in its definition. You may find the full definition of the book domain model used by this sample application +[here](src/book.ts). ## Book Controller @@ -158,6 +155,14 @@ contents are as follows: ```typescript type PartialBook = { id: string } & Partial; +function deserialiseAll(plainBooks: any[]): T[] { + const books: T[] = []; + for (const plainBook of plainBooks) { + books.push('id' in plainBook ? plainBook : deserialise(plainBook)); + } + return books; +} + function deserialise(plainBook: any): T { let book = null; if (plainBook.edition) { @@ -174,7 +179,7 @@ function deserialise(plainBook: any): T { export class BookController { constructor( @Inject('BOOK_REPOSITORY') - private readonly bookRepository: Repository, + private readonly bookRepository: TransactionalRepository, ) {} @Get() @@ -200,11 +205,30 @@ export class BookController { return this.save(book); } + @Post('/all') + async saveAll( + @Body({ + transform: (plainBooks) => deserialiseAll(plainBooks), + }) + books: (Book | PartialBook)[], + ): Promise { + try { + return await this.bookRepository.saveAll(books); + } catch (error) { + throw new BadRequestException(error); + } + } + @Delete(':id') async deleteById(@Param('id') id: string): Promise { return this.bookRepository.deleteById(id); } + @Delete() + async deleteAll(): Promise { + return this.bookRepository.deleteAll(); + } + private async save(book: Book | PartialBook): Promise { try { return await this.bookRepository.save(book); @@ -223,8 +247,8 @@ simple CRUD application that introducing services would be over-engineering. I r code necessary for the sake of maximising the actual purpose of this documentation: illustrate how to integrate `monguito` on a NodeJS-based enterprise application. -Moreover, you would probably not write a `deserialise` function to enable the transformation of JSON request bodies into -domain objects when dealing with `POST` requests. Instead, you would rather use +Moreover, you would probably not write a `deserialise` or `deserialiseAll` functions to enable the transformation of +JSON request bodies into domain objects when dealing with `POST` requests. Instead, you would rather use a [NestJS pipe](https://docs.nestjs.com/pipes#pipes) to do so, thus properly implementing the Single Responsibility principle. Once again, I wanted to share the simplest possible working example at the expense of not conveying to the recommended practices in NestJS application construction. That being said, I would highly recommend you to @@ -251,7 +275,10 @@ part of the second step: writing the last required class `AppModule`. The defini ```typescript @Module({ imports: [ - MongooseModule.forRoot('mongodb://localhost:27016/book-repository'), + MongooseModule.forRoot('mongodb://localhost:27016/book-repository', { + directConnection: true, + replicaSet: 'rs0', + }), ], providers: [ { From c6285ee39c9bb02944ed315a57afbc656194f0a5 Mon Sep 17 00:00:00 2001 From: Josu Martinez Date: Tue, 6 Feb 2024 00:30:24 +0100 Subject: [PATCH 40/48] docs: update NestJS app doc with transactional ops --- .../nestjs-mongoose-book-manager/README.md | 83 ++++++++++++++----- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/README.md b/examples/nestjs-mongoose-book-manager/README.md index 0d60436..fee1f94 100644 --- a/examples/nestjs-mongoose-book-manager/README.md +++ b/examples/nestjs-mongoose-book-manager/README.md @@ -298,45 +298,88 @@ that `BookController` is the sole controller for the book manager application. # Custom Repository Validation -This application comes with some e2e tests that you may find useful when validating your own NestJS application. You may -find the whole test infrastructure [here](./test/book.controller.test.ts). +This application comes with a couple of unit tests that you may find useful when validating your own NestJS application. +The first test suite validates the [basic CRUD operations](../../README.md/#basic-crud-operations) included in `BookController` and is encoded in the [book.controller.test.ts](./test/book.controller.test.ts) file. The second test suite validates the [transactional CRUD operations](../../README.md/#transactional-crud-operations) also written in `BookController` and is implemented on [book.transactional-controller.test.ts](./test/book.transactional-controller.test.ts). -First of all, you need to create a testing module for your app. Here is a way you can follow to do so: +As mentioned in `monguito`'s main documentation, basic CRUD operations may run on a MongoDB standalone instance. However, transactional CRUD operations can only run on a MongoDB cluster such as replica set. Therefore, the nature of basic and transactional CRUD operations determines the configuration of the aforementioned test suites: [book.controller.test.ts](./test/book.controller.test.ts) works with an in-memory version of standalone MongoDB, whereas [book.transactional-controller.test.ts](./test/book.transactional-controller.test.ts) operates over an in-memory version of MongoDB replica set. + +Let's now focus on the module configuration and application initialisation for these test files. Keep in mind that, in both cases, you first need to create a testing module for your app. + +## Initialisation of MongoDB Standalone-based App + +Here is how you initialise the test application required to run the tests described at [book.controller.test.ts](./test/book.controller.test.ts): ```typescript let bookManager: INestApplication; beforeAll(async () => { const appModule = await Test.createTestingModule({ - imports: [rootMongooseTestModule(), AppModule], + imports: [rootMongooseStandaloneMongoTestModule(), AppModule], }).compile(); + await setupConnection(); + bookManager = appModule.createNestApplication(); await bookManager.init(); -}); +}, timeout); ``` -You may want to create an instance of `MongoMemoryServer` (the main class exported by the library -[`mongodb-memory-server` NPM dependency](https://www.npmjs.com/package/mongodb-memory-server)) instead of a -full-blown MongoDB instance. This instance is vital to inject the custom repository at `BookController` at test runtime. -The creation of the instance is defined by the `rootMongooseTestModule` function included -at [`mongo-server.ts`](../../test/util/mongo-server.ts). This is its implementation: +You may want to create an instance of `MongoMemoryServer` (the main class exported by the library [`mongodb-memory-server` NPM dependency](https://www.npmjs.com/package/mongodb-memory-server)) instead of a full-blown MongoDB standalone instance. This instance is required to inject the custom repository at `BookController` at test runtime. The creation of the instance is done at the `rootMongooseStandaloneMongoTestModule` function included at [`mongo-server.ts`](../../test/util/mongo-server.ts). This is its implementation: ```typescript +const dbName = 'test'; let mongoServer: MongoMemoryServer; -export const rootMongooseTestModule = ( +export const rootMongooseStandaloneMongoTestModule = ( options: MongooseModuleOptions = {}, - port = 27016, - dbName = 'book-repository', ) => MongooseModule.forRootAsync({ useFactory: async () => { mongoServer = await MongoMemoryServer.create({ - instance: { - port, - dbName: dbName, - }, + instance: { dbName, port: 27017 }, + }); + const mongoUri = mongoServer.getUri(); + return { + uri: mongoUri, + ...options, + }; + }, + }); +``` + +## Initialisation of MongoDB Replica Set-based App + +Here is how you initialise the test application required to run the tests described at [book.transactional-controller.test.ts](./test/book.transactional-controller.test.ts): + +```typescript +let bookManager: INestApplication; + +beforeAll(async () => { + const appModule = await Test.createTestingModule({ + imports: [rootMongooseReplicaSetMongoTestModule(), AppModule], + }).compile(); + + await setupConnection(); + + bookManager = appModule.createNestApplication(); + await bookManager.init(); +}, timeout); +``` + +You may want to create an instance of `MongoMemoryReplSet` (also defined in the library [`mongodb-memory-server` NPM dependency](https://www.npmjs.com/package/mongodb-memory-server)) instead of a full-blown MongoDB replica set instance. This instance is required to inject the custom repository at `BookController` at test runtime. The creation of the instance is done at the `rootMongooseReplicaSetMongoTestModule` function included at [`mongo-server.ts`](../../test/util/mongo-server.ts). This is its implementation: + +```typescript +const dbName = 'test'; +let mongoServer: MongoMemoryServer; + +export const rootMongooseReplicaSetMongoTestModule = ( + options: MongooseModuleOptions = {}, +) => + MongooseModule.forRootAsync({ + useFactory: async () => { + mongoServer = await MongoMemoryReplSet.create({ + instanceOpts: [{ port: 27016 }], + replSet: { name: 'rs0', dbName, count: 1 }, }); const mongoUri = mongoServer.getUri(); return { @@ -347,11 +390,9 @@ export const rootMongooseTestModule = ( }); ``` -You may perceive that the `port` and `dbName` input parameters match those of the database connection -specified [earlier](#book-manager-module). This is no coincidence. +## Connection Setup -Finally, you can then use the `mongoServer` instance to perform several explicit Mongoose-based DB operations such as -those specified at [mongo-server.ts](../../test/util/mongo-server.ts). +You may have appreciated the invocation of `setupConnection` before the initialisation of both testing applications. This function tells Mongoose to connect to the pertaining MongoDB instance (standalone or replica set). You may find the details of this function as well as some other validation helper functions at [`mongo-server.ts`](../../test/util/mongo-server.ts). ## Run the Tests From 252e1b83ddbc969af1fb96d871701823ba8ab40d Mon Sep 17 00:00:00 2001 From: Josu Martinez Date: Tue, 6 Feb 2024 00:32:16 +0100 Subject: [PATCH 41/48] chore: add any coverage folder --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d6a0a17..4fd69af 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ lerna-debug.log* .DS_Store # Tests -/coverage +/**/coverage /.nyc_output # IDEs and editors From 2d12ba4f7c788f92c6289fa51070c4ff19606813 Mon Sep 17 00:00:00 2001 From: Josu Martinez Date: Wed, 7 Feb 2024 23:51:51 +0100 Subject: [PATCH 42/48] docs: update documentation --- README.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f74c251..45e6a6a 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ # What is `monguito`? -`monguito` is a lightweight and type-safe [MongoDB](https://www.mongodb.com/) handling library for [Node.js](https://nodejs.org/) applications that implements both the Abstract [Repository](https://www.martinfowler.com/eaaCatalog/repository.html) and the [Polymorphic](https://www.mongodb.com/developer/products/mongodb/polymorphic-pattern/) patterns. +`monguito` is a lightweight and type-safe [MongoDB](https://www.mongodb.com/) handling library for [Node.js](https://nodejs.org/) applications that implements both the abstract [repository](https://www.martinfowler.com/eaaCatalog/repository.html) and the [polymorphic](https://www.mongodb.com/developer/products/mongodb/polymorphic-pattern/) patterns. -It allows you (dear developer) to define any custom MongoDB repository in a fast, easy, and structured manner, releasing you from having to write all the boilerplate code for basic CRUD operations, while enabling you to decouple domain and persistence logic. Moreover, despite its small size, it includes several optional features such as seamless audit data handling support. +It allows you (dear developer) to define any custom MongoDB repository in a fast, easy, and structured manner, releasing you from having to write all the boilerplate code for basic CRUD operations, and also decoupling your domain layer from the persistence logic. Moreover, despite its small size, it includes several optional features such as seamless audit data handling support. Last but not least, `monguito` wraps [Mongoose](https://mongoosejs.com/), a very popular and solid MongoDB ODM for Node.js applications. `monguito` enables you to use any Mongoose feature such as [aggregation pipelines](https://mongoosejs.com/docs/api/aggregate.html) or [middleware functions](https://mongoosejs.com/docs/middleware.html). Furthermore, it leverages Mongoose [schemas](https://mongoosejs.com/docs/guide.html) to enable developers focus on their own persistance models, leaving everything else to the library. @@ -94,7 +94,7 @@ const books: Book[] = bookRepository.findAll(); No more leaking of the persistence logic into your domain/application logic! 🀩 -# Polymorphic Domain Model Specification +### Polymorphic Domain Model Specification `MongooseBookRepository` handles database operations over a _polymorphic_ domain model that defines `Book` as supertype and `PaperBook` and `AudioBook` as subtypes. Code complexity to support polymorphic domain models is hidden @@ -109,13 +109,13 @@ and you are done. # Supported Database Operations -We support two kinds of CRUD operations: _basic_ and _transactional_. Both kinds specify [atomic]() operations; however, while the former kind of operations are inherently atomic, the latter kind of operations require some transactional logic to ensure atomicity. Moreover, while basic CRUD operations may be safely executed on a MongoDB standalone instance, transactional CRUD operations are only atomic when run as part of a larger cluster e.g., a sharded cluster or a replica set. Using a MongoDB cluster in your production environment is, by the way, [the official recommendation](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/). +We support two kinds of CRUD operations: _basic_ and _transactional_. Both kinds specify [atomic]() operations; however, while the former are inherently atomic, the latter require some transactional logic to ensure atomicity. Moreover, basic CRUD operations can be safely executed on a MongoDB standalone instance, but transactional CRUD operations are only atomic when run as part of a larger cluster e.g., a sharded cluster or a replica set. Using a MongoDB cluster in your production environment is, by the way, [the official recommendation](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/). + +Let's now explore these two kinds of operations in detail. ## Basic CRUD Operations -Let's have a look to `Repository`, the generic interface implemented by `MongooseRepository`. Keep in mind that the current -semantics for these operations are those provided at `MongooseRepository`; if you want any of these operations to behave -differently then you must override it at your custom repository implementation. +`Repository` is the generic interface implemented by `MongooseRepository`. Its definition is as follows: ```typescript type PartialEntityWithId = { id: string } & Partial; @@ -135,6 +135,9 @@ interface Repository { object type (e.g., `PaperBook` or `AudioBook`). This way, you can be sure that the resulting values of the CRUD operations are of the type you expect. +> [!NOTE] +> Keep in mind that the current semantics for these operations are those provided at `MongooseRepository`; if you want any of these operations to behave differently then you must override it at your custom repository implementation. + ### `findById` Returns an [`Optional`](https://github.com/bromne/typescript-optional#readme) entity matching the given `id`. @@ -143,8 +146,7 @@ This value wraps an actual entity or `null` in case that no entity matches the g The `Optional` type is meant to create awareness about the nullable nature of the operation result on the custom repository clients. This type helps client code developers to easily reason about all possible result types without having to handle slippery `null` values or exceptions (i.e., the alternatives to `Optional`), as mentioned by Joshua Bloch in his book -Effective Java. Furthermore, the `Optional` API is quite complete and includes many elegant solutions to handle all use cases. -Check it out! +[Effective Java](https://www.oreilly.com/library/view/effective-java-3rd/9780134686097/). Furthermore, the `Optional` API is quite complete and includes many elegant solutions to handle all use cases. Check it out! ### `findAll` @@ -159,21 +161,20 @@ additional and non-required search `options`: ### `save` -Persists the given entity by either inserting or updating it and returns the persisted entity. It the entity specifies an `id` field, this function updates it, assuming that it exists in the collection. Otherwise, this operation results in an exception being thrown. On the contrary, if the entity does not specify an `id` field, it inserts it into the collection. +Persists the given entity by either inserting or updating it and returns the persisted entity. If the entity specifies an `id` field, this function updates it, unless it does not exist in the pertaining collection, in which case this operation results in an exception being thrown. Otherwise, if the entity does not specify an `id` field, it inserts it into the collection. -Trying to persist a new entity that includes a developer specified `id` is considered a _system invariant violation_; -only Mongoose is able to produce MongoDB identifiers to prevent `id` collisions and undesired entity updates. +Beware that trying to persist a new entity that includes a developer specified `id` is considered a _system invariant violation_; only Mongoose is able to produce MongoDB identifiers to prevent `id` collisions and undesired entity updates. Finally, this function specifies an optional `userId` argument to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details). ### `deleteById` -Deletes an entity which `id` field value that matches the given `id`. When it does, the function returns `true`. Otherwise, it returns `false`. +Deletes an entity which `id` field value matches the given `id`. When it does, the function returns `true`. Otherwise, it returns `false`. ## Transactional CRUD Operations -Let's now see the specification for `TransactionalRepository`, an interface that defines transactional CRUD operations. This interface also is an extension of `Repository` for convenience purposes, as you most likely want to also invoke basic CRUD operations using a single repository instance. Futhermore, `MongooseTransactionalRepository` is the class that implements `TransactionalRepository`. +Let's now explore the definition of `TransactionalRepository`, an interface that defines transactional CRUD operations. This interface is an extension of `Repository`, thus includes all the basic CRUD operations. Futhermore, `MongooseTransactionalRepository` is the class that implements `TransactionalRepository`. ```typescript export interface TransactionalRepository @@ -183,7 +184,7 @@ export interface TransactionalRepository userId?: string, ) => Promise; - deleteAll: (filters?: any) => Promise; + deleteAll: (options?: DeleteOptions) => Promise; } ``` @@ -192,16 +193,20 @@ export interface TransactionalRepository ### `saveAll` -Persists the given list of entities by either inserting or updating them and returns the persisted entities. As with the `save` operation, `saveAll` inserts or updates each entity of the list based on the existence of the `id` field. +Persists the given list of entities by either inserting or updating them and returns the list of persisted entities. As with the `save` operation, `saveAll` inserts or updates each entity of the list based on the existence of the `id` field. + +In the event of any error, this operation rollbacks all its changes. In other words, it does not save any given entity, thus guaranteeing operation atomicity. ### `deleteAll` -Deletes all the entities that match the MongoDB query specified within the `options` parameter. This operation returns the total amount of deleted entities. +Deletes all the entities that match the MongoDB `filters` query specified within the `options` parameter. This operation returns the total amount of deleted entities. + +In the event of any error, this operation rollbacks all its changes. In other words, it does not delete any entity, thus guaranteeing operation atomicity. # Examples You may find an example of how to instantiate and use a repository that performs basic CRUD operations over instances -of `Book` and its aforementioned subtypes under [`book.repository.test.ts`](test/book.repository.test.ts). You may also find an example on `monguito`'s transactional CRUD operations on [`book.transactional-repository.test.ts`](test/book.transactional-repository.test.ts). +of `Book` and its aforementioned subtypes at [`book.repository.test.ts`](test/book.repository.test.ts). You may also find an example on `monguito`'s transactional CRUD operations at [`book.transactional-repository.test.ts`](test/book.transactional-repository.test.ts). Moreover, if you are interested in knowing how to inject and use a custom repository in a NestJS application, visit [`nestjs-mongoose-book-manager`](examples/nestjs-mongoose-book-manager). But before jumping to that link, we From 235543fb5b38f93cf5f8c9f7d013f8c705215f23 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:38:10 +0100 Subject: [PATCH 43/48] fix: add id as path variable to update endpoint --- .../src/book.controller.ts | 7 ++++--- .../test/book.controller.test.ts | 13 +++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/nestjs-mongoose-book-manager/src/book.controller.ts b/examples/nestjs-mongoose-book-manager/src/book.controller.ts index 2c24212..1de633e 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.controller.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.controller.ts @@ -56,11 +56,12 @@ export class BookController { return this.save(book); } - @Patch() + @Patch(':id') async update( - @Body() - book: PartialBook, + @Param('id') id: string, + @Body() book: PartialBook, ): Promise { + book.id = id; return this.save(book); } diff --git a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts index 9995f2a..aedd842 100644 --- a/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts +++ b/examples/nestjs-mongoose-book-manager/test/book.controller.test.ts @@ -104,9 +104,12 @@ describe('Given the book manager controller', () => { describe('when updating a book', () => { describe('that is invalid', () => { it('returns a bad request HTTP status code', () => { + const paperBookToUpdate = { + edition: 0, + }; return request(bookManager.getHttpServer()) - .patch('/books') - .send() + .patch(`/books/${storedPaperBook.id}`) + .send(paperBookToUpdate) .expect(HttpStatus.BAD_REQUEST); }); }); @@ -114,11 +117,10 @@ describe('Given the book manager controller', () => { describe('that is not stored', () => { it('returns a bad request HTTP status code', () => { const paperBookToUpdate = { - id: '000000000000000000000000', edition: 4, }; return request(bookManager.getHttpServer()) - .patch('/books') + .patch('/books/000000000000000000000000') .send(paperBookToUpdate) .expect(HttpStatus.BAD_REQUEST); }); @@ -127,11 +129,10 @@ describe('Given the book manager controller', () => { describe('that is stored', () => { it('returns the updated book', () => { const paperBookToUpdate = { - id: storedPaperBook.id, edition: 4, }; return request(bookManager.getHttpServer()) - .patch('/books') + .patch(`/books/${storedPaperBook.id}`) .send(paperBookToUpdate) .expect(HttpStatus.OK) .expect((response) => { From 393ca461c4c501bbf062cef545a9f602fa030682 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Fri, 9 Feb 2024 00:12:40 +0100 Subject: [PATCH 44/48] docs: updated documentation --- README.md | 12 ++-- .../nestjs-mongoose-book-manager/README.md | 70 +++++-------------- 2 files changed, 21 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 45e6a6a..d8cc6d9 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ and you are done. # Supported Database Operations -We support two kinds of CRUD operations: _basic_ and _transactional_. Both kinds specify [atomic]() operations; however, while the former are inherently atomic, the latter require some transactional logic to ensure atomicity. Moreover, basic CRUD operations can be safely executed on a MongoDB standalone instance, but transactional CRUD operations are only atomic when run as part of a larger cluster e.g., a sharded cluster or a replica set. Using a MongoDB cluster in your production environment is, by the way, [the official recommendation](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/). +The library supports two kinds of CRUD operations: _basic_ and _transactional_. Both kinds specify [atomic]() operations; however, while the former are inherently atomic, the latter require some transactional logic to ensure atomicity. Moreover, basic CRUD operations can be safely executed on a MongoDB standalone instance, but transactional CRUD operations are only atomic when run as part of a larger cluster e.g., a sharded cluster or a replica set. Using a MongoDB cluster in your production environment is, by the way, [the official recommendation](https://www.mongodb.com/docs/manual/tutorial/convert-standalone-to-replica-set/). Let's now explore these two kinds of operations in detail. @@ -208,9 +208,7 @@ In the event of any error, this operation rollbacks all its changes. In other wo You may find an example of how to instantiate and use a repository that performs basic CRUD operations over instances of `Book` and its aforementioned subtypes at [`book.repository.test.ts`](test/book.repository.test.ts). You may also find an example on `monguito`'s transactional CRUD operations at [`book.transactional-repository.test.ts`](test/book.transactional-repository.test.ts). -Moreover, if you are interested in knowing how to inject and use a custom repository in a NestJS application, visit -[`nestjs-mongoose-book-manager`](examples/nestjs-mongoose-book-manager). But before jumping to that link, we -recommend reading the following section. +Moreover, if you are interested in knowing how to inject and use a custom repository in a NestJS application, visit [`nestjs-mongoose-book-manager`](examples/nestjs-mongoose-book-manager). But before jumping to that link, I recommend reading the following section. # Write Your Own Repository Interfaces @@ -297,13 +295,13 @@ safely be `undefined` until the pertaining domain object instance is inserted (i database. The fact that `Entity` is an interface instead of an abstract class is not a coincidence; JavaScript is a single -inheritance-based programming language, and we strongly believe that you are entitled to design the domain model at your +inheritance-based programming language, and I strongly believe that you are entitled to design the domain model at your will, with no dependencies to other libraries. But all that being said, you may decide not to use it at all, and that would be just fine. All you need to do is ensure that your domain objects specify an optional `id` field. ## Define Your Custom Schemas -We believe that writing your own database schemas is a good practice, as opposed of using decorators +I believe that writing your own database schemas is a good practice, as opposed of using decorators at your domain model. This is mainly to avoid marrying the underlying infrastructure, thus enabling you to easily get rid of this repository logic if something better comes in. It also allows you to have more control on the persistence properties of your domain objects. After all, database definition is a thing that Mongoose is really rock-solid about. @@ -409,7 +407,7 @@ models, but it implements the [Data Mapper](https://martinfowler.com/eaaCatalog/ the Repository pattern, which in complex domain model scenarios results in query logic duplication. Moreover, `monguito` is also type-safe. -Considering that Mongoose is currently the most mature MongoDB handling utility, we decided to keep it as `monguito`'s +Considering that Mongoose is currently the most mature MongoDB handling utility, I decided to keep it as `monguito`'s foundation. # Project Validation diff --git a/examples/nestjs-mongoose-book-manager/README.md b/examples/nestjs-mongoose-book-manager/README.md index fee1f94..abfa293 100644 --- a/examples/nestjs-mongoose-book-manager/README.md +++ b/examples/nestjs-mongoose-book-manager/README.md @@ -1,9 +1,9 @@ This is an example of how to use `monguito` in a NestJS application that uses a MongoDB replica set instance with -a single node. It is a dummy book manager that exposes an endpoint for each CRUD operation offered by `monguito`. +a single node. The application models a dummy book manager that exposes an endpoint for each CRUD operation offered by `monguito`. A book may be of type `Book` or any of its subtypes i.e., `PaperBook` and `AudioBook`. > [!WARNING] -> Some basic knowledge on [NestJS](https://docs.nestjs.com/) is assumed, as well as that you have read the main documentation of [monguito](../../README.md). The goal of this documentation is not to provide a comprehensive guide on `monguito` usage. Thus, you may want to check the [sample application code](./src) as you go reading. +> Some basic knowledge on [NestJS](https://docs.nestjs.com/) and [monguito](../../README.md) is assumed. The goal of this documentation is not to provide a comprehensive guide on `monguito` usage; you may want to check the [sample application code](./src) as you go reading. # Main Contents @@ -22,7 +22,7 @@ need to install of the project dependencies by running the following command: $ yarn install ``` -## Run +## Execution The application requires a running instance of MongoDB. It includes a `docker-compose.yml` file that will fire up a MongoDB replica set instance, assuming that Docker Desktop is running. @@ -155,26 +155,6 @@ contents are as follows: ```typescript type PartialBook = { id: string } & Partial; -function deserialiseAll(plainBooks: any[]): T[] { - const books: T[] = []; - for (const plainBook of plainBooks) { - books.push('id' in plainBook ? plainBook : deserialise(plainBook)); - } - return books; -} - -function deserialise(plainBook: any): T { - let book = null; - if (plainBook.edition) { - book = new PaperBook(plainBook); - } else if (plainBook.hostingPlatforms) { - book = new AudioBook(plainBook); - } else { - book = new Book(plainBook); - } - return book; -} - @Controller('books') export class BookController { constructor( @@ -197,11 +177,12 @@ export class BookController { return this.save(book); } - @Patch() + @Patch(':id') async update( - @Body() - book: PartialBook, + @Param('id') id: string, + @Body() book: PartialBook, ): Promise { + book.id = id; return this.save(book); } @@ -247,30 +228,16 @@ simple CRUD application that introducing services would be over-engineering. I r code necessary for the sake of maximising the actual purpose of this documentation: illustrate how to integrate `monguito` on a NodeJS-based enterprise application. -Moreover, you would probably not write a `deserialise` or `deserialiseAll` functions to enable the transformation of -JSON request bodies into domain objects when dealing with `POST` requests. Instead, you would rather use -a [NestJS pipe](https://docs.nestjs.com/pipes#pipes) to do so, thus properly implementing the Single Responsibility -principle. Once again, I wanted to share the simplest possible working example at the expense of not conveying to the -recommended practices in NestJS application construction. That being said, I would highly recommend you to -read [this section](https://docs.nestjs.com/pipes#class-validator) on how to use `class-validator` -and `class-transformer` for the validation and deserialisation of JSON request bodies in the development of complex -enterprise applications. +The functions `deserialise` and `deserialiseAll` deserialise books in JSON format into actual instances of type `Book` or any of its subtypes. I am not showing its code as it does not bring too much value here. Moreover, you would probably not write them; instead, you would rather use a [NestJS pipe](https://docs.nestjs.com/pipes#pipes) to perform book deserialisation, thus properly implementing the Single Responsibility principle. I wanted to share the simplest possible working example at the expense of not conveying to the recommended practices in NestJS application construction. That being said, I would highly recommend you to read [this section](https://docs.nestjs.com/pipes#class-validator) on how to use `class-validator` and `class-transformer` for the validation and deserialisation of JSON request bodies in the development of complex enterprise applications. ## Book Manager Module NestJS implements the Dependency Inversion principle; developers specify their component dependencies and NestJS uses its built-in dependency injector to inject those dependencies during component instantiation. -So, how do we specify the dependencies of the components that compose the book manager sample application? There are two -easy steps that we need to take: The first step consists of writing some decorators in the `MongooseBookRepository` -and `BookController` classes, as I already did in the code definition for both. The former class specifies that its -instances are `Injectable` to other components. It also specifies that to instantiate a book repository, NestJS needs to -inject a Mongoose connection. This is done with the `InjectConnection` decorator related to the `connection` constructor -input parameter. +Book manager application component injection is defined at `AppModule`. First, it describes a Mongoose connection to a MongoDB replica set instance as a dynamic module `import` that is to be injected to an instance of `MongooseBookRepository`, as showed earlier at that class' constructor. Moreover, `MongooseBookRepository` is a `provider` to be injected to the `BookController`. The custom token specified both at the controller `bookRepository` constructor parameter and the `provider` (i.e., `BOOK_REPOSITORY`) must match. Finally, `AppModule` determines `BookController` as the sole controller of the application. -On another hand, the definition of `BookController` specifies that, during instantiation, the controller consumes an -instance of a book `Repository` defined by the `BOOK_REPOSITORY` custom token. The definition of this custom token is -part of the second step: writing the last required class `AppModule`. The definition of this class is as follows: +Here is the definition of `AppModule`: ```typescript @Module({ @@ -291,21 +258,16 @@ part of the second step: writing the last required class `AppModule`. The defini export class AppModule {} ``` -This class module specifies the Mongoose connection required to instantiate `MongooseBookRepository` at the `imports` -property of the `Module` decorator. It also determines that any component dependent on a provider identified by -the `BOOK_REPOSITORY` custom token is to get an instance of `MongooseBookRepository`. Finally, it determines -that `BookController` is the sole controller for the book manager application. - # Custom Repository Validation -This application comes with a couple of unit tests that you may find useful when validating your own NestJS application. -The first test suite validates the [basic CRUD operations](../../README.md/#basic-crud-operations) included in `BookController` and is encoded in the [book.controller.test.ts](./test/book.controller.test.ts) file. The second test suite validates the [transactional CRUD operations](../../README.md/#transactional-crud-operations) also written in `BookController` and is implemented on [book.transactional-controller.test.ts](./test/book.transactional-controller.test.ts). +This application comes with a couple of unit tests that you may find useful when creating the tests of your own NestJS application. +The first test suite validates the [basic CRUD operations](../../README.md/#basic-crud-operations) included in `BookController` and is encoded at [book.controller.test.ts](./test/book.controller.test.ts). The second test suite validates the [transactional CRUD operations](../../README.md/#transactional-crud-operations) also written in `BookController` and is implemented in [book.transactional-controller.test.ts](./test/book.transactional-controller.test.ts). -As mentioned in `monguito`'s main documentation, basic CRUD operations may run on a MongoDB standalone instance. However, transactional CRUD operations can only run on a MongoDB cluster such as replica set. Therefore, the nature of basic and transactional CRUD operations determines the configuration of the aforementioned test suites: [book.controller.test.ts](./test/book.controller.test.ts) works with an in-memory version of standalone MongoDB, whereas [book.transactional-controller.test.ts](./test/book.transactional-controller.test.ts) operates over an in-memory version of MongoDB replica set. +As mentioned in `monguito`'s [main documentation](../../README.md), basic CRUD operations may run on a standalone MongoDB instance. However, transactional CRUD operations can only run on a MongoDB cluster such as replica set. Therefore, the nature of basic and transactional CRUD operations determines the configuration of the aforementioned test suites: [book.controller.test.ts](./test/book.controller.test.ts) works with an in-memory standalone MongoDB instance, whereas [book.transactional-controller.test.ts](./test/book.transactional-controller.test.ts) operates over an in-memory MongoDB replica set instance. Let's now focus on the module configuration and application initialisation for these test files. Keep in mind that, in both cases, you first need to create a testing module for your app. -## Initialisation of MongoDB Standalone-based App +## Initialisation of Standalone MongoDB-based App Here is how you initialise the test application required to run the tests described at [book.controller.test.ts](./test/book.controller.test.ts): @@ -324,7 +286,7 @@ beforeAll(async () => { }, timeout); ``` -You may want to create an instance of `MongoMemoryServer` (the main class exported by the library [`mongodb-memory-server` NPM dependency](https://www.npmjs.com/package/mongodb-memory-server)) instead of a full-blown MongoDB standalone instance. This instance is required to inject the custom repository at `BookController` at test runtime. The creation of the instance is done at the `rootMongooseStandaloneMongoTestModule` function included at [`mongo-server.ts`](../../test/util/mongo-server.ts). This is its implementation: +`MongoMemoryServer` models an in-memory standalone MongoDB instance, pretty handy to substitute a full-blown MongoDB instance during validation. This class is part of the [`mongodb-memory-server` NPM dependency](https://www.npmjs.com/package/mongodb-memory-server) used on the book manager application. The creation of the instance is done at the `rootMongooseStandaloneMongoTestModule` function included at [`mongo-server.ts`](../../test/util/mongo-server.ts): ```typescript const dbName = 'test'; @@ -366,7 +328,7 @@ beforeAll(async () => { }, timeout); ``` -You may want to create an instance of `MongoMemoryReplSet` (also defined in the library [`mongodb-memory-server` NPM dependency](https://www.npmjs.com/package/mongodb-memory-server)) instead of a full-blown MongoDB replica set instance. This instance is required to inject the custom repository at `BookController` at test runtime. The creation of the instance is done at the `rootMongooseReplicaSetMongoTestModule` function included at [`mongo-server.ts`](../../test/util/mongo-server.ts). This is its implementation: +You may want to create an in-memory instance of MongoDB replica set, modelled by `MongoMemoryReplSet` (also included at [`mongodb-memory-server`](https://www.npmjs.com/package/mongodb-memory-server)), instead of a full-blown instance. The creation of the instance is done at the `rootMongooseReplicaSetMongoTestModule` function included at [`mongo-server.ts`](../../test/util/mongo-server.ts): ```typescript const dbName = 'test'; From b464fe018907730f1b42b3f1e63121836916d6ad Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:40:01 +0100 Subject: [PATCH 45/48] refactor: collate all operation options parameter types --- .eslintrc.cjs | 30 ++--- README.md | 16 +-- .../src/book.controller.ts | 2 +- src/index.ts | 10 ++ src/mongoose.repository.ts | 23 ++-- src/mongoose.transactional-repository.ts | 16 +-- src/repository.ts | 22 ++-- src/transactional-repository.ts | 19 +--- src/util/operation-options.ts | 104 ++++++++++++++++++ src/util/search-options.ts | 41 ------- 10 files changed, 175 insertions(+), 108 deletions(-) create mode 100644 src/util/operation-options.ts delete mode 100644 src/util/search-options.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 763712f..9f28f63 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,28 +1,28 @@ module.exports = { - parser: "@typescript-eslint/parser", + parser: '@typescript-eslint/parser', parserOptions: { // project: "tsconfig.json", tsconfigRootDir: __dirname, - sourceType: "module" + sourceType: 'module', }, - plugins: ["@typescript-eslint/eslint-plugin", "no-only-or-skip-tests"], + plugins: ['@typescript-eslint/eslint-plugin', 'no-only-or-skip-tests'], extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', ], root: true, env: { node: true, - jest: true + jest: true, }, - ignorePatterns: [".eslintrc.js"], + ignorePatterns: ['.eslintrc.js'], rules: { - "@typescript-eslint/interface-name-prefix": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "no-console": "error", - "no-only-or-skip-tests/no-only-tests": "error", - "no-only-or-skip-tests/no-skip-tests": "error" - } + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'no-console': ['error', { allow: ['warn'] }], + 'no-only-or-skip-tests/no-only-tests': 'error', + 'no-only-or-skip-tests/no-skip-tests': 'error', + }, }; diff --git a/README.md b/README.md index d8cc6d9..e4e6898 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ interface Repository { findAll: (options?: SearchOptions) => Promise; save: ( entity: S | PartialEntityWithId, - userId?: string, + options?: SaveOptions, ) => Promise; deleteById: (id: string) => Promise; } @@ -150,8 +150,7 @@ slippery `null` values or exceptions (i.e., the alternatives to `Optional`), as ### `findAll` -Returns an array including all the persisted entities, or an empty array otherwise. This operation accepts some -additional and non-required search `options`: +Returns an array including all the persisted entities, or an empty array otherwise. This operation accepts some extra search `options`: - `filters`: a [MongoDB query](https://www.mongodb.com/docs/manual/tutorial/query-documents/) to filter results - `sortBy`: a [MongoDB sort criteria](https://www.mongodb.com/docs/manual/reference/method/cursor.sort/#mongodb-method-cursor.sort) @@ -165,8 +164,7 @@ Persists the given entity by either inserting or updating it and returns the per Beware that trying to persist a new entity that includes a developer specified `id` is considered a _system invariant violation_; only Mongoose is able to produce MongoDB identifiers to prevent `id` collisions and undesired entity updates. -Finally, this function specifies an optional `userId` argument to enable user audit data handling (read -[this section](#built-in-audit-data-support) for further details). +This operation accepts `userId` as an extra `options` parameter to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details on this topic). ### `deleteById` @@ -181,10 +179,10 @@ export interface TransactionalRepository extends Repository { saveAll: ( entities: (S | PartialEntityWithId)[], - userId?: string, + options?: SaveAllOptions, ) => Promise; - deleteAll: (options?: DeleteOptions) => Promise; + deleteAll: (options?: DeleteAllOptions) => Promise; } ``` @@ -197,6 +195,8 @@ Persists the given list of entities by either inserting or updating them and ret In the event of any error, this operation rollbacks all its changes. In other words, it does not save any given entity, thus guaranteeing operation atomicity. +This operation accepts `userId` as an extra `options` parameter to enable user audit data handling (read [this section](#built-in-audit-data-support) for further details on this topic). + ### `deleteAll` Deletes all the entities that match the MongoDB `filters` query specified within the `options` parameter. This operation returns the total amount of deleted entities. @@ -394,7 +394,7 @@ class AuditableBook extends AuditableClass implements Entity { `monguito` will produce and save the audit data for any domain object implementing `Auditable` or extending `AuditableClass` that is to be stored in MongoDB invoking the repository `save` operation. The user audit data is optional; if you want `monguito` to handle it for you, simply invoke `save` with a value for the `userId` input -parameter. +parameter as `options` parameter. # Comparison to other Alternatives diff --git a/examples/nestjs-mongoose-book-manager/src/book.controller.ts b/examples/nestjs-mongoose-book-manager/src/book.controller.ts index 1de633e..d627187 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.controller.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.controller.ts @@ -93,7 +93,7 @@ export class BookController { try { return await this.bookRepository.save(book); } catch (error) { - throw new BadRequestException(error); + throw new BadRequestException('Bad request', { cause: error }); } } } diff --git a/src/index.ts b/src/index.ts index 7d6cecc..8092409 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,12 @@ import { UndefinedConstructorException, ValidationException, } from './util/exceptions'; +import { + DeleteAllOptions, + SaveAllOptions, + SaveOptions, + SearchOptions, +} from './util/operation-options'; import { AuditableSchema, BaseSchema, extendSchema } from './util/schema'; export { @@ -22,12 +28,16 @@ export { AuditableSchema, BaseSchema, Constructor, + DeleteAllOptions, Entity, IllegalArgumentException, MongooseRepository, MongooseTransactionalRepository, PartialEntityWithId, Repository, + SaveAllOptions, + SaveOptions, + SearchOptions, TransactionalRepository, TypeData, TypeMap, diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index b65077f..09a624b 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -7,11 +7,7 @@ import mongoose, { UpdateQuery, } from 'mongoose'; import { Optional } from 'typescript-optional'; -import { - OperationOptions, - PartialEntityWithId, - Repository, -} from './repository'; +import { PartialEntityWithId, Repository } from './repository'; import { isAuditable } from './util/audit'; import { Entity } from './util/entity'; import { @@ -19,7 +15,7 @@ import { UndefinedConstructorException, ValidationException, } from './util/exceptions'; -import { SearchOptions } from './util/search-options'; +import { SaveOptions, SearchOptions } from './util/operation-options'; /** * Models a domain object instance constructor. @@ -150,18 +146,27 @@ export abstract class MongooseRepository> async save( entity: S | PartialEntityWithId, userId?: string, - options?: OperationOptions, + options?: SaveOptions, ): Promise { if (!entity) throw new IllegalArgumentException('The given entity must be valid'); + if (userId) { + console.warn( + "The 'userId' property is deprecated. Use 'options.userId' instead.", + ); + } try { let document; if (!entity.id) { - document = await this.insert(entity as S, userId, options?.session); + document = await this.insert( + entity as S, + userId ?? options?.userId, + options?.session, + ); } else { document = await this.update( entity as PartialEntityWithId, - userId, + userId ?? options?.userId, options?.session, ); } diff --git a/src/mongoose.transactional-repository.ts b/src/mongoose.transactional-repository.ts index ecb8f61..6cdfd30 100644 --- a/src/mongoose.transactional-repository.ts +++ b/src/mongoose.transactional-repository.ts @@ -1,12 +1,10 @@ import { ClientSession, Connection, UpdateQuery } from 'mongoose'; import { MongooseRepository, TypeMap } from './mongoose.repository'; import { PartialEntityWithId } from './repository'; -import { - DeleteOptions, - TransactionalRepository, -} from './transactional-repository'; +import { TransactionalRepository } from './transactional-repository'; import { Entity } from './util/entity'; import { IllegalArgumentException } from './util/exceptions'; +import { DeleteAllOptions, SaveAllOptions } from './util/operation-options'; import { runInTransaction } from './util/transaction'; /** @@ -30,13 +28,17 @@ export abstract class MongooseTransactionalRepository< /** @inheritdoc */ async saveAll( entities: (S | PartialEntityWithId)[], - userId?: string, + options?: SaveAllOptions, ): Promise { return await runInTransaction( async (session: ClientSession) => await Promise.all( entities.map( - async (entity) => await this.save(entity, userId, { session }), + async (entity) => + await this.save(entity, undefined, { + userId: options?.userId, + session, + }), ), ), { connection: this.connection }, @@ -44,7 +46,7 @@ export abstract class MongooseTransactionalRepository< } /** @inheritdoc */ - async deleteAll(options?: DeleteOptions): Promise { + async deleteAll(options?: DeleteAllOptions): Promise { if (options?.filters === null) { throw new IllegalArgumentException('Null filters are disallowed'); } diff --git a/src/repository.ts b/src/repository.ts index 702f14b..10171e4 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -1,7 +1,6 @@ -import { ClientSession } from 'mongoose'; import { Optional } from 'typescript-optional'; import { Entity } from './util/entity'; -import { SearchOptions } from './util/search-options'; +import { SaveOptions, SearchOptions } from './util/operation-options'; /** * Models an entity with partial content that specifies a Mongo `id` and (optionally) a Mongoose discriminator key. @@ -12,14 +11,6 @@ export type PartialEntityWithId = { __t?: string; } & Partial; -/** - * Specifies some operation options e.g., a Mongoose session required in operations to run within a transaction. - */ -export type OperationOptions = { - userId?: string; - session?: ClientSession; -}; - /** * Specifies a list of common database CRUD operations. */ @@ -34,7 +25,7 @@ export interface Repository { /** * Finds all entities. - * @param {SearchOptions} options (optional) the desired search options (i.e., field filters, sorting, and pagination data). + * @param {SearchOptions} options (optional) search operation options. * @returns {Promise} all entities. * @throws {IllegalArgumentException} if the given `options` specifies an invalid parameter. */ @@ -43,8 +34,8 @@ export interface Repository { /** * Saves (insert or update) an entity. * @param {S | PartialEntityWithId} entity the entity to save. - * @param {string=} userId (optional) the ID of the user executing the action. - * @param {OperationOptions} OperationOptions (optional) operation options. + * @param {string=} userId DEPRECATED! use 'options.userId' instead. (optional) the ID of the user executing the action. + * @param {SaveOptions=} options (optional) save operation options. * @returns {Promise} the saved entity. * @throws {IllegalArgumentException} if the given entity is `undefined` or `null` or * specifies an `id` not matching any existing entity. @@ -52,8 +43,11 @@ export interface Repository { */ save: ( entity: S | PartialEntityWithId, + /** + * @deprecated This property is deprecated. Use 'options.userId' instead. + */ userId?: string, - options?: OperationOptions, + options?: SaveOptions, ) => Promise; /** diff --git a/src/transactional-repository.ts b/src/transactional-repository.ts index 9eac700..9c63c9a 100644 --- a/src/transactional-repository.ts +++ b/src/transactional-repository.ts @@ -1,13 +1,6 @@ import { PartialEntityWithId, Repository } from './repository'; import { Entity } from './util/entity'; - -/** - * Specifies options for the `deleteAll` operation. - * - `filter`: a MongoDB query object to select the entities to be deleted - */ -export interface DeleteOptions { - filters?: any; -} +import { DeleteAllOptions, SaveAllOptions } from './util/operation-options'; /** * Specifies a list of common database CRUD operations that must execute in a database transaction. @@ -17,7 +10,7 @@ export interface TransactionalRepository /** * Saves (insert or update) a list of entities. * @param {S[] | PartialEntityWithId[]} entities the list of entities to save. - * @param {string} userId (optional) the ID of the user executing the action. + * @param {SaveAllOptions=} options (optional) save operation options. * @returns {Promise} the list of saved entities. * @throws {IllegalArgumentException} if any of the given entities is `undefined` or `null` or * specifies an `id` not matching any existing entity. @@ -25,15 +18,15 @@ export interface TransactionalRepository */ saveAll: ( entities: (S | PartialEntityWithId)[], - userId?: string, + options?: SaveAllOptions, ) => Promise; /** * Deletes all the entities that match the given filter, if any. No filter specification will * result in the deletion of all entities. - * @param {DeleteOptions=} options (optional) deletion options. + * @param {DeleteAllOptions=} options (optional) delete operation options. * @returns {number} the number of deleted entities. - * @see {@link DeleteOptions} + * @see {@link DeleteAllOptions} */ - deleteAll: (options?: DeleteOptions) => Promise; + deleteAll: (options?: DeleteAllOptions) => Promise; } diff --git a/src/util/operation-options.ts b/src/util/operation-options.ts new file mode 100644 index 0000000..08e54a9 --- /dev/null +++ b/src/util/operation-options.ts @@ -0,0 +1,104 @@ +import { ClientSession } from 'mongoose'; +import { IllegalArgumentException } from './exceptions'; + +/** + * Specifies options required to peform transactional operations. + */ +type TransactionOptions = { + /** + * (optional) A Mongoose session required in operations to run within a transaction. + * @type {ClientSession} + */ + session?: ClientSession; +}; + +/** + * Specifies options required to perform audit on side effect operation execution. + */ +type AuditOptions = { + /** + * (Optional) The id of the user performing the operation. + * @type {string} + */ + userId?: string; +}; + +/** + * Page specification utility class. + */ +export class Pageable { + /** + * The page number to retrieve. + * @type {number} + */ + readonly pageNumber: number; + + /** + * The number of entities composing the result. + * @type {number} + */ + readonly offset: number; + + /** + * Creates a pageable instance. + * @param {Pageable} pageable the instance to create the pageable from. + * @throws {IllegalArgumentException} if the given instance does not specify a page number and an offset. + */ + constructor(pageable: Pageable) { + if (!pageable.pageNumber) { + throw new IllegalArgumentException( + 'A value for the page number is missing', + ); + } + if (!pageable.offset) { + throw new IllegalArgumentException('A value for the offset is missing'); + } + this.pageNumber = pageable.pageNumber; + this.offset = pageable.offset; + } +} + +/** + * Specifies options for the `findAll` operation. + * @property {Pageable=} pageable + */ +export type SearchOptions = { + /** + * (Optional) A MongoDB entity field-based query to filter results. + * @type {any} + */ + filters?: any; + + /** + * (Optional) A MongoDB sort criteria to return results in some sorted order. + * @type {any} + */ + sortBy?: any; + + /** + * (Optional) Page-related options. + * @type {Pageable} + */ + pageable?: Pageable; +}; + +/** + * Specifies options for the `save` operation. + */ +export type SaveOptions = AuditOptions & TransactionOptions; + +/** + * Specifies options for the `saveAll` operation. + */ +export type SaveAllOptions = AuditOptions; + +/** + * Specifies options for the `deleteAll` operation. + */ +export type DeleteAllOptions = { + /** + * (Optional) A MongoDB query object to select the entities to be deleted. + * @type {any} + */ + filters?: any; +}; diff --git a/src/util/search-options.ts b/src/util/search-options.ts deleted file mode 100644 index c789544..0000000 --- a/src/util/search-options.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { IllegalArgumentException } from './exceptions'; - -/** - * Page specification utility class. - * @property {number} pageNumber the page number to retrieve. - * @property {number} offset the number of entities composing the result. - */ -export class Pageable { - readonly pageNumber: number; - readonly offset: number; - - /** - * Creates a pageable instance. - * @param {Pageable} pageable the instance to create the pageable from. - * @throws {IllegalArgumentException} if the given instance does not specify a page number and an offset. - */ - constructor(pageable: Pageable) { - if (!pageable.pageNumber) { - throw new IllegalArgumentException( - 'A value for the page number is missing', - ); - } - if (!pageable.offset) { - throw new IllegalArgumentException('A value for the offset is missing'); - } - this.pageNumber = pageable.pageNumber; - this.offset = pageable.offset; - } -} - -/** - * Specifies some options to narrow down a search operation. - * @property {any=} filters (optional) a MongoDB entity field-based query to filter results. - * @property {any=} sortBy (optional) a MongoDB sort criteria to return results in some sorted order. - * @property {Pageable=} pageable (optional) page data (i.e., page number and offset) required to return a particular set of results. - */ -export type SearchOptions = { - filters?: any; - sortBy?: any; - pageable?: Pageable; -}; From d395f664bd2896db6109bd3079791909dcac0a50 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:04:37 +0100 Subject: [PATCH 46/48] docs: update operation options documentation Also performed version bump. --- package.json | 2 +- src/util/operation-options.ts | 35 +++++++---------------------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index bfddca8..53d4d94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monguito", - "version": "4.2.0", + "version": "4.3.0", "description": "MongoDB Abstract Repository implementation for Node.js", "author": { "name": "Josu Martinez", diff --git a/src/util/operation-options.ts b/src/util/operation-options.ts index 08e54a9..95307a4 100644 --- a/src/util/operation-options.ts +++ b/src/util/operation-options.ts @@ -3,28 +3,22 @@ import { IllegalArgumentException } from './exceptions'; /** * Specifies options required to peform transactional operations. + * @property {ClientSession=} session (optional) a Mongoose session required in operations to run within a transaction. */ type TransactionOptions = { - /** - * (optional) A Mongoose session required in operations to run within a transaction. - * @type {ClientSession} - */ session?: ClientSession; }; /** * Specifies options required to perform audit on side effect operation execution. + * @property {string=} userId (optional) the id of the user performing the operation. */ type AuditOptions = { - /** - * (Optional) The id of the user performing the operation. - * @type {string} - */ userId?: string; }; /** - * Page specification utility class. + * Specifies paging configuration for search operations. */ export class Pageable { /** @@ -60,25 +54,13 @@ export class Pageable { /** * Specifies options for the `findAll` operation. - * @property {Pageable=} pageable + * @property {any=} filters (optional) filters for the search. + * @property {any=} sortBy (optional) sorting criteria for the search. + * @property {Pageable=} pageable (optional) paging configuration. */ export type SearchOptions = { - /** - * (Optional) A MongoDB entity field-based query to filter results. - * @type {any} - */ filters?: any; - - /** - * (Optional) A MongoDB sort criteria to return results in some sorted order. - * @type {any} - */ sortBy?: any; - - /** - * (Optional) Page-related options. - * @type {Pageable} - */ pageable?: Pageable; }; @@ -94,11 +76,8 @@ export type SaveAllOptions = AuditOptions; /** * Specifies options for the `deleteAll` operation. + * @property {any=} filters (optional) a MongoDB query object to select the entities to be deleted. */ export type DeleteAllOptions = { - /** - * (Optional) A MongoDB query object to select the entities to be deleted. - * @type {any} - */ filters?: any; }; From 45ef76485dc4f96b0780e8faa5362822ab6e4d0f Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:13:03 +0100 Subject: [PATCH 47/48] fix: update pipeline to run NestJS example app tests Also updated the Node.js version matrix to the current versions. --- .github/workflows/pipeline.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 69679ed..c92146d 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -12,8 +12,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-latest, windows-latest ] - node-version: [ 18.x, 20.x ] + os: [ubuntu-latest, windows-latest] + node-version: [18.x, 20.x] steps: - name: Code Checkout πŸ›ŽοΈ uses: actions/checkout@v3 @@ -32,8 +32,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-latest, windows-latest ] - node-version: [ 18.x, 20.x ] + os: [ubuntu-latest, windows-latest] + node-version: [18.x, 20.x, 21.x] steps: - name: Code Checkout πŸ›ŽοΈ uses: actions/checkout@v3 @@ -48,8 +48,12 @@ jobs: - name: Install NestJS Example App Dependencies πŸ’Ύ run: | cd ./examples/nestjs-mongoose-book-manager - yarn install -f + yarn install --force - name: Build NestJS Example App πŸ”§ - run: yarn build + run: | + cd ./examples/nestjs-mongoose-book-manager + yarn build - name: Run NestJS Example App Tests πŸ§ͺ - run: yarn test \ No newline at end of file + run: | + cd ./examples/nestjs-mongoose-book-manager + yarn test From ec7b86d5810baee8591b5ed2e6a8be68f56710c1 Mon Sep 17 00:00:00 2001 From: Josu Martinez <6097850+Josuto@users.noreply.github.com> Date: Sat, 10 Feb 2024 21:35:32 +0100 Subject: [PATCH 48/48] chore: update GitHub actions pipeline configuration Selected Node.js 20 and 21 and updated GH actions checkout and setup-node to v4. --- .github/workflows/pipeline.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index c92146d..fbcf795 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -13,12 +13,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - node-version: [18.x, 20.x] + node-version: [20.x, 21.x] steps: - - name: Code Checkout πŸ›ŽοΈ - uses: actions/checkout@v3 + - name: Checkout Code πŸ›ŽοΈ + uses: actions/checkout@v4 - name: Setup Node ${{ matrix.node-version }} πŸ•ΈοΈ - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install Library Dependencies πŸ’Ύ @@ -33,12 +33,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - node-version: [18.x, 20.x, 21.x] + node-version: [20.x, 21.x] steps: - - name: Code Checkout πŸ›ŽοΈ - uses: actions/checkout@v3 + - name: Checkout Code πŸ›ŽοΈ + uses: actions/checkout@v4 - name: Setup Node ${{ matrix.node-version }} πŸ•ΈοΈ - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install Library Dependencies πŸ’Ύ