@feathersjs/adapter-commons 5.0.0-pre.9 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/query.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { _ } from '@feathersjs/commons'
2
+ import { BadRequest } from '@feathersjs/errors'
3
+ import { Query } from '@feathersjs/feathers'
4
+ import { FilterQueryOptions, FilterSettings, PaginationParams } from './declarations'
5
+
6
+ const parse = (value: any) => (typeof value !== 'undefined' ? parseInt(value, 10) : value)
7
+
8
+ const isPlainObject = (value: any) => _.isObject(value) && value.constructor === {}.constructor
9
+
10
+ const validateQueryProperty = (query: any, operators: string[] = []): Query => {
11
+ if (!isPlainObject(query)) {
12
+ return query
13
+ }
14
+
15
+ for (const key of Object.keys(query)) {
16
+ if (key.startsWith('$') && !operators.includes(key)) {
17
+ throw new BadRequest(`Invalid query parameter ${key}`, query)
18
+ }
19
+
20
+ const value = query[key]
21
+
22
+ if (isPlainObject(value)) {
23
+ query[key] = validateQueryProperty(value, operators)
24
+ }
25
+ }
26
+
27
+ return {
28
+ ...query
29
+ }
30
+ }
31
+
32
+ const getFilters = (query: Query, settings: FilterQueryOptions) => {
33
+ const filterNames = Object.keys(settings.filters)
34
+
35
+ return filterNames.reduce((current, key) => {
36
+ const queryValue = query[key]
37
+ const filter = settings.filters[key]
38
+
39
+ if (filter) {
40
+ const value = typeof filter === 'function' ? filter(queryValue, settings) : queryValue
41
+
42
+ if (value !== undefined) {
43
+ current[key] = value
44
+ }
45
+ }
46
+
47
+ return current
48
+ }, {} as { [key: string]: any })
49
+ }
50
+
51
+ const getQuery = (query: Query, settings: FilterQueryOptions) => {
52
+ const keys = Object.keys(query).concat(Object.getOwnPropertySymbols(query) as any as string[])
53
+
54
+ return keys.reduce((result, key) => {
55
+ if (typeof key === 'string' && key.startsWith('$')) {
56
+ if (settings.filters[key] === undefined) {
57
+ throw new BadRequest(`Invalid filter value ${key}`)
58
+ }
59
+ } else {
60
+ result[key] = validateQueryProperty(query[key], settings.operators)
61
+ }
62
+
63
+ return result
64
+ }, {} as Query)
65
+ }
66
+
67
+ /**
68
+ * Returns the converted `$limit` value based on the `paginate` configuration.
69
+ * @param _limit The limit value
70
+ * @param paginate The pagination options
71
+ * @returns The converted $limit value
72
+ */
73
+ export const getLimit = (_limit: any, paginate?: PaginationParams) => {
74
+ const limit = parse(_limit)
75
+
76
+ if (paginate && (paginate.default || paginate.max)) {
77
+ const base = paginate.default || 0
78
+ const lower = typeof limit === 'number' && !isNaN(limit) && limit >= 0 ? limit : base
79
+ const upper = typeof paginate.max === 'number' ? paginate.max : Number.MAX_VALUE
80
+
81
+ return Math.min(lower, upper)
82
+ }
83
+
84
+ return limit
85
+ }
86
+
87
+ export const OPERATORS = ['$in', '$nin', '$lt', '$lte', '$gt', '$gte', '$ne', '$or']
88
+
89
+ export const FILTERS: FilterSettings = {
90
+ $skip: (value: any) => parse(value),
91
+ $sort: (sort: any): { [key: string]: number } => {
92
+ if (typeof sort !== 'object' || Array.isArray(sort)) {
93
+ return sort
94
+ }
95
+
96
+ return Object.keys(sort).reduce((result, key) => {
97
+ result[key] = typeof sort[key] === 'object' ? sort[key] : parse(sort[key])
98
+
99
+ return result
100
+ }, {} as { [key: string]: number })
101
+ },
102
+ $limit: (_limit: any, { paginate }: FilterQueryOptions) => getLimit(_limit, paginate),
103
+ $select: (select: any) => {
104
+ if (Array.isArray(select)) {
105
+ return select.map((current) => `${current}`)
106
+ }
107
+
108
+ return select
109
+ },
110
+ $or: (or: any, { operators }: FilterQueryOptions) => {
111
+ if (Array.isArray(or)) {
112
+ return or.map((current) => validateQueryProperty(current, operators))
113
+ }
114
+
115
+ return or
116
+ },
117
+ $and: (and: any, { operators }: FilterQueryOptions) => {
118
+ if (Array.isArray(and)) {
119
+ return and.map((current) => validateQueryProperty(current, operators))
120
+ }
121
+
122
+ return and
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Converts Feathers special query parameters and pagination settings
128
+ * and returns them separately as `filters` and the rest of the query
129
+ * as `query`. `options` also gets passed the pagination settings and
130
+ * a list of additional `operators` to allow when querying properties.
131
+ *
132
+ * @param query The initial query
133
+ * @param options Options for filtering the query
134
+ * @returns An object with `query` which contains the query without `filters`
135
+ * and `filters` which contains the converted values for each filter.
136
+ */
137
+ export function filterQuery(_query: Query, options: FilterQueryOptions = {}) {
138
+ const query = _query || {}
139
+ const settings = {
140
+ ...options,
141
+ filters: {
142
+ ...FILTERS,
143
+ ...options.filters
144
+ },
145
+ operators: OPERATORS.concat(options.operators || [])
146
+ }
147
+
148
+ return {
149
+ filters: getFilters(query, settings),
150
+ query: getQuery(query, settings)
151
+ }
152
+ }
package/src/service.ts CHANGED
@@ -1,231 +1,188 @@
1
- import { NotImplemented, BadRequest, MethodNotAllowed } from '@feathersjs/errors';
2
- import { ServiceMethods, Params, Id, NullableId, Paginated } from '@feathersjs/feathers';
3
- import { filterQuery } from './filter-query';
1
+ import { Id, Paginated, Query } from '@feathersjs/feathers'
2
+ import {
3
+ AdapterParams,
4
+ AdapterServiceOptions,
5
+ InternalServiceMethods,
6
+ PaginationOptions
7
+ } from './declarations'
8
+ import { filterQuery } from './query'
4
9
 
5
- const callMethod = (self: any, name: any, ...args: any[]) => {
6
- if (typeof self[name] !== 'function') {
7
- return Promise.reject(new NotImplemented(`Method ${name} not available`));
8
- }
9
-
10
- return self[name](...args);
11
- };
10
+ export const VALIDATED = Symbol('@feathersjs/adapter/sanitized')
12
11
 
13
12
  const alwaysMulti: { [key: string]: boolean } = {
14
13
  find: true,
15
14
  get: false,
16
15
  update: false
17
- };
18
-
19
- export interface ServiceOptions {
20
- events?: string[];
21
- multi: boolean|string[];
22
- id: string;
23
- paginate: {
24
- default?: number;
25
- max?: number;
26
- }
27
- whitelist?: string[];
28
- allow: string[];
29
- filters: string[];
30
- }
31
-
32
- export interface AdapterOptions<M = any> extends Pick<ServiceOptions, 'multi'|'allow'|'paginate'> {
33
- Model?: M;
34
- }
35
-
36
- export interface AdapterParams<M = any> extends Params {
37
- adapter?: Partial<AdapterOptions<M>>;
38
16
  }
39
17
 
40
18
  /**
41
- * Hook-less (internal) service methods. Directly call database adapter service methods
42
- * without running any service-level hooks. This can be useful if you need the raw data
43
- * from the service and don't want to trigger any of its hooks.
44
- *
45
- * Important: These methods are only available internally on the server, not on the client
46
- * side and only for the Feathers database adapters.
47
- *
48
- * These methods do not trigger events.
49
- *
50
- * @see {@link https://docs.feathersjs.com/guides/migrating.html#hook-less-service-methods}
19
+ * An abstract base class that a database adapter can extend from to implement the
20
+ * `__find`, `__get`, `__update`, `__patch` and `__remove` methods.
51
21
  */
52
- export interface InternalServiceMethods<T = any, D = Partial<T>> {
22
+ export abstract class AdapterBase<
23
+ Result = any,
24
+ Data = Result,
25
+ PatchData = Partial<Data>,
26
+ ServiceParams extends AdapterParams = AdapterParams,
27
+ Options extends AdapterServiceOptions = AdapterServiceOptions,
28
+ IdType = Id
29
+ > implements InternalServiceMethods<Result, Data, PatchData, ServiceParams, IdType>
30
+ {
31
+ options: Options
32
+
33
+ constructor(options: Options) {
34
+ this.options = {
35
+ id: 'id',
36
+ events: [],
37
+ paginate: false,
38
+ multi: false,
39
+ filters: {},
40
+ operators: [],
41
+ ...options
42
+ }
43
+ }
44
+
45
+ get id() {
46
+ return this.options.id
47
+ }
48
+
49
+ get events() {
50
+ return this.options.events
51
+ }
53
52
 
54
53
  /**
55
- * Retrieve all resources from this service, skipping any service-level hooks.
54
+ * Check if this adapter allows multiple updates for a method.
55
+ * @param method The method name to check.
56
+ * @param params The service call params.
57
+ * @returns Wether or not multiple updates are allowed.
58
+ */
59
+ allowsMulti(method: string, params: ServiceParams = {} as ServiceParams) {
60
+ const always = alwaysMulti[method]
61
+
62
+ if (typeof always !== 'undefined') {
63
+ return always
64
+ }
65
+
66
+ const { multi } = this.getOptions(params)
67
+
68
+ if (multi === true || !multi) {
69
+ return multi
70
+ }
71
+
72
+ return multi.includes(method)
73
+ }
74
+
75
+ /**
76
+ * Returns the combined options for a service call. Options will be merged
77
+ * with `this.options` and `params.adapter` for dynamic overrides.
56
78
  *
57
- * @param params - Service call parameters {@link Params}
58
- * @see {@link HookLessServiceMethods}
59
- * @see {@link https://docs.feathersjs.com/api/services.html#find-params|Feathers API Documentation: .find(params)}
79
+ * @param params The parameters for the service method call
80
+ * @returns The actual options for this call
81
+ */
82
+ getOptions(params: ServiceParams): Options {
83
+ const paginate = params.paginate !== undefined ? params.paginate : this.options.paginate
84
+
85
+ return {
86
+ ...this.options,
87
+ paginate,
88
+ ...params.adapter
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Returns a sanitized version of `params.query`, converting filter values
94
+ * (like $limit and $skip) into the expected type. Will throw an error if
95
+ * a `$` prefixed filter or operator value that is not allowed in `filters`
96
+ * or `operators` is encountered.
97
+ *
98
+ * @param params The service call parameter.
99
+ * @returns A new object containing the sanitized query.
100
+ */
101
+ async sanitizeQuery(params: ServiceParams = {} as ServiceParams): Promise<Query> {
102
+ // We don't need legacy query sanitisation if the query has been validated by a schema already
103
+ if (params.query && (params.query as any)[VALIDATED]) {
104
+ return params.query || {}
105
+ }
106
+
107
+ const options = this.getOptions(params)
108
+ const { query, filters } = filterQuery(params.query, options)
109
+
110
+ return {
111
+ ...filters,
112
+ ...query
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Retrieve all resources from this service.
118
+ * Does not sanitize the query and should only be used on the server.
119
+ *
120
+ * @param _params - Service call parameters {@link ServiceParams}
60
121
  */
61
- _find (params?: AdapterParams): Promise<T | T[] | Paginated<T>>;
122
+ abstract _find(_params?: ServiceParams & { paginate?: PaginationOptions }): Promise<Paginated<Result>>
123
+ abstract _find(_params?: ServiceParams & { paginate: false }): Promise<Result[]>
124
+ abstract _find(params?: ServiceParams): Promise<Result[] | Paginated<Result>>
62
125
 
63
126
  /**
64
127
  * Retrieve a single resource matching the given ID, skipping any service-level hooks.
128
+ * Does not sanitize the query and should only be used on the server.
65
129
  *
66
130
  * @param id - ID of the resource to locate
67
- * @param params - Service call parameters {@link Params}
131
+ * @param params - Service call parameters {@link ServiceParams}
68
132
  * @see {@link HookLessServiceMethods}
69
133
  * @see {@link https://docs.feathersjs.com/api/services.html#get-id-params|Feathers API Documentation: .get(id, params)}
70
134
  */
71
- _get (id: Id, params?: AdapterParams): Promise<T>;
135
+ abstract _get(id: IdType, params?: ServiceParams): Promise<Result>
72
136
 
73
137
  /**
74
138
  * Create a new resource for this service, skipping any service-level hooks.
139
+ * Does not check if multiple updates are allowed and should only be used on the server.
75
140
  *
76
141
  * @param data - Data to insert into this service.
77
- * @param params - Service call parameters {@link Params}
142
+ * @param params - Service call parameters {@link ServiceParams}
78
143
  * @see {@link HookLessServiceMethods}
79
144
  * @see {@link https://docs.feathersjs.com/api/services.html#create-data-params|Feathers API Documentation: .create(data, params)}
80
145
  */
81
- _create (data: D | D[], params?: AdapterParams): Promise<T | T[]>;
146
+ abstract _create(data: Data, params?: ServiceParams): Promise<Result>
147
+ abstract _create(data: Data[], params?: ServiceParams): Promise<Result[]>
148
+ abstract _create(data: Data | Data[], params?: ServiceParams): Promise<Result | Result[]>
82
149
 
83
150
  /**
84
- * Replace any resources matching the given ID with the given data, skipping any service-level hooks.
151
+ * Completely replace the resource identified by id, skipping any service-level hooks.
152
+ * Does not sanitize the query and should only be used on the server.
85
153
  *
86
154
  * @param id - ID of the resource to be updated
87
155
  * @param data - Data to be put in place of the current resource.
88
- * @param params - Service call parameters {@link Params}
156
+ * @param params - Service call parameters {@link ServiceParams}
89
157
  * @see {@link HookLessServiceMethods}
90
158
  * @see {@link https://docs.feathersjs.com/api/services.html#update-id-data-params|Feathers API Documentation: .update(id, data, params)}
91
159
  */
92
- _update (id: Id, data: D, params?: AdapterParams): Promise<T>;
160
+ abstract _update(id: IdType, data: Data, params?: ServiceParams): Promise<Result>
93
161
 
94
162
  /**
95
163
  * Merge any resources matching the given ID with the given data, skipping any service-level hooks.
164
+ * Does not sanitize the query and should only be used on the server.
96
165
  *
97
166
  * @param id - ID of the resource to be patched
98
167
  * @param data - Data to merge with the current resource.
99
- * @param params - Service call parameters {@link Params}
168
+ * @param params - Service call parameters {@link ServiceParams}
100
169
  * @see {@link HookLessServiceMethods}
101
170
  * @see {@link https://docs.feathersjs.com/api/services.html#patch-id-data-params|Feathers API Documentation: .patch(id, data, params)}
102
171
  */
103
- _patch (id: NullableId, data: D, params?: AdapterParams): Promise<T | T[]>;
172
+ abstract _patch(id: null, data: PatchData, params?: ServiceParams): Promise<Result[]>
173
+ abstract _patch(id: IdType, data: PatchData, params?: ServiceParams): Promise<Result>
174
+ abstract _patch(id: IdType | null, data: PatchData, params?: ServiceParams): Promise<Result | Result[]>
104
175
 
105
176
  /**
106
177
  * Remove resources matching the given ID from the this service, skipping any service-level hooks.
178
+ * Does not sanitize query and should only be used on the server.
107
179
  *
108
180
  * @param id - ID of the resource to be removed
109
- * @param params - Service call parameters {@link Params}
181
+ * @param params - Service call parameters {@link ServiceParams}
110
182
  * @see {@link HookLessServiceMethods}
111
183
  * @see {@link https://docs.feathersjs.com/api/services.html#remove-id-params|Feathers API Documentation: .remove(id, params)}
112
184
  */
113
- _remove (id: NullableId, params?: AdapterParams): Promise<T | T[]>;
114
- }
115
-
116
- export class AdapterService<
117
- T = any,
118
- D = Partial<T>,
119
- O extends Partial<ServiceOptions> = Partial<ServiceOptions>
120
- > implements ServiceMethods<T|Paginated<T>, D> {
121
- options: ServiceOptions & O;
122
-
123
- constructor (options: O) {
124
- this.options = Object.assign({
125
- id: 'id',
126
- events: [],
127
- paginate: {},
128
- multi: false,
129
- filters: [],
130
- allow: []
131
- }, options);
132
- }
133
-
134
- get id () {
135
- return this.options.id;
136
- }
137
-
138
- get events () {
139
- return this.options.events;
140
- }
141
-
142
- filterQuery (params: AdapterParams = {}, opts: any = {}) {
143
- const paginate = typeof params.paginate !== 'undefined'
144
- ? params.paginate
145
- : this.getOptions(params).paginate;
146
- const { query = {} } = params;
147
- const options = Object.assign({
148
- operators: this.options.whitelist || this.options.allow || [],
149
- filters: this.options.filters,
150
- paginate
151
- }, opts);
152
- const result = filterQuery(query, options);
153
-
154
- return Object.assign(result, { paginate });
155
- }
156
-
157
- allowsMulti (method: string, params: AdapterParams = {}) {
158
- const always = alwaysMulti[method];
159
-
160
- if (typeof always !== 'undefined') {
161
- return always;
162
- }
163
-
164
- const { multi: option } = this.getOptions(params);
165
-
166
- if (option === true || option === false) {
167
- return option;
168
- }
169
-
170
- return option.includes(method);
171
- }
172
-
173
- getOptions (params: AdapterParams): ServiceOptions & { model?: any } {
174
- return {
175
- ...this.options,
176
- ...params.adapter
177
- }
178
- }
179
-
180
- find (params?: AdapterParams): Promise<T[] | Paginated<T>> {
181
- return callMethod(this, '_find', params);
182
- }
183
-
184
- get (id: Id, params?: AdapterParams): Promise<T> {
185
- return callMethod(this, '_get', id, params);
186
- }
187
-
188
- create (data: Partial<T>, params?: AdapterParams): Promise<T>;
189
- create (data: Partial<T>[], params?: AdapterParams): Promise<T[]>;
190
- create (data: Partial<T> | Partial<T>[], params?: AdapterParams): Promise<T | T[]> {
191
- if (Array.isArray(data) && !this.allowsMulti('create', params)) {
192
- return Promise.reject(new MethodNotAllowed('Can not create multiple entries'));
193
- }
194
-
195
- return callMethod(this, '_create', data, params);
196
- }
197
-
198
- update (id: Id, data: D, params?: AdapterParams): Promise<T> {
199
- if (id === null || Array.isArray(data)) {
200
- return Promise.reject(new BadRequest(
201
- 'You can not replace multiple instances. Did you mean \'patch\'?'
202
- ));
203
- }
204
-
205
- return callMethod(this, '_update', id, data, params);
206
- }
207
-
208
- patch (id: Id, data: Partial<T>, params?: AdapterParams): Promise<T>;
209
- patch (id: null, data: Partial<T>, params?: AdapterParams): Promise<T[]>;
210
- patch (id: NullableId, data: Partial<T>, params?: AdapterParams): Promise<T | T[]>;
211
- patch (id: NullableId, data: Partial<T>, params?: AdapterParams): Promise<T | T[]> {
212
- if (id === null && !this.allowsMulti('patch', params)) {
213
- return Promise.reject(new MethodNotAllowed('Can not patch multiple entries'));
214
- }
215
-
216
- return callMethod(this, '_patch', id, data, params);
217
- }
218
-
219
- remove (id: Id, params?: AdapterParams): Promise<T>;
220
- remove (id: null, params?: AdapterParams): Promise<T[]>;
221
- remove (id: NullableId, params?: AdapterParams): Promise<T | T[]>;
222
- remove (id: NullableId, params?: AdapterParams): Promise<T | T[]> {
223
- if (id === null && !this.allowsMulti('remove', params)) {
224
- return Promise.reject(new MethodNotAllowed('Can not remove multiple entries'));
225
- }
226
-
227
- return callMethod(this, '_remove', id, params);
228
- }
229
-
230
- async setup () {}
185
+ abstract _remove(id: null, params?: ServiceParams): Promise<Result[]>
186
+ abstract _remove(id: IdType, params?: ServiceParams): Promise<Result>
187
+ abstract _remove(id: IdType | null, params?: ServiceParams): Promise<Result | Result[]>
231
188
  }