diff --git a/README.md b/README.md index ac990d6..aeb196e 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,110 @@ wServices['article'].some({ This only returns the articleNumber property of all articles. +#### Ordering + +With the some function you can order requested data. You can either merely order by fields or by complex expressions. + +```ts +/** + * Order by createdDate in ascending order. + * + * ?orderBy=createdDate asc + */ +wServices['article'].some({ + orderBy: [{ FIELD: 'createdDate', SORT: 'asc' }] +}); +``` + +```ts +/** + * First order by createdDate in ascending order, then by articleNumber in descending order + * If you omit SORT the default ordering will be set to ascending. + * + * ?orderBy=createdDate asc, articleNumber desc + */ +wServices['article'].some({ + orderBy: [{ FIELD: 'createdDate' }, { FIELD: 'articleNumber', SORT: 'desc' }] +}); +``` + +```ts +/** + * Order with conditional sorting: if internalNote is not null then 1, + * if packagingQuantity > 400 then 2, otherwise 3. + * Then order by articleNumber in ascending order. + * + * ?orderBy=(not internalNote null) ? 1 : (packagingQuantity > 400) ? 2 : 3 asc, articleNumber asc + */ +wServices['article'].some({ + orderBy: [ + { + CASE: [ + { WHEN: { internalNote: { NULL: false } }, THEN: 1 }, + { WHEN: { packagingQuantity: { GT: 400 } }, THEN: 2 } + ], + ELSE: 3, + SORT: 'asc' + }, + { FIELD: 'articleNumber' } + ] +}); +``` + +The `THEN` and `ELSE` values can also be a `FieldOrderBy` to fall back to a field-based ordering: + +```ts +/** + * Order with conditional sorting: if internalNote is not null, order by articleNumber, + * otherwise order by createdDate. + * + * ?orderBy=(not internalNote null) ? articleNumber : createdDate asc + */ +wServices['article'].some({ + orderBy: [ + { + CASE: [{ WHEN: { internalNote: { NULL: false } }, THEN: { FIELD: 'articleNumber' } }], + ELSE: { FIELD: 'createdDate' }, + SORT: 'asc' + } + ] +}); +``` + +For properties that are objects or arrays of objects, use dot-notation to reference nested fields: + +```ts +/** + * Order by a nested property of an array/object field. + * + * ?orderBy=articlePrices.price asc + */ +wServices['article'].some({ + orderBy: [{ FIELD: 'articlePrices.price', SORT: 'asc' }] +}); +``` + +There are three modifier functions, `TRIM`, `LOWER` and `LENGTH`, which can be used to adjust the expessions: + +```ts +/** + * First order by the length of the trimmed lastName in descending order, then by firstName in ascending order + * + * ?orderBy=length(trim(lastName)) desc, firstName asc + */ +wServices['party'].some({ + orderBy: [ + { + FIELD: 'lastName', + LENGTH: true, + TRIM: true, + SORT: 'desc' + }, + { FIELD: 'firstName' } + ] +}); +``` + ### Aborting a request To abort a request an AbortController has to be instantiated and its signal has to be passed to the request. The controller can diff --git a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt index 6be4287..4fa26cb 100644 --- a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt +++ b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt @@ -1,3 +1,56 @@ +const flattenOrderBy = (orderBy: OrderBy[] = []): string => { + + const applyModifiers = (property: string, modifier: { TRIM?: boolean; LOWER?: boolean; LENGTH?: boolean }) => { + let result = property; + if (modifier.TRIM) { + result = `trim(${result})`; + } + if (modifier.LOWER) { + result = `lower(${result})`; + } + if (modifier.LENGTH) { + result = `length(${result})`; + } + return result; + }; + + const isConditionalOrderBy = ( + orderByItem: OrderBy + ): orderByItem is ConditionalOrderBy => + !!orderByItem && typeof orderByItem === 'object' && 'CASE' in orderByItem; + + const flattenValue = (value: number | FieldOrderBy): string => { + if (typeof value === 'number') { + return value.toString(); + } + return applyModifiers(value.FIELD, value); + }; + + const flattenConditional = (item: ConditionalOrderBy): string => { + const cases = item.CASE.map((c) => { + const whenStr = flattenWhere(c.WHEN as QueryFilter, []).join(' and '); + return `(${whenStr}) ? ${flattenValue(c.THEN)}`; + }); + + return cases.reduceRight((acc, caseStr) => `${caseStr} : ${acc}`, flattenValue(item.ELSE)); + }; + + const flattenSingle = (orderByItem: OrderBy): string => { + if (isConditionalOrderBy(orderByItem)) { + const expr = flattenConditional(orderByItem); + const sort = orderByItem.SORT === 'desc' ? 'desc' : 'asc'; + return `${expr} ${sort}`; + } + + const field = orderByItem as FieldOrderBy; + const property = applyModifiers(field.FIELD, field); + const sort = field.SORT === 'desc' ? 'desc' : 'asc'; + return `${property} ${sort}`; + }; + + return orderBy.map(flattenSingle).join(', '); +} + export type ComparisonOperator = | 'EQ' | 'NE' @@ -68,20 +121,53 @@ export type QueryFilter = SingleFilterExpr & { NOT?: QueryFilter; }; +export type ConditionalOrderByCase = { + WHEN: QueryFilter; + THEN: number | FieldOrderBy; +}; + +export type ConditionalOrderBy = { + CASE: ConditionalOrderByCase[]; + ELSE: number | FieldOrderBy; + SORT?: 'asc' | 'desc'; +}; + +export type FieldPath = { + [K in keyof T & string]: NonNullable extends Array + ? U extends Record + ? `${K}.${FieldPath}` + : never + : NonNullable extends Record + ? K | `${K}.${FieldPath>}` + : K; +}[keyof T & string]; + +export type FieldOrderBy = { + FIELD: FieldPath; + SORT?: 'asc' | 'desc'; + LOWER?: boolean; + TRIM?: boolean; + LENGTH?: boolean; +}; + +export type OrderBy = FieldOrderBy | ConditionalOrderBy; + export type CountQuery = { where?: QueryFilter; }; -export type SomeQuery = { +type SomeQueryBase = { serializeNulls?: boolean; include?: QuerySelect; properties?: P; where?: QueryFilter; select?: QuerySelect; - sort?: Sort[]; pagination?: Pagination; }; +export type SomeQuery = SomeQueryBase & + ({ sort?: Sort[]; orderBy?: never } | { sort?: never; orderBy?: OrderBy[] }); + const comparisonOperatorList: ComparisonOperator[] = [ 'EQ', 'NE', @@ -209,6 +295,14 @@ const flattenWhere = ( return entries; }; +const assembleOrderBy = (orderBy: OrderBy[] = []): Record => { + if(!orderBy.length) { + return {} + } + const flattedOrderBy = flattenOrderBy(orderBy); + return flattedOrderBy.length ? { orderBy: flattedOrderBy } : {}; +} + const assembleFilterParam = ( obj: QueryFilter = {} ): Record => { @@ -256,32 +350,31 @@ const _some = ( endpoint: string, query?: SomeQuery & { params?: Record }, requestOptions?: RequestOptions -) => - { - const usePost = cfg?.usePost ?? globalConfig?.usePost - const payload = { - serializeNulls: query?.serializeNulls, - additionalProperties: query?.properties?.join(','), - properties: query?.select - ? flattenSelect(query.select).join(',') - : undefined, - includeReferencedEntities: query?.include - ? Object.keys(query.include).join(',') - : undefined, - ...assembleFilterParam(query?.where), - ...flattenSort(query?.sort), - ...query?.params, - ...query?.pagination - } - - return wrapResponse(() => - raw(cfg, usePost ? `${endpoint}/query` : endpoint, { - method: usePost ? 'POST' : 'GET', - ...(usePost ? { body: payload } : { query: payload }) - }, requestOptions).then((data) => ({ - entities: data.result, - references: data.referencedEntities ?? {}, - properties: data.additionalProperties ?? {} - })) - ) +) => { + const usePost = cfg?.usePost ?? globalConfig?.usePost; + const payload = { + serializeNulls: query?.serializeNulls, + additionalProperties: query?.properties?.join(','), + properties: query?.select + ? flattenSelect(query.select).join(',') + : undefined, + includeReferencedEntities: query?.include + ? Object.keys(query.include).join(',') + : undefined, + ...assembleFilterParam(query?.where), + ...flattenSort(query?.sort), + ...assembleOrderBy(query?.orderBy), + ...query?.params, + ...query?.pagination }; + return wrapResponse(() => + raw(cfg, usePost ? `${endpoint}/query` : endpoint, { + method: usePost ? 'POST' : 'GET', + ...(usePost ? { body: payload } : { query: payload }) + }, requestOptions).then((data) => ({ + entities: data.result, + references: data.referencedEntities ?? {}, + properties: data.additionalProperties ?? {} + })) + ); +};