Skip to content

Commit

Permalink
feat: add default & default-by-element option for filter(s) parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
tada5hi committed Oct 15, 2022
1 parent 0029731 commit c2e552d
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 70 deletions.
155 changes: 96 additions & 59 deletions src/parameter/filters/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import { determineFilterOperatorLabelsByValue } from './utils';

// --------------------------------------------------

// --------------------------------------------------

function buildOptions(options?: FiltersParseOptions) : FiltersParseOptions {
options ??= {};

Expand All @@ -36,6 +34,88 @@ function buildOptions(options?: FiltersParseOptions) : FiltersParseOptions {
return options;
}

function transformFiltersParseOutputElement(element: FiltersParseOutputElement) : FiltersParseOutputElement {
if (
hasOwnProperty(element, 'alias') &&
(
typeof element.alias === 'undefined' ||
element.alias === null
)
) {
delete element.alias;
}

if (typeof element.value === 'string') {
const { value, operators } = determineFilterOperatorLabelsByValue(element.value);
if (operators.length > 0) {
element.value = value;
element.operator = {};

for (let i = 0; i < operators.length; i++) {
element.operator[operators[i]] = true;
}
}
}

// todo: cast unknown/unknown[] to number ( Number(value) <- isNan? ) or boolean ('false', 'FALSE', 'true', ... )

return element;
}

function transformFiltersParseOutput(output: FiltersParseOutput) {
for (let i = 0; i < output.length; i++) {
output[i] = transformFiltersParseOutputElement(output[i]);
}

return output;
}

function buildDefaultFiltersParseOutput(
options: FiltersParseOptions,
input?: Record<string, FiltersParseOutputElement>,
) : FiltersParseOutput {
const inputKeys = Object.keys(input || {});

if (
!options.defaultByElement &&
inputKeys.length > 0
) {
return Object.values(input);
}

if (options.default) {
const keys = Object.keys(options.default);

const output : FiltersParseOutput = [];

for (let i = 0; i < keys.length; i++) {
const fieldDetails = getFieldDetails(keys[i], options.defaultAlias);

if (
options.defaultByElement &&
inputKeys.length > 0
) {
const fieldWithAlias = buildFieldWithAlias(fieldDetails);
if (hasOwnProperty(input, fieldWithAlias)) {
continue;
}
}

if (options.defaultByElement || inputKeys.length === 0) {
output.push(transformFiltersParseOutputElement({
...(fieldDetails.alias ? { alias: fieldDetails.alias } : {}),
key: fieldDetails.name,
value: options.default[keys[i]],
}));
}
}

return input ? [...Object.values(input), ...output] : output;
}

return input ? Object.values(input) : [];
}

export function parseQueryFilters(
data: unknown,
options?: FiltersParseOptions,
Expand All @@ -50,27 +130,26 @@ export function parseQueryFilters(
return [];
}

// Object.prototype.toString.call(data)
/* istanbul ignore next */
if (typeof data !== 'object') {
return [];
if (typeof data !== 'object' || data === null) {
return transformFiltersParseOutput(
buildDefaultFiltersParseOutput(options),
);
}

const { length } = Object.keys(data);
if (length === 0) {
return [];
return transformFiltersParseOutput(
buildDefaultFiltersParseOutput(options),
);
}

options = buildOptions(options);

const temp : Record<string, {
key: string,
alias?: string,
value: string | boolean | number | null
}> = {};
const temp : Record<string, FiltersParseOutputElement> = {};

// transform to appreciate data format & validate input
let keys = Object.keys(data);
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
/* istanbul ignore next */
if (!hasOwnProperty(data, keys[i])) {
Expand Down Expand Up @@ -106,7 +185,7 @@ export function parseQueryFilters(

keys[i] = getNameByAliasMapping(keys[i], options.aliasMapping);

const fieldDetails : FieldDetails = getFieldDetails(keys[i]);
const fieldDetails : FieldDetails = getFieldDetails(keys[i], options.defaultAlias);
if (!isAllowedByRelations(fieldDetails, options.relations, options.defaultAlias)) {
// eslint-disable-next-line no-continue
continue;
Expand All @@ -122,56 +201,14 @@ export function parseQueryFilters(
continue;
}

let alias : string | undefined;

if (
typeof fieldDetails.path === 'undefined' &&
typeof fieldDetails.alias === 'undefined'
) {
alias = options.defaultAlias ?
options.defaultAlias :
undefined;
} else {
alias = fieldDetails.alias;
}

temp[keyWithAlias] = {
...(fieldDetails.alias ? { alias: fieldDetails.alias } : {}),
key: fieldDetails.name,
...(alias ? { alias } : {}),
value: value as string | boolean | number,
};
}

const items : FiltersParseOutput = [];

/* istanbul ignore next */
keys = Object.keys(temp);
for (let i = 0; i < keys.length; i++) {
/* istanbul ignore next */
if (!Object.prototype.hasOwnProperty.call(temp, keys[i])) {
continue;
}

const filter : FiltersParseOutputElement = {
...(temp[keys[i]].alias ? { alias: temp[keys[i]].alias } : {}),
key: temp[keys[i]].key,
value: temp[keys[i]].value,
};

if (typeof filter.value === 'string') {
const { value, operators } = determineFilterOperatorLabelsByValue(filter.value);
if (operators.length > 0) {
filter.value = value;
filter.operator = {};

for (let i = 0; i < operators.length; i++) {
filter.operator[operators[i]] = true;
}
}
}

items.push(filter);
}

return items;
return transformFiltersParseOutput(
buildDefaultFiltersParseOutput(options, temp),
);
}
8 changes: 5 additions & 3 deletions src/parameter/filters/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ export type FilterOperatorConfig<V extends string | number | boolean | null | un
};

type FilterValue<V> = V extends string | number | boolean ? (V | V[]) : never;
type FilterValueOperator<V extends string | number | boolean> = `!${V}` | `!~${V}` | `~${V}` | `<${V}` | `<=${V}` | `>${V}` | `>=${V}`;
type FilterValueWithOperator<V> = V extends string | number | boolean ?
(FilterValue<V> | FilterValueOperator<V> | Array<FilterValueOperator<V>>) :
never;

type FilterValueOperator<V extends string | number | boolean> = `!${V}` | `!~${V}` | `~${V}` | `<${V}` | `<=${V}` | `>${V}` | `>=${V}`;

export type FiltersBuildInputValue<T> = T extends OnlyScalar<T> ?
T | FilterValueWithOperator<T> | FilterOperatorConfig<T> :
T extends OnlyObject<T> ? FiltersBuildInput<Flatten<T>> : never;
Expand All @@ -39,7 +38,10 @@ export type FiltersBuildInput<T> = {
// Parse
// -----------------------------------------------------------

export type FiltersParseOptions = ParseOptionsBase<Parameter.FILTERS>;
export type FiltersParseOptions = ParseOptionsBase<Parameter.FILTERS> & {
default?: Record<string, FilterValueWithOperator<any>>,
defaultByElement?: boolean
};

export type FiltersParseOutputElement = ParseOutputElementBase<Parameter.FILTERS, FilterValue<string | number | boolean | null>> & {
operator?: {
Expand Down
17 changes: 9 additions & 8 deletions src/parameter/sort/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import {

// --------------------------------------------------

// --------------------------------------------------

function isMultiDimensionalArray(arr: unknown) : arr is unknown[][] {
if (!Array.isArray(arr)) {
return false;
Expand All @@ -28,14 +26,14 @@ function isMultiDimensionalArray(arr: unknown) : arr is unknown[][] {
return arr.length > 0 && Array.isArray(arr[0]);
}

function applyDefault(options: SortParseOptions) {
function buildDefaultSortParseOutput(options: SortParseOptions) : SortParseOutput {
if (options.default) {
const keys = Object.keys(options.default);

const output : SortParseOutputElement[] = [];
const output : SortParseOutput = [];

for (let i = 0; i < keys.length; i++) {
const fieldDetails = getFieldDetails(keys[i]);
const fieldDetails = getFieldDetails(keys[i], options.defaultAlias);

output.push({
key: fieldDetails.name,
Expand Down Expand Up @@ -79,7 +77,7 @@ export function parseQuerySort(
prototype !== '[object Array]' &&
prototype !== '[object Object]'
) {
return applyDefault(options);
return buildDefaultSortParseOutput(options);
}

let parts : string[] = [];
Expand All @@ -92,7 +90,10 @@ export function parseQuerySort(
parts = data.filter((item) => typeof item === 'string');
}

if (typeof data === 'object') {
if (
typeof data === 'object' &&
data !== null
) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
/* istanbul ignore next */
Expand Down Expand Up @@ -148,7 +149,7 @@ export function parseQuerySort(
}

if (!matched) {
return applyDefault(options);
return buildDefaultSortParseOutput(options);
}

if (isMultiDimensionalArray(options.allowed)) {
Expand Down
92 changes: 92 additions & 0 deletions test/unit/filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,98 @@ describe('src/filter/index.ts', () => {
] as FiltersParseOutput)
})

it('should transform filters with default', () => {
const options : FiltersParseOptions= {
allowed: ['id', 'age'],
defaultAlias: 'user',
default: {
age: '<18'
}
};

let data = parseQueryFilters({ id: 1 }, options);
expect(data).toEqual([
{
key: 'id',
value: 1,
alias: 'user'
}
] as FiltersParseOutput);

data = parseQueryFilters({ name: 'Peter' }, options);
expect(data).toEqual([
{
key: 'age',
value: '18',
alias: 'user',
operator: {
lessThan: true
}
}
] as FiltersParseOutput);

data = parseQueryFilters({name: 'Peter'}, options);
expect(data).toEqual([
{
key: 'age',
value: '18',
alias: 'user',
operator: {
lessThan: true
}
}
] as FiltersParseOutput)
});

it('should transform filters with default by element', () => {
const options: FiltersParseOptions = {
allowed: ['id', 'age'],
default: {
id: 18,
age: '<18',
},
defaultByElement: true
};

let data = parseQueryFilters([], options);
expect(data).toEqual([
{
key: 'id',
value: 18,
},
{
key: 'age',
value: '18',
operator: {
lessThan: true
}
}
] as FiltersParseOutput);

data = parseQueryFilters({id: 5}, options);
expect(data).toEqual([
{
key: 'id',
value: 5,
},
{
key: 'age',
value: '18',
operator: {
lessThan: true
}
}
] as FiltersParseOutput);

data = parseQueryFilters({id: 5}, {...options, defaultByElement: false});
expect(data).toEqual([
{
key: 'id',
value: 5,
}
] as FiltersParseOutput);
});

it('should transform filters with different operators', () => {
// equal operator
let data = parseQueryFilters({ id: '1' }, { allowed: ['id'] });
Expand Down

0 comments on commit c2e552d

Please sign in to comment.