diff --git a/.eslintrc.cjs b/eslint.config.cjs similarity index 100% rename from .eslintrc.cjs rename to eslint.config.cjs diff --git a/examples/nestjs-mongoose-book-manager/src/book.repository.ts b/examples/nestjs-mongoose-book-manager/src/book.repository.ts index 297ed39..37e996f 100644 --- a/examples/nestjs-mongoose-book-manager/src/book.repository.ts +++ b/examples/nestjs-mongoose-book-manager/src/book.repository.ts @@ -24,9 +24,12 @@ export class MongooseBookRepository constructor(@InjectConnection() connection: Connection) { super( { - Default: { type: Book, schema: BookSchema }, - PaperBook: { type: PaperBook, schema: PaperBookSchema }, - AudioBook: { type: AudioBook, schema: AudioBookSchema }, + type: Book, + schema: BookSchema, + subtypes: [ + { type: PaperBook, schema: PaperBookSchema }, + { type: AudioBook, schema: AudioBookSchema }, + ], }, connection, ); diff --git a/examples/nestjs-mongoose-book-manager/yarn.lock b/examples/nestjs-mongoose-book-manager/yarn.lock index b998bff..6750e44 100644 --- a/examples/nestjs-mongoose-book-manager/yarn.lock +++ b/examples/nestjs-mongoose-book-manager/yarn.lock @@ -5551,6 +5551,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== diff --git a/package.json b/package.json index de6c300..7d86a4d 100644 --- a/package.json +++ b/package.json @@ -2,16 +2,10 @@ "name": "monguito", "version": "5.1.1", "description": "MongoDB Abstract Repository implementation for Node.js", - "author": { - "name": "Josu Martinez", - "email": "josu.martinez@gmail.com" - }, + "author": "Josu Martinez ", "license": "MIT", "homepage": "https://github.com/josuto/monguito#readme", - "repository": { - "type": "git", - "url": "https://github.com/josuto/monguito.git" - }, + "repository": "https://github.com/josuto/monguito.git", "bugs": { "url": "https://github.com/josuto/monguito/issues", "email": "josu.martinez@gmail.com" diff --git a/src/index.ts b/src/index.ts index bdb2b67..142a041 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { MongooseTransactionalRepository } from './mongoose.transactional-reposi import { PartialEntityWithId, Repository } from './repository'; import { TransactionalRepository } from './transactional-repository'; import { Auditable, AuditableClass, isAuditable } from './util/audit'; +import { DomainModel, DomainTree } from './util/domain-model'; import { Entity } from './util/entity'; import { IllegalArgumentException, @@ -26,24 +27,16 @@ import { SchemaPlugin, } from './util/schema'; import { runInTransaction, TransactionOptions } from './util/transaction'; -import { - AbsConstructor, - Constructor, - SubtypeData, - SubtypeMap, - SupertypeData, - TypeMap, -} from './util/type-map'; export { - AbsConstructor, Auditable, AuditableClass, AuditableSchema, BaseSchema, - Constructor, DeleteAllOptions, DeleteByIdOptions, + DomainModel, + DomainTree, Entity, extendSchema, FindAllOptions, @@ -60,12 +53,8 @@ export { SaveOptions, SchemaOptions, SchemaPlugin, - SubtypeData, - SubtypeMap, - SupertypeData, TransactionalRepository, TransactionOptions, - TypeMap, UndefinedConstructorException, ValidationException, }; diff --git a/src/mongoose.repository.ts b/src/mongoose.repository.ts index 25736e8..3e76add 100644 --- a/src/mongoose.repository.ts +++ b/src/mongoose.repository.ts @@ -1,5 +1,6 @@ import mongoose, { Connection, + FilterQuery, HydratedDocument, Model, UpdateQuery, @@ -7,6 +8,7 @@ import mongoose, { import { Optional } from 'typescript-optional'; import { PartialEntityWithId, Repository } from './repository'; import { isAuditable } from './util/audit'; +import { DomainModel, DomainTree } from './util/domain-model'; import { Entity } from './util/entity'; import { IllegalArgumentException, @@ -20,7 +22,6 @@ import { FindOneOptions, SaveOptions, } from './util/operation-options'; -import { Constructor, TypeMap, TypeMapImpl } from './util/type-map'; /** * Abstract Mongoose-based implementation of the {@link Repository} interface. @@ -28,37 +29,36 @@ import { Constructor, TypeMap, TypeMapImpl } from './util/type-map'; export abstract class MongooseRepository> implements Repository { - private readonly typeMap: TypeMapImpl; + private readonly domainTree: DomainTree; protected readonly entityModel: Model; /** * Sets up the underlying configuration to enable database operation execution. - * @param {TypeMap} typeMap a map of domain object types supported by this repository. + * @param {DomainModel} domainModel the domain model supported by this repository. * @param {Connection=} connection (optional) a MongoDB instance connection. */ protected constructor( - typeMap: TypeMap, + domainModel: DomainModel, protected readonly connection?: Connection, ) { - this.typeMap = new TypeMapImpl(typeMap); + this.domainTree = new DomainTree(domainModel); this.entityModel = this.createEntityModel(connection); } private createEntityModel(connection?: Connection) { let entityModel; - const supertypeData = this.typeMap.getSupertypeData(); if (connection) { entityModel = connection.model( - supertypeData.type.name, - supertypeData.schema, + this.domainTree.type.name, + this.domainTree.schema, ); } else { entityModel = mongoose.model( - supertypeData.type.name, - supertypeData.schema, + this.domainTree.type.name, + this.domainTree.schema, ); } - for (const subtypeData of this.typeMap.getSubtypesData()) { + for (const subtypeData of this.domainTree.getSubtypeTree()) { entityModel.discriminator(subtypeData.type.name, subtypeData.schema); } return entityModel; @@ -69,10 +69,6 @@ export abstract class MongooseRepository> id: string, options?: FindByIdOptions, ): Promise> { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); if (!id) throw new IllegalArgumentException('The given ID must be valid'); const document = await this.entityModel .findById(id) @@ -83,32 +79,17 @@ export abstract class MongooseRepository> /** @inheritdoc */ async findOne( - filters: any, - options?: FindOneOptions, + options?: FindOneOptions, ): Promise> { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); - if (filters) - console.warn( - 'Since v5.0.1 the "filters" parameter is deprecated. Use "options.filters" instead.', - ); - if (!filters && !options?.filters) - throw new IllegalArgumentException('Missing search criteria (filters)'); const document = await this.entityModel - .findOne(options?.filters ?? filters) + .findOne(options?.filters ?? undefined) .session(options?.session ?? null) .exec(); return Optional.ofNullable(this.instantiateFrom(document) as S); } /** @inheritdoc */ - async findAll(options?: FindAllOptions): Promise { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); + async findAll(options?: FindAllOptions): Promise { if (options?.pageable?.pageNumber && options?.pageable?.pageNumber < 0) { throw new IllegalArgumentException( 'The given page number must be a positive number', @@ -124,7 +105,7 @@ export abstract class MongooseRepository> const pageNumber = options?.pageable?.pageNumber ?? 0; try { const documents = await this.entityModel - .find(options?.filters) + .find(options?.filters as FilterQuery) .skip(pageNumber > 0 ? (pageNumber - 1) * offset : 0) .limit(offset) .sort(options?.sortBy) @@ -144,10 +125,6 @@ export abstract class MongooseRepository> entity: S | PartialEntityWithId, options?: SaveOptions, ): Promise { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); if (!entity) throw new IllegalArgumentException( 'The given entity cannot be null or undefined', @@ -183,10 +160,6 @@ export abstract class MongooseRepository> entity: S, options?: SaveOptions, ): Promise { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); if (!entity) throw new IllegalArgumentException( 'The given entity cannot be null or undefined', @@ -212,12 +185,12 @@ export abstract class MongooseRepository> entity: S | PartialEntityWithId, ): void { const entityClassName = entity['constructor']['name']; - if (!this.typeMap.has(entityClassName)) { + if (!this.domainTree.has(entityClassName)) { throw new IllegalArgumentException( `The entity with name ${entityClassName} is not included in the setup of the custom repository`, ); } - const isSubtype = entityClassName !== this.typeMap.getSupertypeName(); + const isSubtype = entityClassName !== this.domainTree.getSupertypeName(); const hasEntityDiscriminatorKey = '__t' in entity; if (isSubtype && !hasEntityDiscriminatorKey) { entity['__t'] = entityClassName; @@ -246,10 +219,6 @@ export abstract class MongooseRepository> entity: PartialEntityWithId, options?: SaveOptions, ): Promise { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); if (!entity) throw new IllegalArgumentException('The given entity must be valid'); const document = await this.entityModel @@ -281,10 +250,6 @@ export abstract class MongooseRepository> /** @inheritdoc */ async deleteById(id: string, options?: DeleteByIdOptions): Promise { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); if (!id) throw new IllegalArgumentException('The given ID must be valid'); const isDeleted = await this.entityModel.findByIdAndDelete(id, { session: options?.session, @@ -303,15 +268,15 @@ export abstract class MongooseRepository> ): S | null { if (!document) return null; const entityKey = document.get('__t'); - const constructor: Constructor | undefined = entityKey - ? (this.typeMap.getSubtypeData(entityKey)?.type as Constructor) - : (this.typeMap.getSupertypeData().type as Constructor); + const constructor = entityKey + ? this.domainTree.getSubtypeConstructor(entityKey) + : this.domainTree.getSupertypeConstructor(); if (constructor) { // safe instantiation as no abstract class instance can be stored in the first place - return new constructor(document.toObject()); + return new constructor(document.toObject()) as S; } throw new UndefinedConstructorException( - `There is no registered instance constructor for the document with ID ${document.id}`, + `There is no registered instance constructor for the document with ID ${document.id} or the constructor is abstract`, ); } } diff --git a/src/mongoose.transactional-repository.ts b/src/mongoose.transactional-repository.ts index d9a8316..4d01380 100644 --- a/src/mongoose.transactional-repository.ts +++ b/src/mongoose.transactional-repository.ts @@ -2,15 +2,14 @@ import { ClientSession, Connection, UpdateQuery } from 'mongoose'; import { MongooseRepository } from './mongoose.repository'; import { PartialEntityWithId } from './repository'; import { TransactionalRepository } from './transactional-repository'; +import { DomainModel } from './util/domain-model'; import { Entity } from './util/entity'; -import { IllegalArgumentException } from './util/exceptions'; import { DeleteAllOptions, SaveAllOptions, SaveOptions, } from './util/operation-options'; import { runInTransaction } from './util/transaction'; -import { TypeMap } from './util/type-map'; /** * Abstract Mongoose-based implementation of the {@link TransactionalRepository} interface. @@ -23,11 +22,11 @@ export abstract class MongooseTransactionalRepository< { /** * Sets up the underlying configuration to enable database operation execution. - * @param {TypeMap} typeMap a map of domain object types supported by this repository. + * @param {DomainModel} domainModel the domain model supported by this repository. * @param {Connection=} connection (optional) a MongoDB instance connection. */ - protected constructor(typeMap: TypeMap, connection?: Connection) { - super(typeMap, connection); + protected constructor(domainModel: DomainModel, connection?: Connection) { + super(domainModel, connection); } /** @inheritdoc */ @@ -35,10 +34,6 @@ export abstract class MongooseTransactionalRepository< entities: (S | PartialEntityWithId)[], options?: SaveAllOptions, ): Promise { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); return await runInTransaction( async (session: ClientSession) => await Promise.all( @@ -55,14 +50,7 @@ export abstract class MongooseTransactionalRepository< } /** @inheritdoc */ - async deleteAll(options?: DeleteAllOptions): Promise { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); - if (options?.filters === null) { - throw new IllegalArgumentException('Null filters are disallowed'); - } + async deleteAll(options?: DeleteAllOptions): Promise { return await runInTransaction( async (session: ClientSession) => (await this.entityModel.deleteMany(options?.filters, { session })) @@ -76,10 +64,6 @@ export abstract class MongooseTransactionalRepository< entity: PartialEntityWithId, options?: SaveOptions, ): Promise { - if (options?.connection) - console.warn( - 'Since v5.0.1 "options.connection" is deprecated as is of no longer use.', - ); const updateOperation = super.update.bind(this); return await runInTransaction( async (session: ClientSession) => diff --git a/src/repository.ts b/src/repository.ts index 5982caf..d90af52 100644 --- a/src/repository.ts +++ b/src/repository.ts @@ -35,15 +35,11 @@ export interface Repository { /** * Finds an entity by some filters. - * @param {any} filters some filters for the search - Deprecated since v5.0.1, use options.filters instead. * @param {FindOneOptions=} options (optional) search operation options. * @returns {Promise>} the entity or null. * @throws {IllegalArgumentException} if the given `filters` parameter is `undefined` or `null`. */ - findOne: ( - filters: any, - options?: FindOneOptions, - ) => Promise>; + findOne: (options?: FindOneOptions) => Promise>; /** * Finds all entities. @@ -51,7 +47,7 @@ export interface Repository { * @returns {Promise} all entities. * @throws {IllegalArgumentException} if the given `options` specifies an invalid parameter. */ - findAll: (options?: FindAllOptions) => Promise; + findAll: (options?: FindAllOptions) => Promise; /** * Saves (insert or update) an entity. diff --git a/src/transactional-repository.ts b/src/transactional-repository.ts index 9c63c9a..0c5ec2f 100644 --- a/src/transactional-repository.ts +++ b/src/transactional-repository.ts @@ -28,5 +28,5 @@ export interface TransactionalRepository * @returns {number} the number of deleted entities. * @see {@link DeleteAllOptions} */ - deleteAll: (options?: DeleteAllOptions) => Promise; + deleteAll: (options?: DeleteAllOptions) => Promise; } diff --git a/src/util/domain-model.ts b/src/util/domain-model.ts new file mode 100644 index 0000000..5d4347e --- /dev/null +++ b/src/util/domain-model.ts @@ -0,0 +1,90 @@ +import { Schema } from 'mongoose'; +import { Entity } from './entity'; +import { IllegalArgumentException } from './exceptions'; + +/** + * Models a domain type instance constructor. + */ +type Constructor = new (...args: any) => T; + +/** + * Models an abstract domain type instance constructor. + */ +type AbsConstructor = abstract new (...args: any) => T; + +/** + * Models some domain type data. + */ +type DomainTypeData = { + type: Constructor | AbsConstructor; + schema: Schema; +}; + +/** + * Models some domain leaf type data. + */ +type DomainLeafTypeData = { type: Constructor; schema: Schema }; + +/** + * Models some domain intermediate type data. + */ +type DomainIntermediateTypeData = { + type: Constructor | AbsConstructor; + schema: Schema; + subtypes: (DomainIntermediateTypeData | DomainLeafTypeData)[]; +}; + +/** + * Domain model specification. + */ +export interface DomainModel extends DomainTypeData { + subtypes?: (DomainIntermediateTypeData | DomainLeafTypeData)[]; +} + +/** + * Domain model implementation. + */ +export class DomainTree implements DomainModel { + readonly type: Constructor | AbsConstructor; + readonly schema: Schema; + readonly subtypes: (DomainIntermediateTypeData | DomainLeafTypeData)[]; + + constructor(domainModel: DomainModel) { + if (!domainModel.type || !domainModel.schema) { + throw new IllegalArgumentException( + 'The given domain model must specify a type and a schema', + ); + } + this.type = domainModel.type; + this.schema = domainModel.schema; + this.subtypes = []; + for (const subtypeData of domainModel.subtypes ?? []) { + this.subtypes.push(new DomainTree(subtypeData)); + } + } + + getSubtypeTree(): DomainModel[] { + return this.subtypes || []; + } + + getSubtypeData(type: string): DomainModel | undefined { + return this.subtypes?.find((subtype) => subtype.type.name === type); + } + + getSubtypeConstructor(type: string): Constructor | undefined { + const subtypeData = this.getSubtypeData(type); + return subtypeData?.type as Constructor; + } + + getSupertypeConstructor(): Constructor | undefined { + return this.type as Constructor; + } + + getSupertypeName(): string { + return this.type.name; + } + + has(type: string): boolean { + return type === this.getSupertypeName() || !!this.getSubtypeData(type); + } +} diff --git a/src/util/operation-options.ts b/src/util/operation-options.ts index 3115145..0efb8e8 100644 --- a/src/util/operation-options.ts +++ b/src/util/operation-options.ts @@ -1,3 +1,4 @@ +import { FilterQuery } from 'mongoose'; import { IllegalArgumentException } from './exceptions'; import { TransactionOptions } from './transaction'; @@ -44,15 +45,19 @@ export class Pageable { } } +export type SortOrder = { + [key: string]: 'asc' | 'desc' | 'ascending' | 'descending' | 1 | -1; +}; + /** * Specifies options for the `findAll` operation. - * @property {any=} filters (optional) some filters for the search. - * @property {any=} sortBy (optional) the sorting criteria for the search. + * @property {FilterQuery=} filters (optional) some filters for the search. + * @property {string | SortOrder=} sortBy (optional) the sorting criteria for the search. * @property {Pageable=} pageable (optional) paging configuration. */ -export type FindAllOptions = { - filters?: any; - sortBy?: any; +export type FindAllOptions = { + filters?: FilterQuery; + sortBy?: string | SortOrder; pageable?: Pageable; } & TransactionOptions; @@ -64,8 +69,8 @@ export type FindByIdOptions = TransactionOptions; /** * Specifies options for the `findOne` operation; */ -export type FindOneOptions = { - filters?: any; +export type FindOneOptions = { + filters?: FilterQuery; } & TransactionOptions; /** @@ -80,10 +85,10 @@ export type SaveAllOptions = AuditOptions & TransactionOptions; /** * Specifies options for the `deleteAll` operation. - * @property {any=} filters (optional) a MongoDB query object to select the entities to be deleted. + * @property {FilterQuery=} filters (optional) a MongoDB query object to select the entities to be deleted. */ -export type DeleteAllOptions = { - filters?: any; +export type DeleteAllOptions = { + filters?: FilterQuery; } & TransactionOptions; /** diff --git a/src/util/transaction.ts b/src/util/transaction.ts index 69b86a6..21ae681 100644 --- a/src/util/transaction.ts +++ b/src/util/transaction.ts @@ -11,7 +11,6 @@ type DbCallback = (session: ClientSession) => Promise; * @property {ClientSession=} session (optional) a transaction session, required to run the operation within an existing transaction. */ export type TransactionOptions = { - connection?: Connection; session?: ClientSession; }; @@ -26,7 +25,7 @@ const MAX_RETRIES = 3; */ export async function runInTransaction( callback: DbCallback, - options?: TransactionOptions, + options?: TransactionOptions & { connection?: Connection }, ): Promise { if (options?.session) return callback(options.session); return await recursiveRunIntransaction(callback, 0, options?.connection); diff --git a/src/util/type-map.ts b/src/util/type-map.ts deleted file mode 100644 index d667561..0000000 --- a/src/util/type-map.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Schema } from 'mongoose'; -import { Entity } from './entity'; -import { IllegalArgumentException } from './exceptions'; - -/** - * Models a domain object instance constructor. - */ -export type Constructor = new (...args: any) => T; - -/** - * Models an abstract domain object supertype instance constructor. - */ - -export type AbsConstructor = abstract new (...args: any) => T; - -/** - * Models some domain object subtype data. - */ -export type SubtypeData = { - type: Constructor; - schema: Schema; -}; - -/** - * Models some domain object supertype data. - */ -export type SupertypeData = { - type: Constructor | AbsConstructor; - schema: Schema; -}; - -/** - * Models a map of domain object subtypes. - */ -export type SubtypeMap = { - [key: string]: SubtypeData; -}; - -/** - * Models a map of domain object supertype and subtypes. - */ -export type TypeMap = - | { - ['Default']: SupertypeData; - } - | SubtypeMap; - -/** - * A `TypeMap` implementation designed to ease map content handling for its clients. - */ -export class TypeMapImpl { - readonly typeNames: string[]; - readonly supertypeData: SupertypeData; - readonly subtypeData: SubtypeData[]; - - constructor(map: TypeMap) { - if (!map.Default) { - throw new IllegalArgumentException( - 'The given map must include domain supertype data', - ); - } - this.supertypeData = map.Default as SupertypeData; - this.typeNames = Object.keys(map).filter((key) => key !== 'Default'); - this.subtypeData = Object.entries(map).reduce((accumulator, entry) => { - if (entry[0] !== 'Default') { - // @ts-expect-error - safe instantiation as any non-root map entry refers to some subtype data - accumulator.push(entry[1]); - } - return accumulator; - }, []); - } - - getSubtypeData(type: string): SubtypeData | undefined { - const index = this.typeNames.indexOf(type); - return index !== -1 ? this.subtypeData[index] : undefined; - } - - getSupertypeData(): SupertypeData { - return this.supertypeData; - } - - getSupertypeName(): string { - return this.getSupertypeData().type.name; - } - - getSubtypesData(): SubtypeData[] { - return this.subtypeData; - } - - has(type: string): boolean { - return ( - type === this.getSupertypeName() || this.typeNames.indexOf(type) !== -1 - ); - } -} diff --git a/test/repository/auditable.book.repository.ts b/test/repository/auditable.book.repository.ts index 9054ac9..95162d7 100644 --- a/test/repository/auditable.book.repository.ts +++ b/test/repository/auditable.book.repository.ts @@ -8,11 +8,14 @@ import { export class MongooseAuditableBookRepository extends MongooseRepository { constructor() { super({ - Default: { type: AuditableBook, schema: AuditableBookSchema }, - AuditablePaperBook: { - type: AuditablePaperBook, - schema: AuditablePaperBookSchema, - }, + type: AuditableBook, + schema: AuditableBookSchema, + subtypes: [ + { + type: AuditablePaperBook, + schema: AuditablePaperBookSchema, + }, + ], }); } } diff --git a/test/repository/book.repository.test.ts b/test/repository/book.repository.test.ts index 27eab56..7f2fb54 100644 --- a/test/repository/book.repository.test.ts +++ b/test/repository/book.repository.test.ts @@ -81,31 +81,17 @@ describe('Given an instance of book repository', () => { }); }); - describe('when searching a book by some filters', () => { - describe('by an undefined filter', () => { - it('throws an exception', async () => { - await expect( - bookRepository.findOne(undefined as unknown as string), - ).rejects.toThrow(IllegalArgumentException); - }); - }); - - describe('by a null filter', () => { - it('throws an exception', async () => { - await expect( - bookRepository.findOne(null as unknown as string), - ).rejects.toThrow(IllegalArgumentException); - }); - }); - - describe('by a filter matching no book', () => { - it('retrieves an empty book', async () => { - const book = await bookRepository.findOne({ title: 'The Hobbit' }); - expect(book).toEqual(Optional.empty()); + describe('when searching one book', () => { + describe('and there is no book stored', () => { + describe('not using any filter', () => { + it('retrieves an empty book', async () => { + const book = await bookRepository.findOne(); + expect(book).toEqual(Optional.empty()); + }); }); }); - describe('by a filter matching one or more books', () => { + describe('and there are books stored', () => { beforeEach(async () => { const paperBookToStore = paperBookFixture(); const audioBookToStore = audioBookFixture(); @@ -131,20 +117,55 @@ describe('Given an instance of book repository', () => { }); }); - describe('by a filter matching one book', () => { + describe('not using any filter', () => { + it('returns an arbitrary book', async () => { + const book = await bookRepository.findOne(); + expect(book.isPresent()).toBe(true); + expect([storedPaperBook, storedAudioBook]).toContainEqual(book.get()); + }); + }); + + describe('using an undefined filter', () => { + it('returns an arbitrary book', async () => { + const book = await bookRepository.findOne({ filters: undefined }); + expect(book.isPresent()).toBe(true); + expect([storedPaperBook, storedAudioBook]).toContainEqual(book.get()); + }); + }); + + describe('using a null filter', () => { + it('returns an arbitrary book', async () => { + const book = await bookRepository.findOne({ + filters: null as unknown as object, + }); + expect(book.isPresent()).toBe(true); + expect([storedPaperBook, storedAudioBook]).toContainEqual(book.get()); + }); + }); + + describe('using a filter matching no book', () => { + it('retrieves an empty book', async () => { + const book = await bookRepository.findOne({ + filters: { title: 'The Hobbit' }, + }); + expect(book).toEqual(Optional.empty()); + }); + }); + + describe('using a filter matching one book', () => { it('retrieves the book', async () => { const book = await bookRepository.findOne({ - title: storedPaperBook.title, + filters: { title: storedPaperBook.title }, }); expect(book.isPresent()).toBe(true); expect(book.get()).toEqual(storedPaperBook); }); }); - describe('by a filter matching several books', () => { + describe('using a filter matching several books', () => { it('retrieves the first book inserted', async () => { const book = await bookRepository.findOne({ - title: { $exists: true }, + filters: { title: { $exists: true } }, }); expect(book.isPresent()).toBe(true); expect(book.get()).toEqual(storedPaperBook); @@ -258,7 +279,7 @@ describe('Given an instance of book repository', () => { describe('and such a value refers to an existing field in some Book type', () => { it('retrieves a list with all books matching the filter', async () => { - const filters = { __t: 'PaperBook' }; + const filters = { title: 'Effective Java' }; const books = await bookRepository.findAll({ filters }); expect(books.length).toBe(1); expect(books).toEqual([storedPaperBook]); @@ -267,19 +288,29 @@ describe('Given an instance of book repository', () => { }); describe('and providing a value for the sort parameter', () => { - describe('and such a value is invalid', () => { - it('throws an exception', async () => { - const sortBy = { title: 2 }; - await expect(bookRepository.findAll({ sortBy })).rejects.toThrow( - IllegalArgumentException, - ); + describe('and such a value is the name of a book property', () => { + it('retrieves an ordered list with books', async () => { + const books = await bookRepository.findAll({ + sortBy: 'title', + }); + expect(books.length).toBe(3); + expect(books).toEqual([storedBook, storedPaperBook, storedAudioBook]); + }); + }); + + describe('and such a value is ascendent on a book property', () => { + it('retrieves an ordered list with books', async () => { + const books = await bookRepository.findAll({ + sortBy: { title: 'asc' }, + }); + expect(books.length).toBe(3); + expect(books).toEqual([storedBook, storedPaperBook, storedAudioBook]); }); }); - describe('and such a value is valid', () => { + describe('and such a value is descendent on a book property', () => { it('retrieves an ordered list with books', async () => { - const sortBy = { title: -1 }; - const books = await bookRepository.findAll({ sortBy }); + const books = await bookRepository.findAll({ sortBy: { title: -1 } }); expect(books.length).toBe(3); expect(books).toEqual([storedAudioBook, storedPaperBook, storedBook]); }); @@ -917,11 +948,10 @@ describe('Given an instance of book repository', () => { describe('and providing a valid value for all optional parameters', () => { 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 }; const books = await bookRepository.findAll({ filters, - sortBy, + sortBy: { title: -1 }, pageable, }); expect(books.length).toBe(1); diff --git a/test/repository/book.repository.ts b/test/repository/book.repository.ts index fbe8c23..c7cdf7d 100644 --- a/test/repository/book.repository.ts +++ b/test/repository/book.repository.ts @@ -1,9 +1,9 @@ +import { Optional } from 'typescript-optional'; import { IllegalArgumentException, MongooseRepository, Repository, } from '../../src'; -import { Optional } from 'typescript-optional'; import { AudioBook, Book, PaperBook } from '../domain/book'; import { AudioBookSchema, BookSchema, PaperBookSchema } from './book.schema'; @@ -17,9 +17,12 @@ export class MongooseBookRepository { constructor() { super({ - Default: { type: Book, schema: BookSchema }, - PaperBook: { type: PaperBook, schema: PaperBookSchema }, - AudioBook: { type: AudioBook, schema: AudioBookSchema }, + type: Book, + schema: BookSchema, + subtypes: [ + { type: PaperBook, schema: PaperBookSchema }, + { type: AudioBook, schema: AudioBookSchema }, + ], }); } diff --git a/test/repository/book.transactional-repository.test.ts b/test/repository/book.transactional-repository.test.ts index 4af735c..d41e50a 100644 --- a/test/repository/book.transactional-repository.test.ts +++ b/test/repository/book.transactional-repository.test.ts @@ -491,13 +491,14 @@ describe('Given an instance of book repository', () => { }); describe('that includes a null filter', () => { - it('throws an exception', async () => { - await expect( - bookRepository.deleteAll({ filters: null as unknown as object }), - ).rejects.toThrow(IllegalArgumentException); + it('deletes all books', async () => { + const deletedBooks = await bookRepository.deleteAll({ + filters: null as unknown as object, + }); + expect(deletedBooks).toBe(2); const storedBooks = await bookRepository.findAll(); - expect(storedBooks).toEqual([storedBook, storedPaperBook]); + expect(storedBooks.length).toBe(0); }); }); diff --git a/test/repository/book.transactional-repository.ts b/test/repository/book.transactional-repository.ts index 537c010..ffe7056 100644 --- a/test/repository/book.transactional-repository.ts +++ b/test/repository/book.transactional-repository.ts @@ -5,9 +5,12 @@ 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 }, + type: Book, + schema: BookSchema, + subtypes: [ + { type: PaperBook, schema: PaperBookSchema }, + { type: AudioBook, schema: AudioBookSchema }, + ], }); } }