Ver código fonte

Simplify querying logic

Remove directives, stick to simple querying via equality.
master
TheoryOfNekomata 6 meses atrás
pai
commit
24b11f61ee
4 arquivos alterados com 299 adições e 228 exclusões
  1. +2
    -2
      README.md
  2. +2
    -2
      packages/core/src/backend/data-source/common.ts
  3. +81
    -84
      packages/core/src/backend/data-source/queries.ts
  4. +214
    -140
      packages/core/test/features/query.test.ts

+ 2
- 2
README.md Ver arquivo

@@ -64,9 +64,9 @@ See [docs folder](./docs) for more details.

https://www.rfc-editor.org/rfc/rfc6902

- HTTP SEARCH Method
- ~~HTTP SEARCH Method~~

https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-00.html
~~https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-00.html~~

- The HTTP QUERY Method



+ 2
- 2
packages/core/src/backend/data-source/common.ts Ver arquivo

@@ -1,6 +1,6 @@
import * as v from 'valibot';
import {Resource} from '../../common';
import {DataSourceQuery} from './queries';
import {DataSourceQueryExpression} from './queries';

type IsCreated = boolean;

@@ -11,7 +11,7 @@ type DeleteResult = unknown;
export interface DataSource<
ItemData extends object = object,
ID extends unknown = unknown,
Query extends DataSourceQuery = DataSourceQuery,
Query extends DataSourceQueryExpression = DataSourceQueryExpression,
> {
initialize(): Promise<unknown>;
getTotalCount?(query?: Query): Promise<TotalCount>;


+ 81
- 84
packages/core/src/backend/data-source/queries.ts Ver arquivo

@@ -11,110 +11,107 @@ export const DATA_SOURCE_QUERY_OPERATORS = Object.keys(DIRECTIVE_MAP) as (keyof

export type DataSourceQueryOperator = typeof DATA_SOURCE_QUERY_OPERATORS[number];

export interface DataSourceOperatorQuery {
export interface DataSourceQueryExpression {
lhs: string;
operator: DataSourceQueryOperator;
rhs: string;
rhs: string | number | boolean;
}

export interface DataSourceFunctionQuery {
name: string;
args: string[];
export interface DataSourceQueryOrGrouping {
type: 'or';
expressions: DataSourceQueryExpression[];
}

export type DataSourceQuery = DataSourceOperatorQuery | DataSourceFunctionQuery;
export interface DataSourceQueryAndGrouping {
type: 'and';
expressions: DataSourceQueryOrGrouping[];
}

export const COERCE_VALUES_TYPES = [
'number',
'string',
'boolean',
] as const;

export type CoerceValuesType = typeof COERCE_VALUES_TYPES[number];

type CoerceValues = Partial<Record<CoerceValuesType, string[]>>;

interface ConvertToDataSourceQueryCollectionOptions {
coerceValues: CoerceValues;
}

const parseDirectives = (valueRaw: string) => {
const fragments = valueRaw.split('.');
const normalizeRhs = (lhs: string, rhs: string, coerceValues: CoerceValues) => {
if (coerceValues?.number?.includes(lhs)) {
return Number(rhs);
}

if (!(
fragments.length > 1 && fragments[0].length === 0
)) {
return {
operator: '=',
rhs: valueRaw,
};
if (coerceValues?.boolean?.includes(lhs)) {
return rhs.trim().toLowerCase() === 'true';
}

const theOperator = fragments.slice(1, -1).reduce(
(theState, f) => {
const whichDirective = Object.entries(DIRECTIVE_MAP).find(
([, match]) => {
const remainingChars = match.split('').reduce(
(rem, matchChar) => {
if (matchChar === rem.charAt(0)) {
return rem.slice(1);
}

return rem;
return rhs?.toString() ?? '';
// this will be sent to the data source, e.g. the SQL query
// we can also make this function act as a "sanitizer"
}

export const convertToDataSourceQueryCollection = (
q: URLSearchParams,
options = {} as ConvertToDataSourceQueryCollectionOptions,
): DataSourceQueryAndGrouping => {
return Array.from(q.entries()).reduce(
(queries, [key, value]) => {
const existingOr = queries.expressions
.find((ex) => ex.expressions.some((ex2) => ex2.lhs === key)) ?? {
type: 'or',
expressions: [],
};
const existingLhs = existingOr.expressions.find((ex) => ex.lhs === key);
const rhs = normalizeRhs(key, value, options.coerceValues);

if (typeof existingLhs === 'undefined') {
return {
...queries,
expressions: [
...queries.expressions,
{
type: 'or',
expressions: [
{
lhs: key,
operator: '=',
rhs,
},
],
},
f.toLowerCase()
);

return remainingChars.length <= 0;
},
);

const selectedOperator = whichDirective?.[0] ?? theState.operator;

if (selectedOperator === '=') {
if (theState.operator === '>') {
return {
...theState,
operator: '>=',
currentToken: `${theState.currentToken}${f}`,
};
}

if (theState.operator === '<') {
return {
...theState,
operator: '<=',
currentToken: `${theState.currentToken}${f}`,
};
}
],
};
}

return {
...theState,
operator: selectedOperator,
currentToken: `${theState.currentToken}${f}`,
...queries,
expressions: queries.expressions.map((ex) => (
ex.expressions.some((ex2) => ex2.lhs !== key)
? ex
: {
...existingOr,
expressions: [
...(existingOr.expressions ?? []),
{
lhs: key,
operator: '=',
rhs,
},
],
}
)),
};
},
{
operator: undefined as (string | undefined),
currentToken: '',
},
);
type: 'and',
expressions: [],
} as DataSourceQueryAndGrouping
)

return {
operator: theOperator.operator ?? '=',
rhs: fragments.at(-1),
};
};

export const convertToDataSourceQueryCollection = (
q: URLSearchParams,
options = {} as ConvertToDataSourceQueryCollectionOptions,
): DataSourceQuery[] => {
return Array.from(q.entries())
.reduce(
(queries, [key, value]) => {
const s = parseDirectives(value);

return [
...queries,
{
lhs: key,
operator: s.operator,
rhs: JSON.stringify(s.rhs),
},
];
},
[] as DataSourceQuery[]
);
// all the queries are to be treated as "AND", as suggested by the & character to separate the query param entries
};

+ 214
- 140
packages/core/test/features/query.test.ts Ver arquivo

@@ -5,178 +5,252 @@ describe('query', () => {
it('returns a data source query collection from search params', () => {
const q = new URLSearchParams();
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toBeInstanceOf(Array);
expect(collection.expressions).toBeInstanceOf(Array);
});

it('returns an equal query', () => {
it('coerces a numeric value', () => {
const q = new URLSearchParams({
attr: 'foo'
attr: '2',
attr2: '2',
});
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual([
const collection = convertToDataSourceQueryCollection(
q,
{
lhs: 'attr',
operator: '=',
rhs: '"foo"',
coerceValues: {
number: ['attr'],
},
},
]);
});

it('returns an equal expression query', () => {
const q = new URLSearchParams({
attr: '.eq.foo'
);
expect(collection).toEqual({
type: 'and',
expressions: [
{
type: 'or',
expressions: [
{
lhs: 'attr',
operator: '=',
rhs: 2,
},
],
},
{
type: 'or',
expressions: [
{
lhs: 'attr2',
operator: '=',
rhs: '2',
},
],
},
],
});
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual([
{
lhs: 'attr',
operator: '=',
rhs: '"foo"',
},
]);
});

it('returns a not equal expression query', () => {
it('coerces a boolean value', () => {
const q = new URLSearchParams({
attr: '.neq.foo'
attr: 'true',
attr2: 'false',
attr3: 'true',
attr4: 'false',
});
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual([
const collection = convertToDataSourceQueryCollection(
q,
{
lhs: 'attr',
operator: '!=',
rhs: '"foo"',
coerceValues: {
boolean: ['attr', 'attr2'],
},
},
]);
});

it('returns a greater than expression query', () => {
const q = new URLSearchParams({
attr: '.gt.foo'
);
expect(collection).toEqual({
type: 'and',
expressions: [
{
type: 'or',
expressions: [
{
lhs: 'attr',
operator: '=',
rhs: true,
},
],
},
{
type: 'or',
expressions: [
{
lhs: 'attr2',
operator: '=',
rhs: false,
},
],
},
{
type: 'or',
expressions: [
{
lhs: 'attr3',
operator: '=',
rhs: 'true',
},
],
},
{
type: 'or',
expressions: [
{
lhs: 'attr4',
operator: '=',
rhs: 'false',
},
],
},
],
});
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual([
{
lhs: 'attr',
operator: '>',
rhs: '"foo"',
},
]);
});

it('returns a less than expression query', () => {
it('returns an equal query', () => {
const q = new URLSearchParams({
attr: '.lt.foo'
attr: 'foo'
});
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual([
{
lhs: 'attr',
operator: '<',
rhs: '"foo"',
},
]);
expect(collection).toEqual({
type: 'and',
expressions: [
{
type: 'or',
expressions: [
{
lhs: 'attr',
operator: '=',
rhs: 'foo',
},
],
},
],
});
});

it.only('returns a greater than or equal expression query', () => {
// expect(
// convertToDataSourceQueryCollection(
// new URLSearchParams({
// attr: '.gt.eq.foo'
// })
// )
// ).toEqual([
// {
// lhs: 'attr',
// operator: '>=',
// rhs: '"foo"',
// },
// ]);
expect(
convertToDataSourceQueryCollection(
new URLSearchParams({
attr: '.gte.foo'
})
)
).toEqual([
{
lhs: 'attr',
operator: '>=',
rhs: '"foo"',
},
]);

expect(
convertToDataSourceQueryCollection(
new URLSearchParams({
attr: '.neq.test'
})
)
).toEqual([
{
lhs: 'attr',
operator: '!=',
rhs: '"test"',
},
it('returns an AND operator query', () => {
const q = new URLSearchParams([
['attr', 'foo'],
['attr2', 'bar'],
]);

// expect(
// convertToDataSourceQueryCollection(
// new URLSearchParams({
// attr: '.gte.foo..test'
// })
// )
// ).toEqual([
// {
// lhs: 'attr',
// operator: '>=',
// rhs: '"foo.test"',
// },
// ]);
});

it('returns a less than or equal expression query', () => {
const q = new URLSearchParams({
attr: '.lt.eq.foo'
});
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual([
{
lhs: 'attr',
operator: '<=',
rhs: '"foo"',
},
]);
expect(collection).toEqual({
type: 'and',
expressions: [
{
type: 'or',
expressions: [
{
lhs: 'attr',
operator: '=',
rhs: 'foo',
},
],
},
{
type: 'or',
expressions: [
{
lhs: 'attr2',
operator: '=',
rhs: 'bar',
},
],
},
],
});
});

it('returns an AND operator query', () => {
it('returns an OR operator query', () => {
const q = new URLSearchParams([
['attr', 'foo'],
['attr', 'bar'],
]);
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual([
{
lhs: 'attr',
operator: '=',
rhs: '"foo"',
},
{
lhs: 'attr',
operator: '=',
rhs: '"bar"',
},
]);
expect(collection).toEqual({
type: 'and',
expressions: [
{
type: 'or',
expressions: [
{
lhs: 'attr',
operator: '=',
rhs: 'foo',
},
{
lhs: 'attr',
operator: '=',
rhs: 'bar',
}
]
},
],
});
});

it.skip('returns a query', () => {
// * is not percent-encoded
// . is not percent-encoded (used for form data such as input text dir...)
//
const q = new URLSearchParams({
'.foo': 'foo',
it('returns an query with appropriate grouping', () => {
const q = new URLSearchParams([
['attr3', 'quux'],
['attr', 'foo'],
['attr4', 'quuux'],
['attr', 'bar'],
['attr2', 'baz'],
]);
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual({
type: 'and',
expressions: [
{
type: 'or',
expressions: [
{
lhs: 'attr3',
operator: '=',
rhs: 'quux',
},
],
},
{
type: 'or',
expressions: [
{
lhs: 'attr',
operator: '=',
rhs: 'foo',
},
{
lhs: 'attr',
operator: '=',
rhs: 'bar',
}
]
},
{
type: 'or',
expressions: [
{
lhs: 'attr4',
operator: '=',
rhs: 'quuux',
},
],
},
{
type: 'or',
expressions: [
{
lhs: 'attr2',
operator: '=',
rhs: 'baz',
},
],
},
],
});

console.log(q.toString());
});
});

Carregando…
Cancelar
Salvar