Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: enable Mongoose plugin registration in schemas #221

Merged
merged 1 commit into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "monguito",
"version": "5.0.1",
"version": "5.1.0",
"description": "MongoDB Abstract Repository implementation for Node.js",
"author": {
"name": "Josu Martinez",
Expand All @@ -16,8 +16,8 @@
"url": "https://github.com/josuto/monguito/issues",
"email": "[email protected]"
},
"engines": {
"node": ">=18.18.0"
"engines": {
"node": ">=18.18.0"
},
"keywords": [
"node",
Expand Down Expand Up @@ -78,6 +78,7 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/mongoose-unique-validator": "^1.0.9",
"@types/node": "^20.12.2",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
Expand All @@ -91,6 +92,7 @@
"microbundle": "^0.15.1",
"mongodb-memory-server": "^9.1.8",
"mongoose": "^8.2.4",
"mongoose-unique-validator": "^5.0.0",
"prettier": "^3.2.5",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
Expand Down
20 changes: 14 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ import {
SaveAllOptions,
SaveOptions,
} from './util/operation-options';
import { AuditableSchema, BaseSchema, extendSchema } from './util/schema';
import { TransactionOptions, runInTransaction } from './util/transaction';
import {
AuditableSchema,
BaseSchema,
extendSchema,
SchemaOptions,
SchemaPlugin,
} from './util/schema';
import { runInTransaction, TransactionOptions } from './util/transaction';
import {
AbsConstructor,
Constructor,
Expand All @@ -39,25 +45,27 @@ export {
DeleteAllOptions,
DeleteByIdOptions,
Entity,
extendSchema,
FindAllOptions,
FindByIdOptions,
FindOneOptions,
IllegalArgumentException,
isAuditable,
MongooseRepository,
MongooseTransactionalRepository,
PartialEntityWithId,
Repository,
runInTransaction,
SaveAllOptions,
SaveOptions,
SchemaOptions,
SchemaPlugin,
SubtypeData,
SubtypeMap,
SupertypeData,
TransactionOptions,
TransactionalRepository,
TransactionOptions,
TypeMap,
UndefinedConstructorException,
ValidationException,
extendSchema,
isAuditable,
runInTransaction,
};
42 changes: 32 additions & 10 deletions src/util/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Schema, SchemaDefinition, SchemaOptions } from 'mongoose';
import {
SchemaOptions as MongooseSchemaOptions,
Schema,
SchemaDefinition,
} from 'mongoose';

export type SchemaPlugin = { fn: (schema: Schema) => void; options?: any };

export type SchemaOptions = MongooseSchemaOptions & {
plugins?: SchemaPlugin[];
};

/**
* Base schema to be extended by all persistable domain object schemas.
Expand Down Expand Up @@ -35,9 +45,9 @@ export const AuditableSchema = extendSchema(
delete result.__v;
},
},
plugins: [{ fn: setUserAuditData }],
},
);
AuditableSchema.plugin(setUserAuditData);

// Mongoose plugin definition
function setUserAuditData(schema: Schema) {
Expand All @@ -53,8 +63,6 @@ function setUserAuditData(schema: Schema) {
});
}

type Plugin = { fn: (schema: Schema) => void; opts?: undefined };

/**
* Creates a new schema from the given data.
* @param {Schema<T>} baseSchema the base schema.
Expand Down Expand Up @@ -94,17 +102,31 @@ export function extendSchema<T = object, S = object>(
...(isExtensionASchema ? extension.options : options),
},
);
registerPlugins(newSchema, baseSchema, extension, options);
return newSchema;
}

function registerPlugins<T = object, S = object>(
newSchema: Schema<T & S>,
baseSchema: Schema<T>,
extension: Schema<T> | SchemaDefinition<S>,
options?: SchemaOptions,
): void {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
baseSchema.plugins.forEach((plugin: Plugin) => {
newSchema.plugin(plugin.fn, plugin.opts);
baseSchema.plugins.forEach((plugin: SchemaPlugin) => {
newSchema.plugin(plugin.fn, plugin.options);
});
if (isExtensionASchema) {
if (extension instanceof Schema) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
extension.plugins.forEach((plugin: Plugin) => {
newSchema.plugin(plugin.fn, plugin.opts);
extension.plugins.forEach((plugin: SchemaPlugin) => {
newSchema.plugin(plugin.fn, plugin.options);
});
}
if (options?.plugins) {
options.plugins.forEach((plugin: SchemaPlugin) => {
newSchema.plugin(plugin.fn, plugin.options);
});
}
return newSchema;
}
17 changes: 15 additions & 2 deletions test/repository/book.repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -978,11 +978,24 @@ describe('Given an instance of book repository', () => {

describe('and does not specify an ID', () => {
describe('and some field values are invalid', () => {
beforeEach(async () => {
const bookToStore = bookFixture();
const storedBookId = await insert(bookToStore, 'books');
storedBook = new Book({
...bookToStore,
id: storedBookId,
});
});

afterEach(async () => {
await deleteAll('books');
});

it('throws an exception', async () => {
const bookToInsert = bookFixture({
title: 'Modern Software Engineering',
title: undefined, // Missing title
description: 'Build Better Software Faster',
isbn: undefined,
isbn: storedBook.isbn, // Duplicated ISBN
});

await expect(bookRepository.save(bookToInsert)).rejects.toThrow(
Expand Down
15 changes: 10 additions & 5 deletions test/repository/book.schema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Schema } from 'mongoose';
import uniqueValidator from 'mongoose-unique-validator';
import { BaseSchema, extendSchema } from '../../src';
import { AudioBook, Book, PaperBook } from '../domain/book';

export const BookSchema: Schema<Book> = extendSchema(BaseSchema, {
title: { type: String, required: true },
description: { type: String, required: false },
isbn: { type: String, required: true, unique: true },
});
export const BookSchema: Schema<Book> = extendSchema(
BaseSchema,
{
title: { type: String, required: true },
description: { type: String, required: false },
isbn: { type: String, required: true, unique: true },
},
{ plugins: [{ fn: uniqueValidator }] },
);

export const PaperBookSchema: Schema<PaperBook> = extendSchema(
BookSchema,
Expand Down
31 changes: 22 additions & 9 deletions test/repository/book.transactional-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,30 @@ describe('Given an instance of book repository', () => {
describe('that includes a book that is undefined', () => {
it('throws an exception', async () => {
const booksToStore = [
bookFixture({ isbn: '1942788340' }),
bookFixture({ isbn: '1942788344' }),
undefined as unknown as Book,
];
await expect(bookRepository.saveAll(booksToStore)).rejects.toThrow(
IllegalArgumentException,
);
expect(await findOne({}, 'books')).toBeNull();
expect(
await findOne({ isbn: booksToStore[0].isbn }, 'books'),
).toBeNull();
});
});

describe('that includes a book that is null', () => {
it('throws an exception', async () => {
const booksToStore = [
bookFixture({ isbn: '1942788340' }),
bookFixture({ isbn: '1942788345' }),
null as unknown as Book,
];
await expect(bookRepository.saveAll(booksToStore)).rejects.toThrow(
IllegalArgumentException,
);
expect(await findOne({}, 'books')).toBeNull();
expect(
await findOne({ isbn: booksToStore[0].isbn }, 'books'),
).toBeNull();
});
});

Expand All @@ -77,7 +81,12 @@ describe('Given an instance of book repository', () => {
await expect(bookRepository.saveAll(booksToStore)).rejects.toThrow(
IllegalArgumentException,
);
expect(await findOne({}, 'books')).toBeNull();
expect(
await findOne(
{ isbn: { $in: [booksToStore[0].isbn, booksToStore[1].isbn] } },
'books',
),
).toBeNull();
});
});

Expand All @@ -87,13 +96,15 @@ describe('Given an instance of book repository', () => {
describe('and some field values of one book are invalid', () => {
it('throws an exception', async () => {
const booksToStore = [
bookFixture({ isbn: '1942788340' }),
bookFixture({ isbn: '1942788346' }),
bookFixture({ isbn: undefined }),
];
await expect(
bookRepository.saveAll(booksToStore),
).rejects.toThrow(ValidationException);
expect(await findOne({}, 'books')).toBeNull();
expect(
await findOne({ isbn: booksToStore[0].isbn }, 'books'),
).toBeNull();
});
});

Expand All @@ -113,13 +124,15 @@ describe('Given an instance of book repository', () => {
describe('and some field values of one book are invalid', () => {
it('throws an exception', async () => {
const booksToStore = [
bookFixture({ isbn: '1942788340' }),
bookFixture({ isbn: '1942788347' }),
paperBookFixture({ isbn: undefined }),
];
await expect(
bookRepository.saveAll(booksToStore),
).rejects.toThrow(ValidationException);
expect(await findOne({}, 'books')).toBeNull();
expect(
await findOne({ isbn: booksToStore[0].isbn }, 'books'),
).toBeNull();
});
});

Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
Expand Down
Loading
Loading