From 3b2ca0a6a8e03e8390272c4d7e930b4bffdaacf5 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sat, 23 Apr 2022 15:47:32 -0700 Subject: [PATCH] feat(typescript): Improve adapter typings (#2605) --- packages/adapter-commons/src/declarations.ts | 123 +++++++++++++ packages/adapter-commons/src/filter-query.ts | 25 +-- packages/adapter-commons/src/index.ts | 6 +- packages/adapter-commons/src/service.ts | 171 ++++-------------- .../adapter-commons/test/filter-query.test.ts | 2 +- packages/adapter-commons/test/service.test.ts | 77 ++++++-- packages/authentication-local/test/fixture.ts | 2 +- packages/authentication/test/service.test.ts | 2 +- packages/client/test/fixture.ts | 4 +- packages/feathers/src/declarations.ts | 4 +- packages/memory/src/index.ts | 135 +++++++++++--- packages/memory/test/index.test.ts | 35 +++- packages/rest-client/test/server.ts | 4 +- packages/schema/test/fixture.ts | 19 +- packages/schema/test/hooks.test.ts | 12 +- packages/socketio-client/test/server.ts | 4 +- 16 files changed, 402 insertions(+), 223 deletions(-) create mode 100644 packages/adapter-commons/src/declarations.ts diff --git a/packages/adapter-commons/src/declarations.ts b/packages/adapter-commons/src/declarations.ts new file mode 100644 index 0000000000..0709046f3d --- /dev/null +++ b/packages/adapter-commons/src/declarations.ts @@ -0,0 +1,123 @@ +import { Query, Params, Paginated, Id, NullableId } from '@feathersjs/feathers'; + +export type FilterSettings = string[]|{ + [key: string]: (value: any, options: any) => any +} + +export interface PaginationOptions { + default?: number; + max?: number; +} + +export type PaginationParams = false|PaginationOptions; + +export type FilterQueryOptions = { + filters?: FilterSettings; + operators?: string[]; + paginate?: PaginationParams; +} + +export interface AdapterServiceOptions { + events?: string[]; + multi?: boolean|string[]; + id?: string; + paginate?: PaginationOptions + /** + * @deprecated renamed to `allow`. + */ + whitelist?: string[]; + allow?: string[]; + filters?: string[]; +} + +export interface AdapterOptions extends Pick { + Model?: M; +} + +export interface AdapterParams extends Params { + adapter?: Partial>; + paginate?: PaginationParams; +} + +/** + * Hook-less (internal) service methods. Directly call database adapter service methods + * without running any service-level hooks. This can be useful if you need the raw data + * from the service and don't want to trigger any of its hooks. + * + * Important: These methods are only available internally on the server, not on the client + * side and only for the Feathers database adapters. + * + * These methods do not trigger events. + * + * @see {@link https://docs.feathersjs.com/guides/migrating.html#hook-less-service-methods} + */ + export interface InternalServiceMethods, P extends AdapterParams = AdapterParams> { + /** + * Retrieve all resources from this service, skipping any service-level hooks. + * + * @param params - Service call parameters {@link Params} + * @see {@link HookLessServiceMethods} + * @see {@link https://docs.feathersjs.com/api/services.html#find-params|Feathers API Documentation: .find(params)} + */ + _find (_params?: P & { paginate?: PaginationOptions }): Promise>; + _find (_params?: P & { paginate: false }): Promise; + _find (params?: P): Promise>; + + /** + * Retrieve a single resource matching the given ID, skipping any service-level hooks. + * + * @param id - ID of the resource to locate + * @param params - Service call parameters {@link Params} + * @see {@link HookLessServiceMethods} + * @see {@link https://docs.feathersjs.com/api/services.html#get-id-params|Feathers API Documentation: .get(id, params)} + */ + _get (id: Id, params?: P): Promise; + + /** + * Create a new resource for this service, skipping any service-level hooks. + * + * @param data - Data to insert into this service. + * @param params - Service call parameters {@link Params} + * @see {@link HookLessServiceMethods} + * @see {@link https://docs.feathersjs.com/api/services.html#create-data-params|Feathers API Documentation: .create(data, params)} + */ + _create (data: Partial, params?: P): Promise; + _create (data: Partial[], params?: P): Promise; + _create (data: Partial|Partial[], params?: P): Promise; + + /** + * Replace any resources matching the given ID with the given data, skipping any service-level hooks. + * + * @param id - ID of the resource to be updated + * @param data - Data to be put in place of the current resource. + * @param params - Service call parameters {@link Params} + * @see {@link HookLessServiceMethods} + * @see {@link https://docs.feathersjs.com/api/services.html#update-id-data-params|Feathers API Documentation: .update(id, data, params)} + */ + _update (id: Id, data: D, params?: P): Promise; + + /** + * Merge any resources matching the given ID with the given data, skipping any service-level hooks. + * + * @param id - ID of the resource to be patched + * @param data - Data to merge with the current resource. + * @param params - Service call parameters {@link Params} + * @see {@link HookLessServiceMethods} + * @see {@link https://docs.feathersjs.com/api/services.html#patch-id-data-params|Feathers API Documentation: .patch(id, data, params)} + */ + _patch (id: null, data: Partial, params?: P): Promise; + _patch (id: Id, data: Partial, params?: P): Promise; + _patch (id: NullableId, data: Partial, params?: P): Promise; + + /** + * Remove resources matching the given ID from the this service, skipping any service-level hooks. + * + * @param id - ID of the resource to be removed + * @param params - Service call parameters {@link Params} + * @see {@link HookLessServiceMethods} + * @see {@link https://docs.feathersjs.com/api/services.html#remove-id-params|Feathers API Documentation: .remove(id, params)} + */ + _remove (id: null, params?: P): Promise; + _remove (id: Id, params?: P): Promise; + _remove (id: NullableId, params?: P): Promise; +} \ No newline at end of file diff --git a/packages/adapter-commons/src/filter-query.ts b/packages/adapter-commons/src/filter-query.ts index 415ac54cbc..4cc29e4472 100644 --- a/packages/adapter-commons/src/filter-query.ts +++ b/packages/adapter-commons/src/filter-query.ts @@ -1,5 +1,7 @@ import { _ } from '@feathersjs/commons'; import { BadRequest } from '@feathersjs/errors'; +import { Query } from '@feathersjs/feathers'; +import { FilterQueryOptions, FilterSettings } from './declarations'; function parse (number: any) { if (typeof number !== 'undefined') { @@ -37,7 +39,7 @@ function convertSort (sort: any) { }, {} as { [key: string]: number }); } -function cleanQuery (query: any, operators: any, filters: any): any { +function cleanQuery (query: Query, operators: any, filters: any): any { if (Array.isArray(query)) { return query.map(value => cleanQuery(value, operators, filters)); } else if (_.isObject(query) && query.constructor === {}.constructor) { @@ -68,7 +70,7 @@ function cleanQuery (query: any, operators: any, filters: any): any { return query; } -function assignFilters (object: any, query: any, filters: any, options: any) { +function assignFilters (object: any, query: Query, filters: FilterSettings, options: any): { [key: string]: any } { if (Array.isArray(filters)) { _.each(filters, (key) => { if (query[key] !== undefined) { @@ -88,7 +90,7 @@ function assignFilters (object: any, query: any, filters: any, options: any) { return object; } -export const FILTERS = { +export const FILTERS: FilterSettings = { $sort: (value: any) => convertSort(value), $limit: (value: any, options: any) => getLimit(parse(value), options.paginate), $skip: (value: any) => parse(value), @@ -100,17 +102,16 @@ export const OPERATORS = ['$in', '$nin', '$lt', '$lte', '$gt', '$gte', '$ne', '$ // Converts Feathers special query parameters and pagination settings // and returns them separately a `filters` and the rest of the query // as `query` -export function filterQuery (query: any, options: any = {}) { +export function filterQuery (query: Query, options: FilterQueryOptions = {}) { const { - filters: additionalFilters = {}, + filters: additionalFilters = [], operators: additionalOperators = [] } = options; - const result: { [key: string]: any } = {}; + const baseFilters = assignFilters({}, query, FILTERS, options); + const filters = assignFilters(baseFilters, query, additionalFilters, options); - result.filters = assignFilters({}, query, FILTERS, options); - result.filters = assignFilters(result.filters, query, additionalFilters, options); - - result.query = cleanQuery(query, OPERATORS.concat(additionalOperators), result.filters); - - return result; + return { + filters, + query: cleanQuery(query, OPERATORS.concat(additionalOperators), filters) as Query + } } diff --git a/packages/adapter-commons/src/index.ts b/packages/adapter-commons/src/index.ts index aa83aca48f..0b3777c516 100644 --- a/packages/adapter-commons/src/index.ts +++ b/packages/adapter-commons/src/index.ts @@ -1,13 +1,15 @@ import { _ } from '@feathersjs/commons'; +import { Params } from '@feathersjs/feathers'; -export { AdapterService, InternalServiceMethods, ServiceOptions, AdapterParams } from './service'; +export * from './declarations'; +export * from './service'; export { filterQuery, FILTERS, OPERATORS } from './filter-query'; export * from './sort'; // Return a function that filters a result object or array // and picks only the fields passed as `params.query.$select` // and additional `otherFields` -export function select (params: any, ...otherFields: string[]) { +export function select (params: Params, ...otherFields: string[]) { const queryFields: string[] | undefined = params?.query?.$select; if (!queryFields) { diff --git a/packages/adapter-commons/src/service.ts b/packages/adapter-commons/src/service.ts index 2a7dc973df..7ec607fbb6 100644 --- a/packages/adapter-commons/src/service.ts +++ b/packages/adapter-commons/src/service.ts @@ -1,5 +1,6 @@ import { NotImplemented, BadRequest, MethodNotAllowed } from '@feathersjs/errors'; -import { ServiceMethods, Params, Id, NullableId, Paginated } from '@feathersjs/feathers'; +import { ServiceMethods, Params, Id, NullableId, Paginated, Query } from '@feathersjs/feathers'; +import { AdapterParams, AdapterServiceOptions, FilterQueryOptions, PaginationOptions } from './declarations'; import { filterQuery } from './filter-query'; const callMethod = (self: any, name: any, ...args: any[]) => { @@ -16,121 +17,17 @@ const alwaysMulti: { [key: string]: boolean } = { update: false }; -export interface PaginationOptions { - default?: number; - max?: number; -} - -export interface ServiceOptions { - events?: string[]; - multi?: boolean|string[]; - id?: string; - paginate?: PaginationOptions - /** - * @deprecated renamed to `allow`. - */ - whitelist?: string[]; - allow?: string[]; - filters?: string[]; -} - -export interface AdapterOptions extends Pick { - Model?: M; -} - -export interface AdapterParams extends Params { - adapter?: Partial>; - paginate?: false|PaginationOptions; -} - /** - * Hook-less (internal) service methods. Directly call database adapter service methods - * without running any service-level hooks. This can be useful if you need the raw data - * from the service and don't want to trigger any of its hooks. - * - * Important: These methods are only available internally on the server, not on the client - * side and only for the Feathers database adapters. - * - * These methods do not trigger events. - * - * @see {@link https://docs.feathersjs.com/guides/migrating.html#hook-less-service-methods} + * The base class that a database adapter can extend from. */ -export interface InternalServiceMethods> { - - /** - * Retrieve all resources from this service, skipping any service-level hooks. - * - * @param params - Service call parameters {@link Params} - * @see {@link HookLessServiceMethods} - * @see {@link https://docs.feathersjs.com/api/services.html#find-params|Feathers API Documentation: .find(params)} - */ - _find (params?: AdapterParams): Promise>; - - /** - * Retrieve a single resource matching the given ID, skipping any service-level hooks. - * - * @param id - ID of the resource to locate - * @param params - Service call parameters {@link Params} - * @see {@link HookLessServiceMethods} - * @see {@link https://docs.feathersjs.com/api/services.html#get-id-params|Feathers API Documentation: .get(id, params)} - */ - _get (id: Id, params?: AdapterParams): Promise; - - /** - * Create a new resource for this service, skipping any service-level hooks. - * - * @param data - Data to insert into this service. - * @param params - Service call parameters {@link Params} - * @see {@link HookLessServiceMethods} - * @see {@link https://docs.feathersjs.com/api/services.html#create-data-params|Feathers API Documentation: .create(data, params)} - */ - _create (data: D | D[], params?: AdapterParams): Promise; - - /** - * Replace any resources matching the given ID with the given data, skipping any service-level hooks. - * - * @param id - ID of the resource to be updated - * @param data - Data to be put in place of the current resource. - * @param params - Service call parameters {@link Params} - * @see {@link HookLessServiceMethods} - * @see {@link https://docs.feathersjs.com/api/services.html#update-id-data-params|Feathers API Documentation: .update(id, data, params)} - */ - _update (id: Id, data: D, params?: AdapterParams): Promise; - - /** - * Merge any resources matching the given ID with the given data, skipping any service-level hooks. - * - * @param id - ID of the resource to be patched - * @param data - Data to merge with the current resource. - * @param params - Service call parameters {@link Params} - * @see {@link HookLessServiceMethods} - * @see {@link https://docs.feathersjs.com/api/services.html#patch-id-data-params|Feathers API Documentation: .patch(id, data, params)} - */ - _patch (id: NullableId, data: D, params?: AdapterParams): Promise; - - /** - * Remove resources matching the given ID from the this service, skipping any service-level hooks. - * - * @param id - ID of the resource to be removed - * @param params - Service call parameters {@link Params} - * @see {@link HookLessServiceMethods} - * @see {@link https://docs.feathersjs.com/api/services.html#remove-id-params|Feathers API Documentation: .remove(id, params)} - */ - _remove (id: NullableId, params?: AdapterParams): Promise; -} - -export class AdapterService< - T = any, - D = Partial, - O extends Partial = Partial -> implements ServiceMethods, D> { - options: ServiceOptions & O; +export class AdapterBase = Partial> { + options: AdapterServiceOptions & O; constructor (options: O) { this.options = Object.assign({ id: 'id', events: [], - paginate: {}, + paginate: false, multi: false, filters: [], allow: [] @@ -145,19 +42,23 @@ export class AdapterService< return this.options.events; } - filterQuery (params: AdapterParams = {}, opts: any = {}) { + filterQuery (params: AdapterParams = {}, opts: FilterQueryOptions = {}) { const paginate = typeof params.paginate !== 'undefined' ? params.paginate : this.getOptions(params).paginate; - const { query = {} } = params; - const options = Object.assign({ - operators: this.options.whitelist || this.options.allow || [], + const query: Query = { ...params.query }; + const options = { + operators: this.options.whitelist || this.options.allow || [], filters: this.options.filters, - paginate - }, opts); + paginate, + ...opts + }; const result = filterQuery(query, options); - return Object.assign(result, { paginate }); + return { + paginate, + ...result + } } allowsMulti (method: string, params: AdapterParams = {}) { @@ -176,24 +77,34 @@ export class AdapterService< return option.includes(method); } - getOptions (params: AdapterParams): ServiceOptions & { model?: any } { + getOptions (params: AdapterParams): AdapterServiceOptions & { model?: any } { return { ...this.options, ...params.adapter } } +} - find (params?: AdapterParams): Promise> { +export class AdapterService< + T = any, + D = Partial, + O extends Partial = Partial, + P extends Params = AdapterParams +> extends AdapterBase implements ServiceMethods, D> { + find (params?: P & { paginate?: PaginationOptions }): Promise>; + find (params?: P & { paginate: false }): Promise; + find (params?: P): Promise>; + find (params?: P): Promise> { return callMethod(this, '_find', params); } - get (id: Id, params?: AdapterParams): Promise { + get (id: Id, params?: P): Promise { return callMethod(this, '_get', id, params); } - create (data: Partial, params?: AdapterParams): Promise; - create (data: Partial[], params?: AdapterParams): Promise; - create (data: Partial | Partial[], params?: AdapterParams): Promise { + create (data: Partial, params?: P): Promise; + create (data: Partial[], params?: P): Promise; + create (data: Partial | Partial[], params?: P): Promise { if (Array.isArray(data) && !this.allowsMulti('create', params)) { return Promise.reject(new MethodNotAllowed('Can not create multiple entries')); } @@ -201,7 +112,7 @@ export class AdapterService< return callMethod(this, '_create', data, params); } - update (id: Id, data: D, params?: AdapterParams): Promise { + update (id: Id, data: D, params?: P): Promise { if (id === null || Array.isArray(data)) { return Promise.reject(new BadRequest( 'You can not replace multiple instances. Did you mean \'patch\'?' @@ -211,10 +122,9 @@ export class AdapterService< return callMethod(this, '_update', id, data, params); } - patch (id: Id, data: Partial, params?: AdapterParams): Promise; - patch (id: null, data: Partial, params?: AdapterParams): Promise; - patch (id: NullableId, data: Partial, params?: AdapterParams): Promise; - patch (id: NullableId, data: Partial, params?: AdapterParams): Promise { + patch (id: Id, data: Partial, params?: P): Promise; + patch (id: null, data: Partial, params?: P): Promise; + patch (id: NullableId, data: Partial, params?: P): Promise { if (id === null && !this.allowsMulti('patch', params)) { return Promise.reject(new MethodNotAllowed('Can not patch multiple entries')); } @@ -222,10 +132,9 @@ export class AdapterService< return callMethod(this, '_patch', id, data, params); } - remove (id: Id, params?: AdapterParams): Promise; - remove (id: null, params?: AdapterParams): Promise; - remove (id: NullableId, params?: AdapterParams): Promise; - remove (id: NullableId, params?: AdapterParams): Promise { + remove (id: Id, params?: P): Promise; + remove (id: null, params?: P): Promise; + remove (id: NullableId, params?: P): Promise { if (id === null && !this.allowsMulti('remove', params)) { return Promise.reject(new MethodNotAllowed('Can not remove multiple entries')); } diff --git a/packages/adapter-commons/test/filter-query.test.ts b/packages/adapter-commons/test/filter-query.test.ts index 9db9b94790..b295c9c197 100644 --- a/packages/adapter-commons/test/filter-query.test.ts +++ b/packages/adapter-commons/test/filter-query.test.ts @@ -88,7 +88,7 @@ describe('@feathersjs/adapter-commons/filterQuery', () => { }); it('allows $limit 0', () => { - const { filters } = filterQuery({ $limit: 0 }, { default: 10 }); + const { filters } = filterQuery({ $limit: 0 }, { paginate: { default: 10 } }); assert.strictEqual(filters.$limit, 0); }); diff --git a/packages/adapter-commons/test/service.test.ts b/packages/adapter-commons/test/service.test.ts index 5329df86ef..2f5635a970 100644 --- a/packages/adapter-commons/test/service.test.ts +++ b/packages/adapter-commons/test/service.test.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import assert from 'assert'; import { NotImplemented } from '@feathersjs/errors'; -import { AdapterService, InternalServiceMethods } from '../src'; -import { Params, Id, NullableId } from '@feathersjs/feathers'; +import { AdapterService, InternalServiceMethods, PaginationOptions } from '../src'; +import { Id, NullableId, Paginated } from '@feathersjs/feathers'; +import { AdapterParams } from '../lib'; const METHODS: [ 'find', 'get', 'create', 'update', 'patch', 'remove' ] = [ 'find', 'get', 'create', 'update', 'patch', 'remove' ]; @@ -27,29 +28,67 @@ describe('@feathersjs/adapter-commons/service', () => { }); describe('works when methods exist', () => { - class MethodService extends AdapterService implements InternalServiceMethods { - _find (_params?: Params) { - return Promise.resolve([]); + type Data = { + id: Id + } + + class MethodService extends AdapterService implements InternalServiceMethods { + _find (_params?: AdapterParams & { paginate?: PaginationOptions }): Promise>; + _find (_params?: AdapterParams & { paginate: false }): Promise; + async _find (params?: AdapterParams): Promise|Data[]> { + if (params && params.paginate === false) { + return { + total: 0, + limit: 10, + skip: 0, + data: [] + } + } + + return []; } - _get (id: Id, _params?: Params) { - return Promise.resolve({ id }); + async _get (id: Id, _params?: AdapterParams) { + return { id }; } - _create (data: Partial | Partial[], _params?: Params) { - return Promise.resolve(data); + async _create (data: Partial[], _params?: AdapterParams): Promise; + async _create (data: Partial, _params?: AdapterParams): Promise; + async _create (data: Partial|Partial[], _params?: AdapterParams): Promise { + if (Array.isArray(data)) { + return [{ + id: 'something' + }]; + } + + return { + id: 'something', + ...data + } } - _update (id: NullableId, _data: any, _params?: Params) { + async _update (id: NullableId, _data: any, _params?: AdapterParams) { return Promise.resolve({ id }); } - _patch (id: NullableId, _data: any, _params?: Params) { - return Promise.resolve({ id }); + async _patch (id: null, _data: any, _params?: AdapterParams): Promise; + async _patch (id: Id, _data: any, _params?: AdapterParams): Promise; + async _patch (id: NullableId, _data: any, _params?: AdapterParams): Promise { + if (id === null) { + return [] + } + + return { id }; } - _remove (id: NullableId, _params?: Params) { - return Promise.resolve({ id }); + async _remove (id: null, _params?: AdapterParams): Promise; + async _remove (id: Id, _params?: AdapterParams): Promise; + async _remove (id: NullableId, _params?: AdapterParams) { + if (id === null) { + return [] as Data[]; + } + + return { id }; } } @@ -116,24 +155,24 @@ describe('@feathersjs/adapter-commons/service', () => { it('filterQuery', () => { const service = new CustomService({ - whitelist: [ '$something' ] + allow: [ '$something' ] }); const filtered = service.filterQuery({ query: { $limit: 10, test: 'me' } }); assert.deepStrictEqual(filtered, { - paginate: {}, + paginate: false, filters: { $limit: 10 }, query: { test: 'me' } }); - const withWhitelisted = service.filterQuery({ + const withAllowed = service.filterQuery({ query: { $limit: 10, $something: 'else' } }); - assert.deepStrictEqual(withWhitelisted, { - paginate: {}, + assert.deepStrictEqual(withAllowed, { + paginate: false, filters: { $limit: 10 }, query: { $something: 'else' } }); diff --git a/packages/authentication-local/test/fixture.ts b/packages/authentication-local/test/fixture.ts index 2bea3964f9..5da5e368b3 100644 --- a/packages/authentication-local/test/fixture.ts +++ b/packages/authentication-local/test/fixture.ts @@ -1,5 +1,5 @@ import { feathers } from '@feathersjs/feathers'; -import { memory, Service as MemoryService } from '@feathersjs/memory'; +import { memory, MemoryService } from '@feathersjs/memory'; import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication'; import { LocalStrategy, hooks } from '../src'; diff --git a/packages/authentication/test/service.test.ts b/packages/authentication/test/service.test.ts index acddbbe8b5..626c235dbd 100644 --- a/packages/authentication/test/service.test.ts +++ b/packages/authentication/test/service.test.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import omit from 'lodash/omit'; import jwt from 'jsonwebtoken'; import { feathers, Application } from '@feathersjs/feathers'; -import { memory, Service as MemoryService } from '@feathersjs/memory'; +import { memory, MemoryService } from '@feathersjs/memory'; import { defaultOptions } from '../src/options'; import { AuthenticationService } from '../src'; diff --git a/packages/client/test/fixture.ts b/packages/client/test/fixture.ts index c286bc1a8f..2ae867485d 100644 --- a/packages/client/test/fixture.ts +++ b/packages/client/test/fixture.ts @@ -1,6 +1,6 @@ import { feathers, Application, HookContext, Id, Params } from '@feathersjs/feathers'; import * as express from '@feathersjs/express'; -import { Service } from '@feathersjs/memory'; +import { MemoryService } from '@feathersjs/memory'; // eslint-disable-next-line no-extend-native Object.defineProperty(Error.prototype, 'toJSON', { @@ -17,7 +17,7 @@ Object.defineProperty(Error.prototype, 'toJSON', { }); // Create an in-memory CRUD service for our Todos -class TodoService extends Service { +class TodoService extends MemoryService { async get (id: Id, params: Params) { if (params.query.error) { throw new Error('Something went wrong'); diff --git a/packages/feathers/src/declarations.ts b/packages/feathers/src/declarations.ts index ad7a90fc4b..01d49ebfd1 100644 --- a/packages/feathers/src/declarations.ts +++ b/packages/feathers/src/declarations.ts @@ -34,9 +34,9 @@ export interface ServiceMethods, P = Params> { remove (id: NullableId, params?: P): Promise; - setup (app: Application, path: string): Promise; + setup? (app: Application, path: string): Promise; - teardown (app: Application, path: string): Promise; + teardown? (app: Application, path: string): Promise; } export interface ServiceOverloads, P = Params> { diff --git a/packages/memory/src/index.ts b/packages/memory/src/index.ts index 7c4ab0f12d..a05ea48b82 100644 --- a/packages/memory/src/index.ts +++ b/packages/memory/src/index.ts @@ -1,14 +1,14 @@ -import { NotFound } from '@feathersjs/errors'; +import { MethodNotAllowed, NotFound } from '@feathersjs/errors'; import { _ } from '@feathersjs/commons'; -import { sorter, select, AdapterService, ServiceOptions, InternalServiceMethods, AdapterParams } from '@feathersjs/adapter-commons'; +import { sorter, select, AdapterBase, AdapterServiceOptions, InternalServiceMethods, PaginationOptions } from '@feathersjs/adapter-commons'; import sift from 'sift'; -import { NullableId, Id } from '@feathersjs/feathers'; +import { NullableId, Id, Params, ServiceMethods, Paginated } from '@feathersjs/feathers'; export interface MemoryServiceStore { [key: string]: T; } -export interface MemoryServiceOptions extends ServiceOptions { +export interface MemoryServiceOptions extends AdapterServiceOptions { store: MemoryServiceStore; startId: number; matcher?: (query: any) => any; @@ -21,31 +21,40 @@ const _select = (data: any, params: any, ...args: any[]) => { return base(JSON.parse(JSON.stringify(data))); }; -export class Service> extends AdapterService implements InternalServiceMethods { - options: MemoryServiceOptions; +export class MemoryAdapter, P extends Params = Params> extends AdapterBase> + implements InternalServiceMethods { store: MemoryServiceStore; _uId: number; constructor (options: Partial> = {}) { - super(_.extend({ + super({ id: 'id', matcher: sift, - sorter - }, options)); - this._uId = options.startId || 0; - this.store = options.store || {}; + sorter, + store: {}, + startId: 0, + ...options + }); + this._uId = this.options.startId; + this.store = { ...this.options.store }; } - async getEntries (params = {}) { + async getEntries (_params?: P) { + const params = _params || {} as P; const { query } = this.filterQuery(params); - return this._find(Object.assign({}, params, { + return this._find({ + ...params, paginate: false, query - }) as any) as Promise; + }); } - async _find (params: AdapterParams = {}) { + async _find (_params?: P & { paginate?: PaginationOptions }): Promise>; + async _find (_params?: P & { paginate: false }): Promise; + async _find (_params?: P): Promise|T[]>; + async _find (_params?: P): Promise|T[]> { + const params = _params || {} as P; const { query, filters, paginate } = this.filterQuery(params); let values = _.values(this.store).filter(this.options.matcher(query)); const total = values.length; @@ -62,21 +71,23 @@ export class Service> extends AdapterService imple values = values.slice(0, filters.$limit); } - const result = { + const result: Paginated = { total, limit: filters.$limit, skip: filters.$skip || 0, data: values.map(value => _select(value, params)) }; - if (!(paginate && (paginate ).default)) { + if (!(paginate && paginate.default)) { return result.data; } return result; } - async _get (id: Id, params: AdapterParams = {}) { + async _get (id: Id, _params?: P): Promise { + const params = _params || {} as P; + if (id in this.store) { const { query } = this.filterQuery(params); const value = this.store[id]; @@ -90,22 +101,35 @@ export class Service> extends AdapterService imple } // Create without hooks and mixins that can be used internally - async _create (data: Partial | Partial[], params: AdapterParams = {}): Promise { + async _create (data: Partial, params?: P): Promise; + async _create (data: Partial[], params?: P): Promise; + async _create (data: Partial|Partial[], _params?: P): Promise; + async _create (data: Partial|Partial[], _params?: P): Promise { + const params = _params || {} as P; + if (Array.isArray(data)) { - return Promise.all(data.map(current => this._create(current, params) as Promise)); + if (!this.allowsMulti('create', params)) { + throw new MethodNotAllowed('Can not create multiple entries'); + } + + return Promise.all(data.map(current => this._create(current, params))); } const id = (data as any)[this.id] || this._uId++; const current = _.extend({}, data, { [this.id]: id }); const result = (this.store[id] = current); - return _select(result, params, this.id); + return _select(result, params, this.id) as T; } - async _update (id: NullableId, data: T, params: AdapterParams = {}) { + async _update (id: Id, data: D, params: P = {} as P): Promise { + if (id === null || Array.isArray(data)) { + throw new MethodNotAllowed('You can not replace multiple instances. Did you mean \'patch\'?'); + } + const oldEntry = await this._get(id); // We don't want our id to change type if it can be coerced - const oldId = oldEntry[this.id]; + const oldId = (oldEntry as any)[this.id]; // eslint-disable-next-line eqeqeq id = oldId == id ? oldId : id; @@ -115,7 +139,11 @@ export class Service> extends AdapterService imple return this._get(id, params); } - async _patch (id: NullableId, data: Partial, params: AdapterParams = {}) { + async _patch (id: null, data: Partial, params?: P): Promise; + async _patch (id: Id, data: Partial, params?: P): Promise; + async _patch (id: NullableId, data: Partial, _params?: P): Promise; + async _patch (id: NullableId, data: Partial, _params?: P): Promise { + const params = _params || {} as P; const patchEntry = (entry: T) => { const currentId = (entry as any)[this.id]; @@ -125,6 +153,10 @@ export class Service> extends AdapterService imple }; if (id === null) { + if(!this.allowsMulti('patch', params)) { + throw new MethodNotAllowed('Can not patch multiple entries'); + } + const entries = await this.getEntries(params); return entries.map(patchEntry); @@ -134,12 +166,21 @@ export class Service> extends AdapterService imple } // Remove without hooks and mixins that can be used internally - async _remove (id: NullableId, params: AdapterParams = {}): Promise { + async _remove (id: null, params?: P): Promise; + async _remove (id: Id, params?: P): Promise; + async _remove (id: NullableId, _params?: P): Promise; + async _remove (id: NullableId, _params?: P): Promise { + const params = _params || {} as P; + if (id === null) { + if(!this.allowsMulti('remove', params)) { + throw new MethodNotAllowed('Can not remove multiple entries'); + } + const entries = await this.getEntries(params); return Promise.all(entries.map(current => - this._remove((current as any)[this.id], params) as Promise + this._remove((current as any)[this.id] as Id, params) )); } @@ -151,6 +192,44 @@ export class Service> extends AdapterService imple } } -export function memory (options: Partial = {}) { - return new Service(options); +export class MemoryService, P extends Params = Params> + extends MemoryAdapter implements ServiceMethods, D, P> { + async find (params?: P & { paginate?: PaginationOptions }): Promise>; + async find (params?: P & { paginate: false }): Promise; + async find (params?: P): Promise|T[]>; + async find (params?: P): Promise|T[]> { + return this._find(params) + } + + async get (id: Id, params?: P): Promise { + return this._get(id, params); + } + + async create (data: Partial, params?: P): Promise; + async create (data: Partial[], params?: P): Promise; + async create (data: Partial|Partial[], params?: P): Promise { + return this._create(data, params); + } + + async update (id: Id, data: D, params?: P): Promise { + return this._update(id, data, params); + } + + async patch (id: Id, data: Partial, params?: P): Promise; + async patch (id: null, data: Partial, params?: P): Promise; + async patch (id: NullableId, data: Partial, params?: P): Promise { + return this._patch(id, data, params); + } + + async remove (id: Id, params?: P): Promise; + async remove (id: null, params?: P): Promise; + async remove (id: NullableId, params?: P): Promise { + return this._remove(id, params); + } } + +export function memory, P extends Params = Params> ( + options: Partial> = {} +) { + return new MemoryService(options) +} \ No newline at end of file diff --git a/packages/memory/test/index.test.ts b/packages/memory/test/index.test.ts index c210610629..386bc883de 100644 --- a/packages/memory/test/index.test.ts +++ b/packages/memory/test/index.test.ts @@ -3,7 +3,7 @@ import adapterTests from '@feathersjs/adapter-tests'; import errors from '@feathersjs/errors'; import { feathers } from '@feathersjs/feathers'; -import { memory } from '../src'; +import { MemoryService } from '../src'; const testSuite = adapterTests([ '.options', @@ -79,12 +79,30 @@ const testSuite = adapterTests([ ]); describe('Feathers Memory Service', () => { + type Person = { + id: number; + name: string; + age: number; + } + + type Animal = { + type: string; + age: number; + } + const events = [ 'testing' ]; - const app = feathers() - .use('/people', memory({ events })) - .use('/people-customid', memory({ - id: 'customid', events - })); + const app = feathers<{ + people: MemoryService, + 'people-customid': MemoryService, + animals: MemoryService, + matcher: MemoryService + }>(); + + app.use('people', new MemoryService({ events })); + app.use('people-customid', new MemoryService({ + id: 'customid', + events + })); it('update with string id works', async () => { const people = app.service('people'); @@ -101,7 +119,7 @@ describe('Feathers Memory Service', () => { }); it('patch record with prop also in query', async () => { - app.use('/animals', memory({ multi: true })); + app.use('animals', new MemoryService({ multi: true })); const animals = app.service('animals'); await animals.create([{ type: 'cat', @@ -122,7 +140,7 @@ describe('Feathers Memory Service', () => { let sorterCalled = false; let matcherCalled = false; - app.use('/matcher', memory({ + app.use('matcher', new MemoryService({ matcher () { matcherCalled = true; return function () { @@ -169,6 +187,7 @@ describe('Feathers Memory Service', () => { name: 'Tester' }); const results = await people.find({ + paginate: false, query: { name: 'Tester', $select: ['name'] diff --git a/packages/rest-client/test/server.ts b/packages/rest-client/test/server.ts index d7ca93150e..a3c58bcb47 100644 --- a/packages/rest-client/test/server.ts +++ b/packages/rest-client/test/server.ts @@ -1,6 +1,6 @@ import { feathers, Id, NullableId, Params } from '@feathersjs/feathers'; import expressify, { rest, urlencoded, json } from '@feathersjs/express'; -import { Service } from '@feathersjs/memory'; +import { MemoryService } from '@feathersjs/memory'; import { FeathersError, NotAcceptable } from '@feathersjs/errors'; // eslint-disable-next-line no-extend-native @@ -34,7 +34,7 @@ interface TodoServiceParams extends Params { } // Create an in-memory CRUD service for our Todos -class TodoService extends Service { +class TodoService extends MemoryService { get (id: Id, params: TodoServiceParams) { if (params.query.error) { throw new Error('Something went wrong'); diff --git a/packages/schema/test/fixture.ts b/packages/schema/test/fixture.ts index 06e0fd6df2..d67291b0d9 100644 --- a/packages/schema/test/fixture.ts +++ b/packages/schema/test/fixture.ts @@ -1,13 +1,14 @@ import { feathers, HookContext, Application as FeathersApplication } from '@feathersjs/feathers'; -import { memory, Service } from '@feathersjs/memory'; +import { memory, MemoryService } from '@feathersjs/memory'; import { GeneralError } from '@feathersjs/errors'; import { schema, resolve, Infer, resolveResult, queryProperty, resolveQuery, resolveData, validateData, validateQuery } from '../src'; +import { AdapterParams } from '../../memory/node_modules/@feathersjs/adapter-commons/lib'; export const userSchema = schema({ $id: 'UserData', @@ -135,10 +136,15 @@ export const messageQueryResolver = resolve, - messages: Service - pagintedMessages: Service + users: MemoryService, + messages: MemoryService + paginatedMessages: MemoryService } type Application = FeathersApplication; @@ -147,8 +153,7 @@ const app = feathers() multi: ['create'] })) .use('messages', memory()) - .use('pagintedMessages', memory({paginate: { default: 10 }})) - ; + .use('paginatedMessages', memory({paginate: { default: 10 }})); app.service('messages').hooks([ validateQuery(messageQuerySchema), @@ -156,7 +161,7 @@ app.service('messages').hooks([ resolveResult(messageResultResolver) ]); -app.service('pagintedMessages').hooks([ +app.service('paginatedMessages').hooks([ validateQuery(messageQuerySchema), resolveQuery(messageQueryResolver), resolveResult(messageResultResolver) diff --git a/packages/schema/test/hooks.test.ts b/packages/schema/test/hooks.test.ts index 338a3fb331..16f523eb27 100644 --- a/packages/schema/test/hooks.test.ts +++ b/packages/schema/test/hooks.test.ts @@ -17,7 +17,7 @@ describe('@feathersjs/schema/hooks', () => { text, userId: user.id }); - messageOnPaginatedService = await app.service('pagintedMessages').create({ + messageOnPaginatedService = await app.service('paginatedMessages').create({ text, userId: user.id }); @@ -58,7 +58,7 @@ describe('@feathersjs/schema/hooks', () => { await assert.rejects(() => app.service('messages').find({ provider: 'external', error: true - } as any), { + }), { name: 'BadRequest', message: 'Error resolving data', code: 400, @@ -117,7 +117,7 @@ describe('@feathersjs/schema/hooks', () => { ...payload }); - const messages = await app.service('pagintedMessages').find({ + const messages = await app.service('paginatedMessages').find({ provider: 'external', query: { $limit: 1, @@ -144,16 +144,18 @@ describe('@feathersjs/schema/hooks', () => { }); const messages = await app.service('messages').find({ + paginate: false, query: { userId: `${user.id}` } - }) as MessageResult[]; + }); assert.strictEqual(messages.length, 1); const userMessages = await app.service('messages').find({ + paginate: false, user - } as any) as MessageResult[]; + }); assert.strictEqual(userMessages.length, 1); assert.strictEqual(userMessages[0].userId, user.id); diff --git a/packages/socketio-client/test/server.ts b/packages/socketio-client/test/server.ts index e9eac21138..26cef361c1 100644 --- a/packages/socketio-client/test/server.ts +++ b/packages/socketio-client/test/server.ts @@ -1,7 +1,7 @@ import { feathers, Id, Params } from '@feathersjs/feathers'; import socketio from '@feathersjs/socketio'; import '@feathersjs/transport-commons'; -import { Service } from '@feathersjs/memory'; +import { MemoryService } from '@feathersjs/memory'; // eslint-disable-next-line no-extend-native Object.defineProperty(Error.prototype, 'toJSON', { @@ -19,7 +19,7 @@ Object.defineProperty(Error.prototype, 'toJSON', { }); // Create an in-memory CRUD service for our Todos -class TodoService extends Service { +class TodoService extends MemoryService { async get (id: Id, params: Params) { if (params.query.error) { throw new Error('Something went wrong');