diff --git a/README.md b/README.md index 36ecf1b..18bd5f5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/packages/core/src/backend/data-source.ts b/packages/core/src/backend/data-source/common.ts similarity index 89% rename from packages/core/src/backend/data-source.ts rename to packages/core/src/backend/data-source/common.ts index f9bc8b9..6489ebe 100644 --- a/packages/core/src/backend/data-source.ts +++ b/packages/core/src/backend/data-source/common.ts @@ -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; getTotalCount?(query?: Query): Promise; @@ -25,7 +26,6 @@ export interface DataSource< newId(): Promise; } - export interface ResourceIdConfig { generationStrategy: GenerationStrategy; serialize: (id: unknown) => string; diff --git a/packages/core/src/backend/data-source/index.ts b/packages/core/src/backend/data-source/index.ts new file mode 100644 index 0000000..9b1871f --- /dev/null +++ b/packages/core/src/backend/data-source/index.ts @@ -0,0 +1,2 @@ +export * from './common'; +export * from './queries'; diff --git a/packages/core/src/backend/data-source/queries.ts b/packages/core/src/backend/data-source/queries.ts new file mode 100644 index 0000000..2ae49ca --- /dev/null +++ b/packages/core/src/backend/data-source/queries.ts @@ -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 +}; diff --git a/packages/core/test/features/query.test.ts b/packages/core/test/features/query.test.ts new file mode 100644 index 0000000..7af9586 --- /dev/null +++ b/packages/core/test/features/query.test.ts @@ -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()); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f541965..167139c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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':