Define function for decoding query params into native query collections.master
@@ -63,3 +63,17 @@ See [docs folder](./docs) for more details. | |||||
- JavaScript Object Notation (JSON) Patch | - JavaScript Object Notation (JSON) Patch | ||||
https://www.rfc-editor.org/rfc/rfc6902 | 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 |
@@ -1,5 +1,6 @@ | |||||
import * as v from 'valibot'; | import * as v from 'valibot'; | ||||
import {Resource} from '../common'; | |||||
import {Resource} from '../../common'; | |||||
import {DataSourceQuery} from './queries'; | |||||
type IsCreated = boolean; | type IsCreated = boolean; | ||||
@@ -10,7 +11,7 @@ type DeleteResult = unknown; | |||||
export interface DataSource< | export interface DataSource< | ||||
ItemData extends object = object, | ItemData extends object = object, | ||||
ID extends unknown = unknown, | ID extends unknown = unknown, | ||||
Query extends object = object, | |||||
Query extends DataSourceQuery = DataSourceQuery, | |||||
> { | > { | ||||
initialize(): Promise<unknown>; | initialize(): Promise<unknown>; | ||||
getTotalCount?(query?: Query): Promise<TotalCount>; | getTotalCount?(query?: Query): Promise<TotalCount>; | ||||
@@ -25,7 +26,6 @@ export interface DataSource< | |||||
newId(): Promise<ID>; | newId(): Promise<ID>; | ||||
} | } | ||||
export interface ResourceIdConfig<IdSchema extends v.BaseSchema> { | export interface ResourceIdConfig<IdSchema extends v.BaseSchema> { | ||||
generationStrategy: GenerationStrategy; | generationStrategy: GenerationStrategy; | ||||
serialize: (id: unknown) => string; | serialize: (id: unknown) => string; |
@@ -0,0 +1,2 @@ | |||||
export * from './common'; | |||||
export * from './queries'; |
@@ -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 | |||||
}; |
@@ -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()); | |||||
}); | |||||
}); |
@@ -84,28 +84,6 @@ importers: | |||||
specifier: ^1.2.0 | specifier: ^1.2.0 | ||||
version: 1.5.0(@types/node@20.12.7) | 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: | packages/examples/cms-web-api: | ||||
dependencies: | dependencies: | ||||
'@modal-sh/yasumi': | '@modal-sh/yasumi': | ||||