From 16c414f016cc15f4a07f9580a4ffa2b906c34947 Mon Sep 17 00:00:00 2001 From: Dogan AY Date: Wed, 25 Mar 2026 09:48:04 +0100 Subject: [PATCH] feat(customizer): improve condition tree leaf typing --- .../src/collection-customizer.ts | 29 ++++++++---- .../src/datasource-customizer.ts | 2 +- .../src/decorators/actions/collection.ts | 5 +- .../datasource-customizer/src/templates.ts | 46 +++++++++++++++++-- .../test/collection-customizer.test.ts | 7 +-- 5 files changed, 70 insertions(+), 19 deletions(-) diff --git a/packages/datasource-customizer/src/collection-customizer.ts b/packages/datasource-customizer/src/collection-customizer.ts index 932b302597..8e4d606041 100644 --- a/packages/datasource-customizer/src/collection-customizer.ts +++ b/packages/datasource-customizer/src/collection-customizer.ts @@ -178,7 +178,9 @@ export default class CollectionCustomizer< */ addChart(name: string, definition: CollectionChartDefinition): this { return this.pushCustomization(async () => { - this.stack.chart.getCollection(this.name).addChart(name, definition); + this.stack.chart + .getCollection(this.name) + .addChart(name, definition as unknown as CollectionChartDefinition); }); } @@ -228,7 +230,10 @@ export default class CollectionCustomizer< ? collectionBeforeRelations : collectionAfterRelations; - collection.registerComputed(name, mapDeprecated(definition)); + collection.registerComputed( + name, + mapDeprecated(definition) as unknown as ComputedDefinition, + ); }); }; @@ -261,7 +266,7 @@ export default class CollectionCustomizer< addHook

( position: P, type: T, - handler: HookHandler[P][T]>, + handler: HookHandler[P][T], void, S, N>, ): this { return this.pushCustomization(async () => { this.stack.hook @@ -554,7 +559,7 @@ export default class CollectionCustomizer< ? this.stack.earlyOpEmulate.getCollection(this.name) : this.stack.lateOpEmulate.getCollection(this.name); - collection.replaceFieldOperator(name, operator, replacer as OperatorDefinition); + collection.replaceFieldOperator(name, operator, replacer as unknown as OperatorDefinition); }); } @@ -574,7 +579,9 @@ export default class CollectionCustomizer< definition: WriteDefinition, ): this { return this.pushCustomization(async () => { - this.stack.write.getCollection(this.name).replaceFieldWriting(name, definition); + this.stack.write + .getCollection(this.name) + .replaceFieldWriting(name, definition as unknown as WriteDefinition); }); } @@ -609,7 +616,9 @@ export default class CollectionCustomizer< */ overrideCreate(handler: CreateOverrideHandler): this { return this.pushCustomization(async () => { - this.stack.override.getCollection(this.name).addCreateHandler(handler); + this.stack.override + .getCollection(this.name) + .addCreateHandler(handler as unknown as CreateOverrideHandler); }); } @@ -625,7 +634,9 @@ export default class CollectionCustomizer< */ overrideUpdate(handler: UpdateOverrideHandler): this { return this.pushCustomization(async () => { - this.stack.override.getCollection(this.name).addUpdateHandler(handler); + this.stack.override + .getCollection(this.name) + .addUpdateHandler(handler as unknown as UpdateOverrideHandler); }); } @@ -641,7 +652,9 @@ export default class CollectionCustomizer< */ overrideDelete(handler: DeleteOverrideHandler): this { return this.pushCustomization(async () => { - this.stack.override.getCollection(this.name).addDeleteHandler(handler); + this.stack.override + .getCollection(this.name) + .addDeleteHandler(handler as unknown as DeleteOverrideHandler); }); } diff --git a/packages/datasource-customizer/src/datasource-customizer.ts b/packages/datasource-customizer/src/datasource-customizer.ts index 43b89fb340..5b54f11847 100644 --- a/packages/datasource-customizer/src/datasource-customizer.ts +++ b/packages/datasource-customizer/src/datasource-customizer.ts @@ -104,7 +104,7 @@ export default class DataSourceCustomizer { */ addChart(name: string, definition: DataSourceChartDefinition): this { this.stack.queueCustomization(async () => { - this.stack.chart.addChart(name, definition); + this.stack.chart.addChart(name, definition as unknown as DataSourceChartDefinition); }); return this; diff --git a/packages/datasource-customizer/src/decorators/actions/collection.ts b/packages/datasource-customizer/src/decorators/actions/collection.ts index 6bf2ea8615..12c3b1274b 100644 --- a/packages/datasource-customizer/src/decorators/actions/collection.ts +++ b/packages/datasource-customizer/src/decorators/actions/collection.ts @@ -8,7 +8,7 @@ import type { SearchOptionsHandler, ValueOrHandler, } from './types/fields'; -import type { TSchema } from '../../templates'; +import type { TFilter, TSchema } from '../../templates'; import type { ActionFormElement, ActionResult, @@ -18,7 +18,6 @@ import type { Filter, GetFormMetas, LayoutElementPageWithField, - PlainFilter, RecordData, } from '@forestadmin/datasource-toolkit'; @@ -177,7 +176,7 @@ export default class ActionCollectionDecorator extends CollectionDecorator { Global: ActionContext, Bulk: ActionContext, Single: ActionContextSingle, - }[action.scope](this, caller, formValues, filter as unknown as PlainFilter, used, changedField); + }[action.scope](this, caller, formValues, filter as unknown as TFilter, used, changedField); } private getSearchedField(element: DynamicFormElementOrPage, search: string): DynamicField | null { diff --git a/packages/datasource-customizer/src/templates.ts b/packages/datasource-customizer/src/templates.ts index 2c0c324f36..c232afbf61 100644 --- a/packages/datasource-customizer/src/templates.ts +++ b/packages/datasource-customizer/src/templates.ts @@ -78,14 +78,52 @@ export type TPartialFlatRow< N extends TCollectionName = TCollectionName, > = RecursivePartial; +/** Operators that require no value */ +type NoValueOperator = + | 'Blank' + | 'Present' + | 'Missing' + | 'Today' + | 'Yesterday' + | 'PreviousMonth' + | 'PreviousQuarter' + | 'PreviousWeek' + | 'PreviousYear' + | 'PreviousMonthToDate' + | 'PreviousQuarterToDate' + | 'PreviousWeekToDate' + | 'PreviousYearToDate' + | 'Past' + | 'Future'; + +/** Operators that require an array of values */ +type ArrayValueOperator = 'In' | 'NotIn' | 'IncludesAll' | 'IncludesNone'; + +/** Operators that always require a number value */ +type NumberValueOperator = + | 'PreviousXDaysToDate' + | 'PreviousXDays' + | 'BeforeXHoursAgo' + | 'AfterXHoursAgo' + | 'LongerThan' + | 'ShorterThan'; + +/** Operators that require a single value matching the field type */ +type SingleValueOperator = Exclude< + Operator, + NoValueOperator | ArrayValueOperator | NumberValueOperator +>; + export type TConditionTreeLeaf< S extends TSchema = TSchema, N extends TCollectionName = TCollectionName, > = { - field: TFieldName; - operator: Operator; - value?: unknown; -}; + [F in TFieldName]: + | { field: F; operator: NoValueOperator } + | { field: F; operator: ArrayValueOperator; value: TFieldType[] } + | { field: F; operator: NumberValueOperator; value: number } + | { field: F; operator: SingleValueOperator; value: TFieldType }; +}[TFieldName]; export type TConditionTreeBranch< S extends TSchema = TSchema, diff --git a/packages/datasource-customizer/test/collection-customizer.test.ts b/packages/datasource-customizer/test/collection-customizer.test.ts index a459a7d4a8..57f98708f7 100644 --- a/packages/datasource-customizer/test/collection-customizer.test.ts +++ b/packages/datasource-customizer/test/collection-customizer.test.ts @@ -9,7 +9,7 @@ import type { ActionDefinition } from '../src/decorators/actions/types/actions'; import type { WriteDefinition } from '../src/decorators/write/write-replace/types'; import type { ColumnSchema } from '@forestadmin/datasource-toolkit'; -import { ConditionTreeLeaf, MissingFieldError, Sort } from '@forestadmin/datasource-toolkit'; +import { MissingFieldError, Sort } from '@forestadmin/datasource-toolkit'; import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__'; import { CollectionCustomizer, DataSourceCustomizer } from '../src'; @@ -639,7 +639,7 @@ describe('Builder > Collection', () => { it('should add a segment', async () => { const { dsc, customizer } = await setup(); - const generator = async () => new ConditionTreeLeaf('fieldName', 'Present'); + const generator = async () => ({ field: 'fieldName', operator: 'Present' } as const); const self = customizer.addSegment('new segment', generator); await dsc.getDataSource(logger); @@ -773,7 +773,8 @@ describe('Builder > Collection', () => { it('should replace operator on field', async () => { const { dsc, customizer } = await setup(); - const replacer = async () => new ConditionTreeLeaf('fieldName', 'NotEqual', null); + const replacer = async () => + ({ field: 'fieldName', operator: 'NotEqual', value: null } as const); const self = customizer.replaceFieldOperator('firstName', 'Present', replacer); await dsc.getDataSource(logger);