Skip to content
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ A [Feathers](https://feathersjs.com) database adapter for [Sequelize](http://seq
- [`service(options)`](#serviceoptions)
- [params.sequelize](#paramssequelize)
- [operators](#operatormap)
- [Modifying the model](#modifyModel)
- [Caveats](#caveats)
- [Sequelize `raw` queries](#sequelize-raw-queries)
- [Working with MSSQL](#working-with-mssql)
Expand Down Expand Up @@ -154,6 +155,27 @@ app.service('users').find({
GET /users?name[$like]=Dav%
```

## Modifying the Model

Sequelize allows you to call methods like `Model.scope()`, `Model.schema()`, and others. To use these methods, extend the class to overwrite the `getModel` method.

```js
const { SequelizeService } = require('feathers-sequelize');

class Service extends SequelizeService {
getModel(params) {
let Model = this.options.Model;
if (params?.sequelize?.scope) {
Model = Model.scope(params.sequelize.scope);
}
if (params?.sequelize?.schema) {
Model = Model.schema(params.sequelize.schema);
}
return Model;
}
}
```

## Caveats

### Sequelize `raw` queries
Expand Down
69 changes: 41 additions & 28 deletions src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MethodNotAllowed, NotFound } from '@feathersjs/errors';
import { MethodNotAllowed, GeneralError, NotFound } from '@feathersjs/errors';
import { _ } from '@feathersjs/commons';
import {
select as selector,
Expand Down Expand Up @@ -38,6 +38,21 @@ const defaultFilters = {
$and: true as const
}

const catchHandler = (handler: any) => {
return (sequelizeError: any) => {
try {
errorHandler(sequelizeError);
} catch (feathersError) {
try {
handler(feathersError, sequelizeError);
} catch (error) {
throw error;
}
}
throw new GeneralError('`handleError` method must throw an error');
}
}

export class SequelizeAdapter<
Result,
Data = Partial<Result>,
Expand All @@ -46,11 +61,11 @@ export class SequelizeAdapter<
> extends AdapterBase<Result, Data, PatchData, ServiceParams, SequelizeAdapterOptions> {
constructor (options: SequelizeAdapterOptions) {
if (!options.Model) {
throw new Error('You must provide a Sequelize Model');
throw new GeneralError('You must provide a Sequelize Model');
}

if (options.operators && !Array.isArray(options.operators)) {
throw new Error('The \'operators\' option must be an array. For migration from feathers.js v4 see: https://github.com/feathersjs-ecosystem/feathers-sequelize/tree/dove#migrate-to-feathers-v5-dove');
throw new GeneralError('The \'operators\' option must be an array. For migration from feathers.js v4 see: https://github.com/feathersjs-ecosystem/feathers-sequelize/tree/dove#migrate-to-feathers-v5-dove');
}

const operatorMap = {
Expand Down Expand Up @@ -99,7 +114,7 @@ export class SequelizeAdapter<

get Model () {
if (!this.options.Model) {
throw new Error('The Model getter was called with no Model provided in options!');
throw new GeneralError('The Model getter was called with no Model provided in options!');
}

return this.options.Model;
Expand All @@ -108,19 +123,12 @@ export class SequelizeAdapter<
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getModel (_params?: ServiceParams) {
if (!this.options.Model) {
throw new Error('getModel was called without a Model present in the constructor options and without overriding getModel! Perhaps you intended to override getModel in a child class?');
throw new GeneralError('getModel was called without a Model present in the constructor options and without overriding getModel! Perhaps you intended to override getModel in a child class?');
}

return this.options.Model;
}

ModelWithScope (params: ServiceParams) {
const Model = this.getModel(params);
if (params?.sequelize?.scope) {
return Model.scope(params.sequelize.scope);
}
return Model;
}

convertOperators (q: any): Query {
if (Array.isArray(q)) {
Expand Down Expand Up @@ -222,23 +230,28 @@ export class SequelizeAdapter<
return sequelize;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
handleError (feathersError: any, _sequelizeError: any) {
throw feathersError;
}

async _find (params?: ServiceParams & { paginate?: PaginationOptions }): Promise<Paginated<Result>>
async _find (params?: ServiceParams & { paginate: false }): Promise<Result[]>
async _find (params?: ServiceParams): Promise<Paginated<Result> | Result[]>
async _find (params: ServiceParams = {} as ServiceParams): Promise<Paginated<Result> | Result[]> {
const Model = this.ModelWithScope(params);
const Model = this.getModel(params);
const { paginate } = this.filterQuery(params);
const sequelizeOptions = this.paramsToAdapter(null, params);

if (!paginate || !paginate.default) {
const result = await Model.findAll(sequelizeOptions).catch(errorHandler);
const result = await Model.findAll(sequelizeOptions).catch(catchHandler(this.handleError));
return result;
}

if (sequelizeOptions.limit === 0) {
const total = (await Model
.count({ ...sequelizeOptions, attributes: undefined })
.catch(errorHandler)) as any as number;
.catch(catchHandler(this.handleError))) as any as number;

return {
total,
Expand All @@ -248,7 +261,7 @@ export class SequelizeAdapter<
}
}

const result = await Model.findAndCountAll(sequelizeOptions).catch(errorHandler);
const result = await Model.findAndCountAll(sequelizeOptions).catch(catchHandler(this.handleError));

return {
total: result.count,
Expand All @@ -259,9 +272,9 @@ export class SequelizeAdapter<
}

async _get (id: Id, params: ServiceParams = {} as ServiceParams): Promise<Result> {
const Model = this.ModelWithScope(params);
const Model = this.getModel(params);
const sequelizeOptions = this.paramsToAdapter(id, params);
const result = await Model.findAll(sequelizeOptions).catch(errorHandler);
const result = await Model.findAll(sequelizeOptions).catch(catchHandler(this.handleError));
if (result.length === 0) {
throw new NotFound(`No record found for id '${id}'`);
}
Expand All @@ -283,13 +296,13 @@ export class SequelizeAdapter<
return []
}

const Model = this.ModelWithScope(params);
const Model = this.getModel(params);
const sequelizeOptions = this.paramsToAdapter(null, params);

if (isArray) {
const instances = await Model
.bulkCreate(data as any[], sequelizeOptions)
.catch(errorHandler);
.catch(catchHandler(this.handleError));

if (sequelizeOptions.returning === false) {
return [];
Expand Down Expand Up @@ -318,7 +331,7 @@ export class SequelizeAdapter<

const result = await Model
.create(data as any, sequelizeOptions as CreateOptions)
.catch(errorHandler);
.catch(catchHandler(this.handleError));

if (sequelizeOptions.raw) {
return select((result as Model).toJSON())
Expand All @@ -334,7 +347,7 @@ export class SequelizeAdapter<
throw new MethodNotAllowed('Can not patch multiple entries')
}

const Model = this.ModelWithScope(params);
const Model = this.getModel(params);
const sequelizeOptions = this.paramsToAdapter(id, params);
const select = selector(params, this.id);
const values = _.omit(data, this.id);
Expand All @@ -361,7 +374,7 @@ export class SequelizeAdapter<
raw: false,
where: { [this.id]: ids.length === 1 ? ids[0] : { [Op.in]: ids } }
} as UpdateOptions)
.catch(errorHandler) as [number, Model[]?];
.catch(catchHandler(this.handleError)) as [number, Model[]?];

if (sequelizeOptions.returning === false) {
return []
Expand Down Expand Up @@ -436,7 +449,7 @@ export class SequelizeAdapter<
await instance
.set(values)
.update(values, sequelizeOptions)
.catch(errorHandler);
.catch(catchHandler(this.handleError));

if (isPresent(sequelizeOptions.include)) {
return this._get(id, {
Expand All @@ -462,7 +475,7 @@ export class SequelizeAdapter<
}

async _update (id: Id, data: Data, params: ServiceParams = {} as ServiceParams): Promise<Result> {
const Model = this.ModelWithScope(params);
const Model = this.getModel(params);
const sequelizeOptions = this.paramsToAdapter(id, params);
const select = selector(params, this.id);

Expand All @@ -484,7 +497,7 @@ export class SequelizeAdapter<
await instance
.set(values)
.update(values, sequelizeOptions)
.catch(errorHandler);
.catch(catchHandler(this.handleError));

if (isPresent(sequelizeOptions.include)) {
return this._get(id, {
Expand Down Expand Up @@ -516,7 +529,7 @@ export class SequelizeAdapter<
throw new MethodNotAllowed('Can not remove multiple entries')
}

const Model = this.ModelWithScope(params);
const Model = this.getModel(params);
const sequelizeOptions = this.paramsToAdapter(id, params);

if (id === null) {
Expand All @@ -539,7 +552,7 @@ export class SequelizeAdapter<
await Model.destroy({
...params.sequelize,
where: { [this.id]: ids.length === 1 ? ids[0] : { [Op.in]: ids } }
}).catch(errorHandler);
}).catch(catchHandler(this.handleError));

if (sequelizeOptions.returning === false) {
return [];
Expand Down
28 changes: 1 addition & 27 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,7 @@ const Model = sequelize.define('people', {
defaultValue: 'pending'
}
}, {
freezeTableName: true,
scopes: {
active: {
where: {
status: 'active'
}
},
pending: {
where: {
status: 'pending'
}
}
}
freezeTableName: true
});
const Order = sequelize.define('orders', {
name: {
Expand Down Expand Up @@ -780,20 +768,6 @@ describe('Feathers Sequelize Service', () => {
}
});
});

it('can set the scope of an operation #130', async () => {
const people = app.service('people');
const data = { name: 'Active', status: 'active' };
const SCOPE_TO_ACTIVE = { sequelize: { scope: 'active' } };
const SCOPE_TO_PENDING = { sequelize: { scope: 'pending' } };
await people.create(data);

const staPeople = await people.find(SCOPE_TO_ACTIVE) as Paginated<any>;
assert.strictEqual(staPeople.data.length, 1);

const stpPeople = await people.find(SCOPE_TO_PENDING) as Paginated<any>;
assert.strictEqual(stpPeople.data.length, 0);
});
});

describe('ORM functionality', () => {
Expand Down