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 | |||
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 {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; |
@@ -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 | |||
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': | |||