diff --git a/src/orchestrators/base.js b/src/orchestrators/base.js index 56a2c52..60fc21a 100644 --- a/src/orchestrators/base.js +++ b/src/orchestrators/base.js @@ -62,12 +62,11 @@ class BaseOrchestrator { } apply(data, key = null) { - const querierMethod = key && `${this.queryKey}:${key}` const args = [this.querier.builder, data] this.querier.builder = - querierMethod && is.fn(this.querier[querierMethod]) - ? this.querier[querierMethod](...args) + key && is.fn(this.querier[key]) + ? this.querier[key](...args) : this.querier.adapter[this.queryKey](...args) return this.querier.builder diff --git a/src/orchestrators/filterer.js b/src/orchestrators/filterer.js index 0e7a2ed..36e934b 100644 --- a/src/orchestrators/filterer.js +++ b/src/orchestrators/filterer.js @@ -36,7 +36,7 @@ class Filterer extends BaseOrchestrator { return filters.reduce( (accumulator, [key, filter]) => ({ ...accumulator, - [`${this.queryKey}:${key}`]: filter.value, + [key]: filter.value, }), {} ) @@ -50,10 +50,7 @@ class Filterer extends BaseOrchestrator { if (!this._validate) { this._validate = this.parser.validate() && - this.querier.adapter.validator.validateFilters( - this.parse(), - this.queryKey - ) && + this.querier.adapter.validator.validateFilters(this.parse()) && this.querier.validator.validate(this.parseFlat()) } @@ -69,10 +66,11 @@ class Filterer extends BaseOrchestrator { return this.querier } - const keys = this.schema.keys() + let key let filter - for (const key of keys) { + for (const filterSchema of this.schema.values()) { + key = this.parser.buildKey(filterSchema) filter = filters.get(key) if (filter) { diff --git a/src/orchestrators/pager.js b/src/orchestrators/pager.js index 7fe256f..a385e80 100644 --- a/src/orchestrators/pager.js +++ b/src/orchestrators/pager.js @@ -23,15 +23,17 @@ class Pager extends BaseOrchestrator { ) } - parseFlat() { + parseFlat(includeQueryKey = true) { if (!this.isEnabled) { return {} } - return Object.entries(this.parse()).reduce( - (accumulator, [field, value]) => ({ + const page = Array.from(this.parse().values()) + + return page.reduce( + (accumulator, pageField) => ({ ...accumulator, - [`${this.queryKey}:${field}`]: value, + [this.parser.buildKey(pageField, includeQueryKey)]: pageField.value, }), {} ) @@ -45,10 +47,7 @@ class Pager extends BaseOrchestrator { if (!this._validate) { this._validate = this.parser.validate() && - this.querier.adapter.validator.validatePage( - this.parse(), - this.queryKey - ) && + this.querier.adapter.validator.validatePage(this.parse()) && this.querier.validator.validate(this.parseFlat()) } @@ -61,7 +60,7 @@ class Pager extends BaseOrchestrator { const page = this.parse() if (page) { - this.apply(page) + this.apply(this.parseFlat(false)) } return this.querier diff --git a/src/orchestrators/sorter.js b/src/orchestrators/sorter.js index 57e59b6..be60a84 100644 --- a/src/orchestrators/sorter.js +++ b/src/orchestrators/sorter.js @@ -33,7 +33,7 @@ class Sorter extends BaseOrchestrator { return sorts.reduce( (accumulator, [key, sort]) => ({ ...accumulator, - [`${this.queryKey}:${key}`]: sort.order, + [key]: sort.order, }), {} ) @@ -47,10 +47,7 @@ class Sorter extends BaseOrchestrator { if (!this._validate) { this._validate = this.parser.validate() && - this.querier.adapter.validator.validateSorts( - this.parse(), - this.queryKey - ) && + this.querier.adapter.validator.validateSorts(this.parse()) && this.querier.validator.validate(this.parseFlat()) } diff --git a/src/parsers/base.js b/src/parsers/base.js index 561e577..4800744 100644 --- a/src/parsers/base.js +++ b/src/parsers/base.js @@ -16,6 +16,10 @@ class BaseParser { this._validate = null } + buildKey(/* parsed */) { + throw new NotImplementedError() + } + parse() { throw new NotImplementedError() } diff --git a/src/parsers/filter.js b/src/parsers/filter.js index 1644ed4..d12de78 100644 --- a/src/parsers/filter.js +++ b/src/parsers/filter.js @@ -11,8 +11,8 @@ class FilterParser extends BaseParser { } } - static buildKey(filter) { - return `${filter.field}[${filter.operator}]` + buildKey({ field, operator }) { + return `${this.queryKey}:${field}[${operator}]` } defineValidation(schema) { @@ -79,9 +79,7 @@ class FilterParser extends BaseParser { } } - return new Map( - filters.map(filter => [this.constructor.buildKey(filter), filter]) - ) + return new Map(filters.map(filter => [this.buildKey(filter), filter])) } } diff --git a/src/parsers/page.js b/src/parsers/page.js index 6e2873c..c58b666 100644 --- a/src/parsers/page.js +++ b/src/parsers/page.js @@ -10,6 +10,16 @@ class PageParser extends BaseParser { } } + buildKey({ field }, includeQueryKey = true) { + let key = field + + if (includeQueryKey) { + key = `${this.queryKey}:${key}` + } + + return key + } + defineValidation(schema) { return schema.alternatives().try([ schema @@ -58,7 +68,12 @@ class PageParser extends BaseParser { page.offset = (page.number - 1) * page.size - return page + return new Map( + Object.entries(page).map(([field, value]) => [ + this.buildKey({ field }), + { field, value }, + ]) + ) } } diff --git a/src/parsers/sort.js b/src/parsers/sort.js index 791bc83..8a9c922 100644 --- a/src/parsers/sort.js +++ b/src/parsers/sort.js @@ -10,8 +10,8 @@ class SortParser extends BaseParser { } } - static buildKey(sort) { - return sort.field + buildKey({ field }) { + return `${this.queryKey}:${field}` } defineValidation(schema) { @@ -72,7 +72,7 @@ class SortParser extends BaseParser { sorts.push(...this.parseObject(this.query)) } - return new Map(sorts.map(sort => [this.constructor.buildKey(sort), sort])) + return new Map(sorts.map(sort => [this.buildKey(sort), sort])) } } diff --git a/src/validators/adapter.js b/src/validators/adapter.js index e74281e..32401d6 100644 --- a/src/validators/adapter.js +++ b/src/validators/adapter.js @@ -25,23 +25,19 @@ class AdapterValidator { return true } - validateFilters(filters, queryKey) { + validateFilters(filters) { if (!this.schema) { return true } for (const [key, filter] of filters) { - this.validateValue( - `filter:${filter.operator}`, - `${queryKey}:${key}`, - filter.value - ) + this.validateValue(`filter:${filter.operator}`, key, filter.value) } return true } - validateSorts(sorts, queryKey) { + validateSorts(sorts) { const schemaKey = 'sort' if (!this.schema || !this.schema[schemaKey]) { @@ -49,21 +45,19 @@ class AdapterValidator { } for (const [key, sort] of sorts) { - this.validateValue(schemaKey, `${queryKey}:${key}`, sort.order) + this.validateValue(schemaKey, key, sort.order) } return true } - validatePage(page, queryKey) { + validatePage(page) { if (!this.schema) { return true } - const entries = Object.entries(page) - - for (const [field, value] of entries) { - this.validateValue(`page:${field}`, `${queryKey}:${field}`, value) + for (const [key, pageField] of page) { + this.validateValue(`page:${pageField.field}`, key, pageField.value) } return true diff --git a/test/src/orchestrators/base.js b/test/src/orchestrators/base.js index 863c663..763acef 100644 --- a/test/src/orchestrators/base.js +++ b/test/src/orchestrators/base.js @@ -162,7 +162,7 @@ describe('apply', () => { jest.spyOn(orchestrator, 'queryKey', 'get').mockReturnValue('sort') querier['sort:test'] = jest.fn(builder => builder) - expect(orchestrator.apply(data, 'test')).toBe(querier.builder) + expect(orchestrator.apply(data, 'sort:test')).toBe(querier.builder) expect(querier['sort:test']).toHaveBeenCalledWith(querier.builder, data) }) diff --git a/test/src/orchestrators/filterer.js b/test/src/orchestrators/filterer.js index 19358b2..8ae3a67 100644 --- a/test/src/orchestrators/filterer.js +++ b/test/src/orchestrators/filterer.js @@ -71,7 +71,7 @@ describe('parse', () => { ) ) - expect(filterer.parse().has('test[=]')).toBe(true) + expect(filterer.parse().has('filter:test[=]')).toBe(true) }) test('calls/uses `querier.defaultFilter` if no query', () => { @@ -86,7 +86,7 @@ describe('parse', () => { expect(filterer.query).toBeFalsy() expect(defaultFilter).toHaveBeenCalled() - expect(parsed.has('test[=]')).toBe(true) + expect(parsed.has('filter:test[=]')).toBe(true) defaultFilter.mockRestore() }) @@ -155,7 +155,7 @@ describe('run', () => { operator: '=', value: 123, }, - 'test[=]' + 'filter:test[=]' ) expect(filterer.apply).toHaveBeenNthCalledWith( @@ -165,7 +165,7 @@ describe('run', () => { operator: '!=', value: 456, }, - 'testing[!=]' + 'filter:testing[!=]' ) }) diff --git a/test/src/orchestrators/pager.js b/test/src/orchestrators/pager.js index 14d3fe3..b599ff8 100644 --- a/test/src/orchestrators/pager.js +++ b/test/src/orchestrators/pager.js @@ -53,6 +53,23 @@ describe('parseFlat', () => { }) }) + test('returns object keys without the query key, if specified', () => { + const pager = new Pager( + new TestQuerier( + { + page: 2, + }, + knex('test') + ) + ) + + expect(pager.parseFlat(false)).toEqual({ + size: 20, + number: 2, + offset: 20, + }) + }) + test('returns empty object if pagination is disabled', () => { const pager = new Pager(new TestQuerier({}, knex('true'))) @@ -73,7 +90,7 @@ describe('parse', () => { ) ) - expect(pager.parse().number).toBe(2) + expect(pager.parse().get('page:number').value).toBe(2) }) test('calls/uses `querier.defaultPage` if no query', () => { @@ -88,7 +105,7 @@ describe('parse', () => { expect(pager.query).toBeFalsy() expect(defaultPage).toHaveBeenCalled() - expect(parsed.number).toBe(2) + expect(parsed.get('page:number').value).toBe(2) defaultPage.mockRestore() }) diff --git a/test/src/orchestrators/sorter.js b/test/src/orchestrators/sorter.js index e290765..c6db706 100644 --- a/test/src/orchestrators/sorter.js +++ b/test/src/orchestrators/sorter.js @@ -71,7 +71,7 @@ describe('parse', () => { ) ) - expect(sorter.parse().has('test')).toBe(true) + expect(sorter.parse().has('sort:test')).toBe(true) }) test('calls/uses `querier.defaultSort` if no query', () => { @@ -86,7 +86,7 @@ describe('parse', () => { expect(sorter.query).toBeFalsy() expect(defaultSort).toHaveBeenCalled() - expect(parsed.has('test')).toBe(true) + expect(parsed.has('sort:test')).toBe(true) defaultSort.mockRestore() }) @@ -147,7 +147,7 @@ describe('run', () => { field: 'testing', order: 'asc', }, - 'testing' + 'sort:testing' ) expect(sorter.apply).toHaveBeenNthCalledWith( @@ -156,7 +156,7 @@ describe('run', () => { field: 'test', order: 'asc', }, - 'test' + 'sort:test' ) }) diff --git a/test/src/parsers/base.js b/test/src/parsers/base.js index ac9d28e..5a13421 100644 --- a/test/src/parsers/base.js +++ b/test/src/parsers/base.js @@ -33,6 +33,14 @@ describe('constructor', () => { }) }) +describe('buildKey', () => { + test('throws `NotImplementedError` when not extended', () => { + const parser = new BaseParser('test', {}, new Schema()) + + expect(() => parser.buildKey()).toThrow(NotImplementedError) + }) +}) + describe('parse', () => { test('throws `NotImplementedError` when not extended', () => { const parser = new BaseParser('test', {}, new Schema()) diff --git a/test/src/parsers/filter.js b/test/src/parsers/filter.js index 8dfde5f..f2884a4 100644 --- a/test/src/parsers/filter.js +++ b/test/src/parsers/filter.js @@ -18,13 +18,13 @@ describe('DEFAULTS', () => { describe('buildKey', () => { test('builds/returns a string to use as a key', () => { - const key = FilterParser.buildKey({ + const parser = new FilterParser('filter', {}, new Schema()) + const key = parser.buildKey({ field: 'test', operator: '=', - value: 123, }) - expect(key).toBe('test[=]') + expect(key).toBe('filter:test[=]') }) }) @@ -123,7 +123,7 @@ describe('parse', () => { { operator: '=' } ) - expect(parser.parse().get('test[=]')).toEqual({ + expect(parser.parse().get('filter:test[=]')).toEqual({ field: 'test', operator: '=', value: 123, @@ -137,7 +137,7 @@ describe('parse', () => { new Schema().filter('test', '!=') ) - expect(parser.parse().get('test[!=]')).toEqual({ + expect(parser.parse().get('filter:test[!=]')).toEqual({ field: 'test', operator: '!=', value: 456, @@ -156,13 +156,13 @@ describe('parse', () => { new Schema().filter('test', '=').filter('test', '!=') ) - expect(parser.parse().get('test[=]')).toEqual({ + expect(parser.parse().get('filter:test[=]')).toEqual({ field: 'test', operator: '=', value: 123, }) - expect(parser.parse().get('test[!=]')).toEqual({ + expect(parser.parse().get('filter:test[!=]')).toEqual({ field: 'test', operator: '!=', value: 456, @@ -179,13 +179,13 @@ describe('parse', () => { new Schema().filter('test1', '=').filter('test2', '!=') ) - expect(parser.parse().get('test1[=]')).toEqual({ + expect(parser.parse().get('filter:test1[=]')).toEqual({ field: 'test1', operator: '=', value: 123, }) - expect(parser.parse().get('test2[!=]')).toEqual({ + expect(parser.parse().get('filter:test2[!=]')).toEqual({ field: 'test2', operator: '!=', value: 456, diff --git a/test/src/parsers/page.js b/test/src/parsers/page.js index d11db15..9a4e47a 100644 --- a/test/src/parsers/page.js +++ b/test/src/parsers/page.js @@ -12,6 +12,29 @@ describe('DEFAULTS', () => { }) }) +describe('buildKey', () => { + test('builds/returns a string to use as a key', () => { + const parser = new PageParser('page', {}, new Schema()) + const key = parser.buildKey({ + field: 'size', + }) + + expect(key).toBe('page:size') + }) + + test('builds/returns a key without the query key, if specified', () => { + const parser = new PageParser('page', {}, new Schema()) + const key = parser.buildKey( + { + field: 'size', + }, + false + ) + + expect(key).toBe('size') + }) +}) + describe('validation', () => { describe('`page=number`', () => { test('throws if the number is not an integer', () => { @@ -71,41 +94,81 @@ describe('validation', () => { describe('parse', () => { test('`page=number` with a string number', () => { const parser = new PageParser('page', '2', new Schema()) + const parsed = parser.parse() - expect(parser.parse()).toEqual({ - size: 20, - number: '2', - offset: 20, + expect(parsed.get('page:size')).toEqual({ + field: 'size', + value: 20, + }) + + expect(parsed.get('page:number')).toEqual({ + field: 'number', + value: '2', + }) + + expect(parsed.get('page:offset')).toEqual({ + field: 'offset', + value: 20, }) }) test('`page=number` with a number number', () => { const parser = new PageParser('page', 2, new Schema()) + const parsed = parser.parse() + + expect(parsed.get('page:size')).toEqual({ + field: 'size', + value: 20, + }) + + expect(parsed.get('page:number')).toEqual({ + field: 'number', + value: 2, + }) - expect(parser.parse()).toEqual({ - size: 20, - number: 2, - offset: 20, + expect(parsed.get('page:offset')).toEqual({ + field: 'offset', + value: 20, }) }) test('`page[number]=value`', () => { const parser = new PageParser('page', { number: '2' }, new Schema()) + const parsed = parser.parse() - expect(parser.parse()).toEqual({ - size: 20, - number: '2', - offset: 20, + expect(parsed.get('page:size')).toEqual({ + field: 'size', + value: 20, + }) + + expect(parsed.get('page:number')).toEqual({ + field: 'number', + value: '2', + }) + + expect(parsed.get('page:offset')).toEqual({ + field: 'offset', + value: 20, }) }) test('`page[size]=value`', () => { const parser = new PageParser('page', { size: '10' }, new Schema()) + const parsed = parser.parse() + + expect(parsed.get('page:size')).toEqual({ + field: 'size', + value: '10', + }) + + expect(parsed.get('page:number')).toEqual({ + field: 'number', + value: 1, + }) - expect(parser.parse()).toEqual({ - size: '10', - number: 1, - offset: 0, + expect(parsed.get('page:offset')).toEqual({ + field: 'offset', + value: 0, }) }) @@ -115,20 +178,41 @@ describe('parse', () => { { number: '2', size: '10' }, new Schema().page() ) + const parsed = parser.parse() - expect(parser.parse()).toEqual({ - size: '10', - number: '2', - offset: 10, + expect(parsed.get('page:size')).toEqual({ + field: 'size', + value: '10', + }) + + expect(parsed.get('page:number')).toEqual({ + field: 'number', + value: '2', + }) + + expect(parsed.get('page:offset')).toEqual({ + field: 'offset', + value: 10, }) }) test('uses the defaults if no query', () => { const parser = new PageParser('page', undefined, new Schema()) + const parsed = parser.parse() + + expect(parsed.get('page:size')).toEqual({ + field: 'size', + value: PageParser.DEFAULTS.size, + }) + + expect(parsed.get('page:number')).toEqual({ + field: 'number', + value: PageParser.DEFAULTS.number, + }) - expect(parser.parse()).toEqual({ - ...PageParser.DEFAULTS, - offset: 0, + expect(parsed.get('page:offset')).toEqual({ + field: 'offset', + value: 0, }) }) diff --git a/test/src/parsers/sort.js b/test/src/parsers/sort.js index ac5ada4..e351693 100644 --- a/test/src/parsers/sort.js +++ b/test/src/parsers/sort.js @@ -14,12 +14,10 @@ describe('DEFAULTS', () => { describe('buildKey', () => { test('builds/returns a string to use as a key', () => { - const key = SortParser.buildKey({ - field: 'test', - order: 'asc', - }) + const parser = new SortParser('sort', {}, new Schema()) + const key = parser.buildKey({ field: 'test' }) - expect(key).toBe('test') + expect(key).toBe('sort:test') }) }) @@ -93,7 +91,7 @@ describe('parse', () => { test('`sort=field`', () => { const parser = new SortParser('sort', 'test', new Schema().sort('test')) - expect(parser.parse().get('test')).toEqual({ + expect(parser.parse().get('sort:test')).toEqual({ field: 'test', order: 'asc', }) @@ -102,7 +100,7 @@ describe('parse', () => { test('`sort[]=field` with one field', () => { const parser = new SortParser('sort', ['test'], new Schema().sort('test')) - expect(parser.parse().get('test')).toEqual({ + expect(parser.parse().get('sort:test')).toEqual({ field: 'test', order: 'asc', }) @@ -117,12 +115,12 @@ describe('parse', () => { const parsed = parser.parse() - expect(parsed.get('test1')).toEqual({ + expect(parsed.get('sort:test1')).toEqual({ field: 'test1', order: 'asc', }) - expect(parsed.get('test2')).toEqual({ + expect(parsed.get('sort:test2')).toEqual({ field: 'test2', order: 'asc', }) @@ -135,7 +133,7 @@ describe('parse', () => { new Schema().sort('test') ) - expect(parser.parse().get('test')).toEqual({ + expect(parser.parse().get('sort:test')).toEqual({ field: 'test', order: 'desc', }) @@ -153,12 +151,12 @@ describe('parse', () => { const parsed = parser.parse() - expect(parsed.get('test1')).toEqual({ + expect(parsed.get('sort:test1')).toEqual({ field: 'test1', order: 'desc', }) - expect(parsed.get('test2')).toEqual({ + expect(parsed.get('sort:test2')).toEqual({ field: 'test2', order: 'asc', }) diff --git a/test/src/validators/adapter.js b/test/src/validators/adapter.js index 5102b62..17710b9 100644 --- a/test/src/validators/adapter.js +++ b/test/src/validators/adapter.js @@ -86,7 +86,7 @@ describe('validateFilters', () => { const validator = new AdapterValidator(() => {}) expect(validator.schema).toBeUndefined() - expect(validator.validateFilters(parser.parse(), 'filter')).toBe(true) + expect(validator.validateFilters(parser.parse())).toBe(true) }) test('returns `true` if all filters are valid', () => { @@ -105,7 +105,7 @@ describe('validateFilters', () => { 'filter:!=': schema.number(), })) - expect(validator.validateFilters(parser.parse(), 'filter')).toBe(true) + expect(validator.validateFilters(parser.parse())).toBe(true) }) test('throws `ValidationError` if a filter is invalid', () => { @@ -118,7 +118,7 @@ describe('validateFilters', () => { 'filter:=': schema.number(), })) - expect(() => validator.validateFilters(parser.parse(), 'filter')).toThrow( + expect(() => validator.validateFilters(parser.parse())).toThrow( new ValidationError('filter:test[=] must be a number') ) }) @@ -130,7 +130,7 @@ describe('validateSorts', () => { const validator = new AdapterValidator(() => {}) expect(validator.schema).toBeUndefined() - expect(validator.validateSorts(parser.parse(), 'sort')).toBe(true) + expect(validator.validateSorts(parser.parse())).toBe(true) }) test('returns `true` if no schema key is defined', () => { @@ -140,7 +140,7 @@ describe('validateSorts', () => { })) expect(validator.schema['sort']).toBeUndefined() - expect(validator.validateSorts(parser.parse(), 'sort')).toBe(true) + expect(validator.validateSorts(parser.parse())).toBe(true) }) test('returns `true` if all sorts are valid', () => { @@ -153,7 +153,7 @@ describe('validateSorts', () => { sort: schema.string().valid('asc'), })) - expect(validator.validateSorts(parser.parse(), 'sort')).toBe(true) + expect(validator.validateSorts(parser.parse())).toBe(true) }) test('throws `ValidationError` if a sort is invalid', () => { @@ -162,7 +162,7 @@ describe('validateSorts', () => { sort: schema.string().invalid('asc'), })) - expect(() => validator.validateSorts(parser.parse(), 'sort')).toThrow( + expect(() => validator.validateSorts(parser.parse())).toThrow( new ValidationError('sort:test contains an invalid value') ) }) @@ -174,7 +174,7 @@ describe('validatePage', () => { const validator = new AdapterValidator(() => {}) expect(validator.schema).toBeUndefined() - expect(validator.validatePage(parser.parse(), 'page')).toBe(true) + expect(validator.validatePage(parser.parse())).toBe(true) }) test('returns `true` if page is valid', () => { @@ -184,7 +184,7 @@ describe('validatePage', () => { 'page:number': schema.number().valid(2), })) - expect(validator.validatePage(parser.parse(), 'page')).toBe(true) + expect(validator.validatePage(parser.parse())).toBe(true) }) test('throws `ValidationError` if page is invalid', () => { @@ -193,7 +193,7 @@ describe('validatePage', () => { 'page:number': schema.number().invalid(2), })) - expect(() => validator.validatePage(parser.parse(), 'page')).toThrow( + expect(() => validator.validatePage(parser.parse())).toThrow( new ValidationError('page:number contains an invalid value') ) })