From 92339f7cf9a13026dce7655caff998af50f93087 Mon Sep 17 00:00:00 2001 From: Olivier Costi Date: Thu, 19 Feb 2026 14:05:06 +0100 Subject: [PATCH 1/3] feat: add orderBy with complex expressions to some query --- .../static/queriesWithQueryLanguage.ts.txt | 200 +++++++++++++++--- 1 file changed, 172 insertions(+), 28 deletions(-) diff --git a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt index fe0a93f..14627fe 100644 --- a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt +++ b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt @@ -1,3 +1,99 @@ +const flattenOrderBy = (orderBy: OrderBy[] = []): string => { + const orderByModifierKeys: (keyof OrderByModifier)[] = ['SORT', 'LOWER', 'TRIM', 'LENGTH']; + + const applyModifiers = (property: string, modifier: { LENGTH?: boolean; TRIM?: boolean; LOWER?: 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 | ConditionalOrderBy | undefined + ): orderByItem is ConditionalOrderBy => + !!orderByItem && typeof orderByItem === 'object' && 'CASE' in orderByItem; + + const isConditionalCaseValue = ( + value: QueryFilter | ConditionalOrderBy + ): value is ConditionalOrderBy => + !!value && typeof value === 'object' && 'CASE' in value; + + const applyModifiersToConditionProperties = ( + condition: string, + modifier?: OrderByModifier + ): string => { + if (!modifier || (!modifier.LOWER && !modifier.TRIM && !modifier.LENGTH)) { + return condition; + } + + return condition.replace( + /([A-Za-z_][A-Za-z0-9_.]*)\s*(=|!=|<=|>=|<|>|~|in|null)\b/g, + (_fullMatch, property: string, operator: string) => + `${applyModifiers(property, modifier)} ${operator}` + ); + }; + + const flattenSingle = ( + orderByItem: OrderBy | ConditionalOrderBy | undefined, + isRootConditional = false + ): string[] => { + if (isConditionalOrderBy(orderByItem)) { + const [, when, , then, , elseVal, modifier] = orderByItem.CASE; + const whenStr = isConditionalCaseValue(when) + ? flattenSingle(when, false)[0] + : applyModifiersToConditionProperties( + flattenWhere(when as QueryFilter, []).join(' and '), + modifier + ); + const thenStr = typeof then === 'number' + ? then.toString() + : flattenSingle(then, false)[0]; + const elseStr = typeof elseVal === 'number' + ? elseVal.toString() + : flattenSingle(elseVal, false)[0]; + let expr = `(${whenStr}) ? ${thenStr} : ${elseStr}`; + if (isRootConditional) { + expr += modifier?.SORT === 'desc' ? ' desc' : ' asc'; + } + return [expr]; + } + const result: string[] = []; + for (const key in orderByItem) { + const value = (orderByItem as any)[key]; + if (!value) continue; + + if (typeof value === 'object' && !Array.isArray(value)) { + const modifierValue = value as OrderByModifier; + const hasModifier = orderByModifierKeys.some((modifierKey) => modifierKey in modifierValue); + + if (hasModifier) { + let propExpr = key; + propExpr = applyModifiers(propExpr, modifierValue); + result.push(propExpr + (modifierValue.SORT === 'desc' ? ' desc' : ' asc')); + continue; + } + + const nestedValue = (value as any)[key]; + if (nestedValue && typeof nestedValue === 'object' && !Array.isArray(nestedValue)) { + let propExpr = key; + propExpr = applyModifiers(propExpr, nestedValue); + result.push(propExpr + (nestedValue.SORT === 'desc' ? ' desc' : ' asc')); + } + } + } + return result; + }; + + return orderBy.flatMap((item) => flattenSingle(item, true)).join(', '); +} + export type ComparisonOperator = | 'EQ' | 'NE' @@ -41,6 +137,46 @@ export type QueryFilter = SingleFilterExpr & { NOT?: QueryFilter; }; +export type OrderByModifier = { + SORT: 'asc' | 'desc'; + LOWER?: boolean; + TRIM?: boolean; + LENGTH?: boolean; +}; + +type RootConditionalOrderByModifier = Omit & { + SORT?: 'asc' | 'desc'; +}; + +type NestedConditionalOrderByModifier = Omit; + +export type ConditionalOrderBy = { + CASE: [ + 'WHEN', QueryFilter | ConditionalOrderBy, + 'THEN', number | ConditionalOrderBy, + 'ELSE', number | ConditionalOrderBy, + (IsRoot extends true + ? RootConditionalOrderByModifier + : NestedConditionalOrderByModifier)? + ]; +}; + +export type PropertyOrderBy = { + [K in keyof E]?: { + [V in keyof E]?: V extends K + ? E[V] extends Array | undefined + ? U extends object + ? Sort + : never + : E[V] extends object | undefined + ? Sort + : OrderByModifier + : never; + }; +}[keyof E]; + +export type OrderBy = PropertyOrderBy | ConditionalOrderBy; + export type CountQuery = { where?: QueryFilter; }; @@ -53,6 +189,7 @@ export type SomeQuery = { select?: QuerySelect; sort?: Sort[]; pagination?: Pagination; + orderBy?: OrderBy[]; }; const comparisonOperatorList: ComparisonOperator[] = [ @@ -177,6 +314,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 => { @@ -224,32 +369,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 ?? {} + })) + ); +}; From 880a59121a6c8221ba3752fe0785ec8125ad63c9 Mon Sep 17 00:00:00 2001 From: Olivier Costi Date: Tue, 17 Mar 2026 07:38:00 +0100 Subject: [PATCH 2/3] feat: types for orderBy --- README.md | 104 ++++++++++++ .../static/queriesWithQueryLanguage.ts.txt | 155 ++++++------------ 2 files changed, 156 insertions(+), 103 deletions(-) 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 e6bd40f..51db9ca 100644 --- a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt +++ b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt @@ -1,7 +1,6 @@ const flattenOrderBy = (orderBy: OrderBy[] = []): string => { - const orderByModifierKeys: (keyof OrderByModifier)[] = ['SORT', 'LOWER', 'TRIM', 'LENGTH']; - const applyModifiers = (property: string, modifier: { LENGTH?: boolean; TRIM?: boolean; LOWER?: boolean }) => { + const applyModifiers = (property: string, modifier: { TRIM?: boolean; LOWER?: boolean; LENGTH?: boolean }) => { let result = property; if (modifier.TRIM) { result = `trim(${result})`; @@ -16,82 +15,40 @@ const flattenOrderBy = (orderBy: OrderBy[] = []): string => { }; const isConditionalOrderBy = ( - orderByItem: OrderBy | ConditionalOrderBy | undefined - ): orderByItem is ConditionalOrderBy => + orderByItem: OrderBy + ): orderByItem is ConditionalOrderBy => !!orderByItem && typeof orderByItem === 'object' && 'CASE' in orderByItem; - const isConditionalCaseValue = ( - value: QueryFilter | ConditionalOrderBy - ): value is ConditionalOrderBy => - !!value && typeof value === 'object' && 'CASE' in value; - - const applyModifiersToConditionProperties = ( - condition: string, - modifier?: OrderByModifier - ): string => { - if (!modifier || (!modifier.LOWER && !modifier.TRIM && !modifier.LENGTH)) { - return condition; + const flattenValue = (value: number | FieldOrderBy): string => { + if (typeof value === 'number') { + return value.toString(); } + return applyModifiers(value.FIELD, value); + }; - return condition.replace( - /([A-Za-z_][A-Za-z0-9_.]*)\s*(=|!=|<=|>=|<|>|~|in|null)\b/g, - (_fullMatch, property: string, operator: string) => - `${applyModifiers(property, modifier)} ${operator}` - ); + 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 | ConditionalOrderBy | undefined, - isRootConditional = false - ): string[] => { + const flattenSingle = (orderByItem: OrderBy): string => { if (isConditionalOrderBy(orderByItem)) { - const [, when, , then, , elseVal, modifier] = orderByItem.CASE; - const whenStr = isConditionalCaseValue(when) - ? flattenSingle(when, false)[0] - : applyModifiersToConditionProperties( - flattenWhere(when as QueryFilter, []).join(' and '), - modifier - ); - const thenStr = typeof then === 'number' - ? then.toString() - : flattenSingle(then, false)[0]; - const elseStr = typeof elseVal === 'number' - ? elseVal.toString() - : flattenSingle(elseVal, false)[0]; - let expr = `(${whenStr}) ? ${thenStr} : ${elseStr}`; - if (isRootConditional) { - expr += modifier?.SORT === 'desc' ? ' desc' : ' asc'; - } - return [expr]; + const expr = flattenConditional(orderByItem); + const sort = orderByItem.SORT === 'desc' ? 'desc' : 'asc'; + return `${expr} ${sort}`; } - const result: string[] = []; - for (const key in orderByItem) { - const value = (orderByItem as any)[key]; - if (!value) continue; - - if (typeof value === 'object' && !Array.isArray(value)) { - const modifierValue = value as OrderByModifier; - const hasModifier = orderByModifierKeys.some((modifierKey) => modifierKey in modifierValue); - - if (hasModifier) { - let propExpr = key; - propExpr = applyModifiers(propExpr, modifierValue); - result.push(propExpr + (modifierValue.SORT === 'desc' ? ' desc' : ' asc')); - continue; - } - const nestedValue = (value as any)[key]; - if (nestedValue && typeof nestedValue === 'object' && !Array.isArray(nestedValue)) { - let propExpr = key; - propExpr = applyModifiers(propExpr, nestedValue); - result.push(propExpr + (nestedValue.SORT === 'desc' ? ' desc' : ' asc')); - } - } - } - return result; + const field = orderByItem as FieldOrderBy; + const property = applyModifiers(field.FIELD, field); + const sort = field.SORT === 'desc' ? 'desc' : 'asc'; + return `${property} ${sort}`; }; - return orderBy.flatMap((item) => flattenSingle(item, true)).join(', '); + return orderBy.map(flattenSingle).join(', '); } export type ComparisonOperator = @@ -164,61 +121,53 @@ export type QueryFilter = SingleFilterExpr & { NOT?: QueryFilter; }; -export type OrderByModifier = { - SORT: 'asc' | 'desc'; - LOWER?: boolean; - TRIM?: boolean; - LENGTH?: boolean; +export type ConditionalOrderByCase = { + WHEN: QueryFilter; + THEN: number | FieldOrderBy; }; -type RootConditionalOrderByModifier = Omit & { +export type ConditionalOrderBy = { + CASE: ConditionalOrderByCase[]; + ELSE: number | FieldOrderBy; SORT?: 'asc' | 'desc'; }; -type NestedConditionalOrderByModifier = Omit; - -export type ConditionalOrderBy = { - CASE: [ - 'WHEN', QueryFilter | ConditionalOrderBy, - 'THEN', number | ConditionalOrderBy, - 'ELSE', number | ConditionalOrderBy, - (IsRoot extends true - ? RootConditionalOrderByModifier - : NestedConditionalOrderByModifier)? - ]; +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 PropertyOrderBy = { - [K in keyof E]?: { - [V in keyof E]?: V extends K - ? E[V] extends Array | undefined - ? U extends object - ? Sort - : never - : E[V] extends object | undefined - ? Sort - : OrderByModifier - : never; - }; -}[keyof E]; - -export type OrderBy = PropertyOrderBy | ConditionalOrderBy; +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; - orderBy?: OrderBy[]; }; +export type SomeQuery = SomeQueryBase & + ({ sort?: Sort[]; orderBy?: never } | { sort?: never; orderBy?: OrderBy[] }); + const comparisonOperatorList: ComparisonOperator[] = [ 'EQ', 'NE', @@ -346,7 +295,7 @@ const flattenWhere = ( return entries; }; -const assembleOrderBy = (orderBy: OrderBy[] = []): Record { +const assembleOrderBy = (orderBy: OrderBy[] = []): Record => { if(!orderBy.length) { return {} } From e6b10a3cce4cb446debba8e5f80fe42428b81866 Mon Sep 17 00:00:00 2001 From: Daniel Knogl Date: Fri, 27 Mar 2026 15:55:00 +0100 Subject: [PATCH 3/3] refactor: fix type issue --- src/generator/01-base/static/queriesWithQueryLanguage.ts.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt index 51db9ca..4fa26cb 100644 --- a/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt +++ b/src/generator/01-base/static/queriesWithQueryLanguage.ts.txt @@ -363,7 +363,7 @@ const _some = ( : undefined, ...assembleFilterParam(query?.where), ...flattenSort(query?.sort), - ...assembleOrderBy(query.orderBy), + ...assembleOrderBy(query?.orderBy), ...query?.params, ...query?.pagination };