Skip to content

Commit

Permalink
refactor: improve readability
Browse files Browse the repository at this point in the history
Also added a missing test.
  • Loading branch information
Josuto committed Apr 28, 2024
1 parent d3f2b8e commit 4d20b18
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 103 deletions.
180 changes: 92 additions & 88 deletions src/mongoose.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,53 +44,24 @@ export abstract class MongooseRepository<T extends Entity & UpdateQuery<T>>
this.entityModel = this.createEntityModel(connection);
}

/** @inheritdoc */
async deleteById(id: string, options?: DeleteByIdOptions): Promise<boolean> {
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,
});
return !!isDeleted;
}

/** @inheritdoc */
async findAll<S extends T>(options?: FindAllOptions): Promise<S[]> {
if (options?.connection)
console.warn(
'Since v5.0.1 "options.connection" is deprecated as is of no longer use.',
);
if (options?.pageable?.pageNumber && options?.pageable?.pageNumber < 0) {
throw new IllegalArgumentException(
'The given page number must be a positive number',
private createEntityModel(connection?: Connection) {
let entityModel;
const supertypeData = this.typeMap.getSupertypeData();
if (connection) {
entityModel = connection.model<T>(
supertypeData.type.name,
supertypeData.schema,
);
}
if (options?.pageable?.offset && options?.pageable?.offset < 0) {
throw new IllegalArgumentException(
'The given page offset must be a positive number',
} else {
entityModel = mongoose.model<T>(
supertypeData.type.name,
supertypeData.schema,
);
}

const offset = options?.pageable?.offset ?? 0;
const pageNumber = options?.pageable?.pageNumber ?? 0;
try {
const documents = await this.entityModel
.find(options?.filters)
.skip(pageNumber > 0 ? (pageNumber - 1) * offset : 0)
.limit(offset)
.sort(options?.sortBy)
.session(options?.session ?? null)
.exec();
return documents.map((document) => this.instantiateFrom(document) as S);
} catch (error) {
throw new IllegalArgumentException(
'The given optional parameters must be valid',
error,
);
for (const subtypeData of this.typeMap.getSubtypesData()) {
entityModel.discriminator(subtypeData.type.name, subtypeData.schema);
}
return entityModel;
}

/** @inheritdoc */
Expand Down Expand Up @@ -132,6 +103,42 @@ export abstract class MongooseRepository<T extends Entity & UpdateQuery<T>>
return Optional.ofNullable(this.instantiateFrom(document) as S);
}

/** @inheritdoc */
async findAll<S extends T>(options?: FindAllOptions): Promise<S[]> {
if (options?.connection)
console.warn(
'Since v5.0.1 "options.connection" is deprecated as is of no longer use.',
);
if (options?.pageable?.pageNumber && options?.pageable?.pageNumber < 0) {
throw new IllegalArgumentException(
'The given page number must be a positive number',
);
}
if (options?.pageable?.offset && options?.pageable?.offset < 0) {
throw new IllegalArgumentException(
'The given page offset must be a positive number',
);
}

const offset = options?.pageable?.offset ?? 0;
const pageNumber = options?.pageable?.pageNumber ?? 0;
try {
const documents = await this.entityModel
.find(options?.filters)
.skip(pageNumber > 0 ? (pageNumber - 1) * offset : 0)
.limit(offset)
.sort(options?.sortBy)
.session(options?.session ?? null)
.exec();
return documents.map((document) => this.instantiateFrom(document) as S);
} catch (error) {
throw new IllegalArgumentException(
'The given optional parameters must be valid',
error,
);
}
}

/** @inheritdoc */
async save<S extends T>(
entity: S | PartialEntityWithId<S>,
Expand All @@ -142,7 +149,9 @@ export abstract class MongooseRepository<T extends Entity & UpdateQuery<T>>
'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');
throw new IllegalArgumentException(
'The given entity cannot be null or undefined',
);
try {
if (!entity.id) {
return await this.insert(entity as S, options);
Expand All @@ -163,49 +172,6 @@ export abstract class MongooseRepository<T extends Entity & UpdateQuery<T>>
}
}

/**
* Instantiates a persistable domain object from the given Mongoose Document.
* @param {HydratedDocument<S> | 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<S extends T>(
document: HydratedDocument<S> | null,
): S | null {
if (!document) return null;
const entityKey = document.get('__t');
const constructor: Constructor<S> | undefined = entityKey
? (this.typeMap.getSubtypeData(entityKey)?.type as Constructor<S>)
: (this.typeMap.getSupertypeData().type as Constructor<S>);
if (constructor) {
// safe instantiation as no abstract class instance can be stored in the first place
return new constructor(document.toObject());
}
throw new UndefinedConstructorException(
`There is no registered instance constructor for the document with ID ${document.id}`,
);
}

private createEntityModel(connection?: Connection) {
let entityModel;
const supertypeData = this.typeMap.getSupertypeData();
if (connection) {
entityModel = connection.model<T>(
supertypeData.type.name,
supertypeData.schema,
);
} else {
entityModel = mongoose.model<T>(
supertypeData.type.name,
supertypeData.schema,
);
}
for (const subtypeData of this.typeMap.getSubtypesData()) {
entityModel.discriminator(subtypeData.type.name, subtypeData.schema);
}
return entityModel;
}

/**
* Inserts an entity.
* @param {S} entity the entity to insert.
Expand All @@ -222,7 +188,9 @@ export abstract class MongooseRepository<T extends Entity & UpdateQuery<T>>
'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');
throw new IllegalArgumentException(
'The given entity cannot be null or undefined',
);
const entityClassName = entity['constructor']['name'];
if (!this.typeMap.has(entityClassName)) {
throw new IllegalArgumentException(
Expand Down Expand Up @@ -293,4 +261,40 @@ export abstract class MongooseRepository<T extends Entity & UpdateQuery<T>>
`There is no document matching the given ID '${entity.id}'`,
);
}

/** @inheritdoc */
async deleteById(id: string, options?: DeleteByIdOptions): Promise<boolean> {
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,
});
return !!isDeleted;
}

/**
* Instantiates a persistable domain object from the given Mongoose Document.
* @param {HydratedDocument<S> | 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<S extends T>(
document: HydratedDocument<S> | null,
): S | null {
if (!document) return null;
const entityKey = document.get('__t');
const constructor: Constructor<S> | undefined = entityKey
? (this.typeMap.getSubtypeData(entityKey)?.type as Constructor<S>)
: (this.typeMap.getSupertypeData().type as Constructor<S>);
if (constructor) {
// safe instantiation as no abstract class instance can be stored in the first place
return new constructor(document.toObject());
}
throw new UndefinedConstructorException(
`There is no registered instance constructor for the document with ID ${document.id}`,
);
}
}
50 changes: 35 additions & 15 deletions test/repository/book.repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,15 +945,15 @@ describe('Given an instance of book repository', () => {
it('throws an exception', async () => {
await expect(
bookRepository.save(undefined as unknown as Book),
).rejects.toThrow('The given entity must be valid');
).rejects.toThrow(IllegalArgumentException);
});
});

describe('that is null', () => {
it('throws an exception', async () => {
await expect(
bookRepository.save(null as unknown as Book),
).rejects.toThrow('The given entity must be valid');
).rejects.toThrow(IllegalArgumentException);
});
});

Expand Down Expand Up @@ -1097,24 +1097,44 @@ describe('Given an instance of book repository', () => {
expect(book.id).toBe(storedBook.id);
expect(book.title).toBe(storedBook.title);
expect(book.description).toBe(bookToUpdate.description);
expect(book.isbn).toBe(storedBook.isbn);
});
});
});

describe('and that specifies all the contents of the supertype', () => {
it('updates the book', async () => {
const bookToUpdate = bookFixture(
{
title: 'Continuous Delivery',
description:
'Boost your development productivity via automation',
},
storedBook.id,
);
describe('and some field values are invalid', () => {
it('throws an exception', async () => {
const bookToUpdate = audioBookFixture(
{
title: undefined,
},
storedBook.id,
);

await expect(bookRepository.save(bookToUpdate)).rejects.toThrow(
ValidationException,
);
});
});

describe('and all field values are valid', () => {
it('updates the book', async () => {
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);
expect(book.title).toBe(bookToUpdate.title);
expect(book.description).toBe(bookToUpdate.description);
const book = await bookRepository.save(bookToUpdate);
expect(book.id).toBe(bookToUpdate.id);
expect(book.title).toBe(bookToUpdate.title);
expect(book.description).toBe(bookToUpdate.description);
expect(book.isbn).toBe(bookToUpdate.isbn);
});
});
});
});
Expand Down

0 comments on commit 4d20b18

Please sign in to comment.