Browse Source

Add initial implementation for query

Define function for decoding query params into native query collections.
master
TheoryOfNekomata 7 months ago
parent
commit
fe1752101a
6 changed files with 287 additions and 25 deletions
  1. +14
    -0
      README.md
  2. +3
    -3
      packages/core/src/backend/data-source/common.ts
  3. +2
    -0
      packages/core/src/backend/data-source/index.ts
  4. +114
    -0
      packages/core/src/backend/data-source/queries.ts
  5. +154
    -0
      packages/core/test/features/query.test.ts
  6. +0
    -22
      pnpm-lock.yaml

+ 14
- 0
README.md View File

@@ -63,3 +63,17 @@ See [docs folder](./docs) for more details.
- JavaScript Object Notation (JSON) Patch

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

- HTTP SEARCH Method

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

- The HTTP QUERY Method

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

- URL Standard

https://url.spec.whatwg.org/#urlencoded-parsing

https://url.spec.whatwg.org/#interface-urlsearchparams

packages/core/src/backend/data-source.ts → packages/core/src/backend/data-source/common.ts View File

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

type IsCreated = boolean;

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


export interface ResourceIdConfig<IdSchema extends v.BaseSchema> {
generationStrategy: GenerationStrategy;
serialize: (id: unknown) => string;

+ 2
- 0
packages/core/src/backend/data-source/index.ts View File

@@ -0,0 +1,2 @@
export * from './common';
export * from './queries';

+ 114
- 0
packages/core/src/backend/data-source/queries.ts View File

@@ -0,0 +1,114 @@
export const DIRECTIVE_MAP = {
'=': 'equals',
'!=': 'not-equals',
'<': 'greater-than',
'>': 'less-than',
'<=': 'greater-than-equal',
'>=': 'less-than-equal',
};

export const DATA_SOURCE_QUERY_OPERATORS = Object.keys(DIRECTIVE_MAP) as (keyof typeof DIRECTIVE_MAP)[];

export type DataSourceQueryOperator = typeof DATA_SOURCE_QUERY_OPERATORS[number];

export interface DataSourceQuery {
lhs: string;
operator: DataSourceQueryOperator;
rhs: string;
}

interface ConvertToDataSourceQueryCollectionOptions {
}

const parseDirectives = (valueRaw: string) => {
let operator = '=';

let hasDirective: boolean;
let valueTest = valueRaw.toLowerCase();
let valuePredicate = valueRaw;
const fragments = valueTest.split('.');
const directiveShorthands = Object.values(DIRECTIVE_MAP).map((s) => {
// TODO how to parse?
})
do {
hasDirective = false;

if (valueTest.startsWith('.neq.')) {
operator = '!=';

valueTest = valueTest.slice('.neq.'.length);
valuePredicate = valuePredicate.slice('.neq.'.length);
hasDirective = true;
} else if (valueTest.startsWith('.gt.eq.')) {
operator = '>=';

valueTest = valueTest.slice('.gt.eq.'.length);
valuePredicate = valuePredicate.slice('.gt.eq.'.length);
hasDirective = true;
} else if (valueTest.startsWith('.gte.')) {
operator = '>=';

valueTest = valueTest.slice('.gte.'.length);
valuePredicate = valuePredicate.slice('.gte.'.length);
hasDirective = true;
} else if (valueTest.startsWith('.lt.eq.')) {
operator = '<=';

valueTest = valueTest.slice('.lt.eq.'.length);
valuePredicate = valuePredicate.slice('.lt.eq.'.length);
hasDirective = true;
} else if (valueTest.startsWith('.lte.')) {
operator = '<=';

valueTest = valueTest.slice('.lte.'.length);
valuePredicate = valuePredicate.slice('.lte.'.length);
hasDirective = true;
} else if (valueTest.startsWith('.gt.')) {
operator = '>';

valueTest = valueTest.slice('.gt.'.length);
valuePredicate = valuePredicate.slice('.gt.'.length);
hasDirective = true;
} else if (valueTest.startsWith('.lt.')) {
operator = '<';

valueTest = valueTest.slice('.lt.'.length);
valuePredicate = valuePredicate.slice('.lt.'.length);
hasDirective = true;
} else if (valueTest.startsWith('.eq.')) {
operator = '=';

valueTest = valueTest.slice('.eq.'.length);
valuePredicate = valuePredicate.slice('.eq.'.length);
hasDirective = true;
}
} while (hasDirective);

return {
operator,
rhs: valuePredicate,
};
};

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
};

+ 154
- 0
packages/core/test/features/query.test.ts View File

@@ -0,0 +1,154 @@
import {describe, expect, it} from 'vitest';
import { convertToDataSourceQueryCollection } from '../../src/backend';

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

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

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

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

it('returns a greater than expression query', () => {
const q = new URLSearchParams({
attr: '.gt.foo'
});
const collection = convertToDataSourceQueryCollection(q);
expect(collection).toEqual([
{
lhs: 'attr',
operator: '>',
rhs: '"foo"',
},
]);
});

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

it('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"',
},
]);
});

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"',
},
]);
});

it('returns an AND 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"',
},
]);
});

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',
});

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

+ 0
- 22
pnpm-lock.yaml View File

@@ -84,28 +84,6 @@ importers:
specifier: ^1.2.0
version: 1.5.0(@types/node@20.12.7)

packages/data-sources/sqlite:
dependencies:
'@modal-sh/yasumi':
specifier: '*'
version: link:../../core
devDependencies:
'@types/node':
specifier: ^20.11.0
version: 20.12.7
pridepack:
specifier: 2.6.0
version: 2.6.0(tslib@2.6.2)(typescript@5.4.5)
tslib:
specifier: ^2.6.2
version: 2.6.2
typescript:
specifier: ^5.3.3
version: 5.4.5
vitest:
specifier: ^1.2.0
version: 1.5.0(@types/node@20.12.7)

packages/examples/cms-web-api:
dependencies:
'@modal-sh/yasumi':


Loading…
Cancel
Save