Skip to content

Commit

Permalink
Merge pull request #8 from prest/implement/query
Browse files Browse the repository at this point in the history
Implement a initial PRestQuery Entity structure
  • Loading branch information
avelino committed Nov 9, 2020
2 parents 0052bf1 + 71a71d6 commit a88bb09
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 24 deletions.
1 change: 1 addition & 0 deletions jest.config.json
Expand Up @@ -2,6 +2,7 @@
"testPathIgnorePatterns": ["<rootDir>/node_modules/"],
"testMatch": ["**/?(*.)+(spec|test).[jt]s?(x)"],
"collectCoverageFrom": ["./src/**/*.ts"],
"coverageReporters": ["text", "html", "lcov"],
"coveragePathIgnorePatterns": ["<rootDir>/src/types", "<rootDir>/src/index.ts"],
"coverageThreshold": {
"global": {
Expand Down
161 changes: 161 additions & 0 deletions src/entity/Query.ts
@@ -0,0 +1,161 @@
type GenericValue = string | number | boolean;
type GenericFilterValue = GenericValue | GenericValue[];
type SimpleFilter = '_page' | '_page_size' | '_rendered' | '_count' | '_select';
type Filters =
| SimpleFilter
| '$eq'
| '$gt'
| '$gte'
| '$lt'
| '$lte'
| '$ne'
| '$in'
| '$nin'
| '$null'
| '$notnull'
| '$true'
| '$nottrue'
| '$false'
| '$notfalse'
| '$like'
| '$ilike';
type FilterStructure = Record<string, GenericFilterValue>;
type QueryFilter = Partial<Record<Filters, FilterStructure | GenericFilterValue>>;
type RederedTypes = 'json' | 'xml';

export class Query {
private filters: QueryFilter;
private fieldsWithSimpleSerialize = ['_page'];

constructor(initialFilters: QueryFilter = {}) {
this.filters = initialFilters;
}

private serializeField(field: Filters, value: GenericFilterValue): string {
return `${field}=${value}`;
}

private serializeFieldWithCondition(field: string, cond: Filters, value: GenericFilterValue): string {
return `${field}=${cond}.${value}`;
}

getFilters(): QueryFilter {
return { ...this.filters };
}

injectSimpleFilter(filter: SimpleFilter, value: GenericFilterValue): Query {
this.filters[filter] = value;
return this;
}

inject(filter: Filters, field: string, value: GenericFilterValue): Query {
if (!this.filters[filter]) {
this.filters[filter] = {};
}

this.filters[filter][field] = value;
return this;
}

eq(field: string, value: GenericFilterValue): Query {
return this.inject('$eq', field, value);
}

gt(field: string, value: GenericFilterValue): Query {
return this.inject('$gt', field, value);
}

gte(field: string, value: GenericFilterValue): Query {
return this.inject('$gte', field, value);
}

lt(field: string, value: GenericFilterValue): Query {
return this.inject('$lt', field, value);
}

lte(field: string, value: GenericFilterValue): Query {
return this.inject('$lte', field, value);
}

ne(field: string, value: GenericFilterValue): Query {
return this.inject('$ne', field, value);
}

in(field: string, value: GenericFilterValue): Query {
return this.inject('$in', field, value);
}

nin(field: string, value: GenericFilterValue): Query {
return this.inject('$nin', field, value);
}

null(field: string, value: GenericFilterValue): Query {
return this.inject('$null', field, value);
}

notnull(field: string, value: GenericFilterValue): Query {
return this.inject('$notnull', field, value);
}

true(field: string, value: GenericFilterValue): Query {
return this.inject('$true', field, value);
}

nottrue(field: string, value: GenericFilterValue): Query {
return this.inject('$nottrue', field, value);
}

false(field: string, value: GenericFilterValue): Query {
return this.inject('$false', field, value);
}

notfalse(field: string, value: GenericFilterValue): Query {
return this.inject('$notfalse', field, value);
}

like(field: string, value: GenericFilterValue): Query {
return this.inject('$like', field, value);
}

ilike(field: string, value: GenericFilterValue): Query {
return this.inject('$ilike', field, value);
}

page(page: number): Query {
return this.injectSimpleFilter('_page', page);
}

pageSize(pageSize: number): Query {
return this.injectSimpleFilter('_page_size', pageSize);
}

count(column: string): Query {
return this.injectSimpleFilter('_count', column);
}

rendered(redered: RederedTypes): Query {
return this.injectSimpleFilter('_rendered', redered);
}

pagination(page: number, pageSize: number): Query {
return this.page(page).pageSize(pageSize);
}

serialize(): string {
const serializedString = Object.keys(this.filters)
.map((filterOp: Filters) => {
return this.fieldsWithSimpleSerialize.indexOf(filterOp) > -1
? this.serializeField(filterOp, this.filters[filterOp] as GenericFilterValue)
: Object.keys(this.filters[filterOp])
.map((key) => {
return this.serializeFieldWithCondition(key, filterOp, this.filters[filterOp][key]);
})
.join('&');
})
.join('&');

return serializedString ? `?${serializedString}` : '';
}
}

export default Query;
21 changes: 15 additions & 6 deletions src/entity/TableConnector.ts
@@ -1,4 +1,5 @@
import Request from './Request';
import Query from './Query';

const DEFAULT_COLUMN_ID = 'id';
type MultipleId = string | number;
Expand All @@ -13,20 +14,28 @@ export class TableConnector<T> extends Request {
this.idColumn = idColumn;
}

query(): Promise<T[]> {
return this.call('get', this.dbPath);
private formatQuery(entry: MultipleId | Query) {
const q = entry instanceof Query ? entry : new Query({ $eq: { [this.idColumn]: entry } });
return q.serialize();
}

query(query?: Query): Promise<T[]> {
const qs = query ? query.serialize() : '';
return this.call('get', this.dbPath + qs);
}

create(data: T): Promise<T> {
return this.call('post', this.dbPath, data);
}

update(id: MultipleId, data: Partial<T>): Promise<T> {
return this.call('patch', `${this.dbPath}?${this.idColumn}=${id}`, data);
update(entry: MultipleId | Query, data: Partial<T>): Promise<T> {
const qs = this.formatQuery(entry);
return this.call('patch', this.dbPath + qs, data);
}

delete(id: MultipleId): Promise<T> {
return this.call('delete', `${this.dbPath}?${this.idColumn}=${id}`);
delete(entry: MultipleId | Query): Promise<T> {
const qs = this.formatQuery(entry);
return this.call('delete', this.dbPath + qs);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -2,6 +2,7 @@ import Api from './entity/Api';

export { PRestDatabase, PRestSchema, PRestTable, PRestTableShowItem } from './entity/Api';
export { TableConnector } from './entity/TableConnector';
export { Query as PRestQuery } from './entity/Query';

export const PRestAPI = Api;
export default Api;
162 changes: 162 additions & 0 deletions tests/unit/entity/Query.spec.ts
@@ -0,0 +1,162 @@
import Query from '~/entity/Query';

describe('entity/Query', () => {
const field1 = 'foo';
const field2 = 'fizz';
const val1 = 'bar';
const val2 = 'fuzz';
const val3 = 1;

const filter1 = '$eq';
const filter2 = '_page';

describe('.inject/.injectSimpleFilter/.getFilters', () => {
it('should inject a filter inside Query', () => {
const q = new Query();
q.inject(filter1, field1, val1);

expect(q.getFilters()).toHaveProperty(`${filter1}.${field1}`, val1);
});

it('should inject twice a filter inside Query', () => {
const q = new Query();
q.inject(filter1, field1, val1);
q.inject(filter1, field2, val2);

expect(q.getFilters()).toHaveProperty(`${filter1}.${field1}`, val1);
expect(q.getFilters()).toHaveProperty(`${filter1}.${field2}`, val2);
});

it('should inject a simple filter', () => {
const q = new Query();
q.injectSimpleFilter(filter2, val1);
expect(q.getFilters()).toHaveProperty(filter2, val1);
});

it('should inject a simple filter and a complex one', () => {
const q = new Query();
q.injectSimpleFilter(filter2, val1);
q.inject('$eq', 'fizz', val2);

expect(q.getFilters()).toHaveProperty(filter2, val1);
expect(q.getFilters()).toHaveProperty('$eq.fizz', val2);
});
});

describe('.serialize()', () => {
it('should serialize an empty array', () => {
const q = new Query();
expect(q.serialize()).toBe('');
});

it('should serialize a single value query', () => {
const q = new Query({ [filter1]: { [field1]: val1 } });
expect(q.serialize()).toBe(`?${field1}=${filter1}.${val1}`);
});

it('should serialize a multiple value query in same filter', () => {
const q = new Query({ [filter1]: { [field1]: val1, [field2]: val2 } });
expect(q.serialize()).toBe(`?${field1}=${filter1}.${val1}&${field2}=${filter1}.${val2}`);
});

it('should serialize a multiple value query in diferent filter', () => {
const customFilter = '$gt';
const q = new Query({ [filter1]: { [field1]: val1 }, [customFilter]: { [field2]: val2 } });
expect(q.serialize()).toBe(`?${field1}=${filter1}.${val1}&${field2}=${customFilter}.${val2}`);
});

it('should serialize a single simple value', () => {
const q = new Query({ [filter2]: val3 });
expect(q.serialize()).toBe(`?${filter2}=${val3}`);
});

it('should serialize a simple value and a complex one', () => {
const q = new Query({ [filter1]: { [field1]: val1 }, [filter2]: val3 });
expect(q.serialize()).toBe(`?${field1}=${filter1}.${val1}&${filter2}=${val3}`);
});
});

describe('.pagination()', () => {
it('should execute the method with success', () => {
const q = new Query();
const val4 = 5;
jest.spyOn(q, 'page');
jest.spyOn(q, 'pageSize');

q.pagination(val3, val4);
expect(q.page).toHaveBeenCalledTimes(1);
expect(q.pageSize).toHaveBeenCalledTimes(1);
expect(q.page).toHaveBeenCalledWith(val3);
expect(q.pageSize).toHaveBeenCalledWith(val4);

jest.resetAllMocks();
jest.restoreAllMocks();
});
});

describe('Simple Filters Methods', () => {
let q;

beforeEach(() => {
q = new Query();
jest.spyOn(q, 'injectSimpleFilter');
});

afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});

[
{ method: 'page', op: '_page' },
{ method: 'pageSize', op: '_page_size' },
{ method: 'count', op: '_count' },
{ method: 'rendered', op: '_rendered' },
].forEach(({ method, op }) => {
it(`should exec .${method} with success`, () => {
q[method](val1);
expect(q.injectSimpleFilter).toHaveBeenCalledTimes(1);
expect(q.injectSimpleFilter).toHaveBeenCalledWith(op, val1);
});
});
});

describe('Complex Filters Methods', () => {
let q;

beforeEach(() => {
q = new Query();
jest.spyOn(q, 'inject');
});

afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});

[
'eq',
'gt',
'gte',
'lt',
'lte',
'ne',
'in',
'nin',
'null',
'notnull',
'true',
'nottrue',
'false',
'notfalse',
'like',
'ilike',
].forEach((method) => {
it(`should exec .${method} with success`, () => {
q[method](field1, val1);
expect(q.inject).toHaveBeenCalledTimes(1);
expect(q.inject).toHaveBeenCalledWith(`$${method}`, field1, val1);
});
});
});
});

0 comments on commit a88bb09

Please sign in to comment.