diff --git a/packages/devextreme/js/__internal/data/abstract_store.ts b/packages/devextreme/js/__internal/data/abstract_store.ts index 8987913c2ed7..57979b10ecc9 100644 --- a/packages/devextreme/js/__internal/data/abstract_store.ts +++ b/packages/devextreme/js/__internal/data/abstract_store.ts @@ -156,6 +156,9 @@ class Store { loadOptions._langParams = { ...this._langParams, ...loadOptions._langParams }; } + // @ts-expect-error `createQuery()` is declared with the public `Query` type, whose + // enumerate() promises a native Promise, while the query implementations resolve a + // Deferred (iteration 2: type createQuery() with the internal query interface) const result: DeferredObj = queryByOptions( this.createQuery(loadOptions), loadOptions, @@ -193,6 +196,9 @@ class Store { } _totalCountImpl(options?: StoreLoadOptions): DeferredObj { + // @ts-expect-error `createQuery()` is declared with the public `Query` type, whose + // count() promises a native Promise, while the query implementations resolve a + // Deferred (iteration 2: type createQuery() with the internal query interface) const result: DeferredObj = queryByOptions( this.createQuery(options), options, diff --git a/packages/devextreme/js/__internal/data/array_store.ts b/packages/devextreme/js/__internal/data/array_store.ts index 7ae9ef524ffc..814e5a272bb9 100644 --- a/packages/devextreme/js/__internal/data/array_store.ts +++ b/packages/devextreme/js/__internal/data/array_store.ts @@ -47,27 +47,23 @@ class ArrayStore extends Store { const index = indexByKey(this, this._array, key); if (index === -1) { - // @ts-expect-error data/utils is untyped: `rejectedPromise` reads its arguments object const rejected: DeferredObj = rejectedPromise(errors.Error('E4009')); return rejected; } - // @ts-expect-error data/utils is untyped: `trivialPromise` reads its arguments object const resolved: DeferredObj = trivialPromise(this._array[index]); return resolved; } _insertImpl(values: unknown): DeferredObj { - // @ts-expect-error array_utils is untyped: `insert` declares every argument as required const result: DeferredObj = insert(this, this._array, values); return result; } _pushImpl(changes: StoreChange[]): void { - // @ts-expect-error array_utils is untyped: `applyBatch` destructures every option as required applyBatch({ keyInfo: this, data: this._array, @@ -76,14 +72,12 @@ class ArrayStore extends Store { } _updateImpl(key: unknown, values: unknown): DeferredObj { - // @ts-expect-error array_utils is untyped: `update` declares every argument as required const result: DeferredObj = update(this, this._array, key, values); return result; } _removeImpl(key: unknown): DeferredObj { - // @ts-expect-error array_utils is untyped: `remove` declares every argument as required const result: DeferredObj = remove(this, this._array, key); return result; diff --git a/packages/devextreme/js/__internal/data/array_utils.ts b/packages/devextreme/js/__internal/data/array_utils.ts new file mode 100644 index 000000000000..63f7a786a7f6 --- /dev/null +++ b/packages/devextreme/js/__internal/data/array_utils.ts @@ -0,0 +1,428 @@ +import { errors } from '@js/common/data/errors'; +import { keysEqual, rejectedPromise, trivialPromise } from '@js/common/data/utils'; +import config from '@js/core/config'; +import Guid from '@js/core/guid'; +import { compileGetter } from '@js/core/utils/data'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { deepExtendArraySafe } from '@js/core/utils/object'; +import { + isDefined, isEmptyObject, isObject, isPlainObject, isString, +} from '@js/core/utils/type'; +import type { StoreChange } from '@js/data/store'; +import { isCollectionLike } from '@ts/core/utils/m_object'; +import type { StoreKey } from '@ts/data/abstract_store'; + +export type KeyExpr = StoreKey | Function; + +export interface KeyInfo { + /* eslint-disable @typescript-eslint/method-signature-style */ + key(): KeyExpr | undefined; + keyOf(obj: unknown): unknown; + /* eslint-enable @typescript-eslint/method-signature-style */ +} + +interface CachedArray extends Array { + _dataByKeyMap?: Record; + _dataByKeyMapLength?: number; +} + +interface GroupItem { + items?: unknown[]; + collapsedItems?: unknown[]; +} + +export interface ApplyBatchOptions { + keyInfo: KeyInfo; + data: unknown[]; + changes: StoreChange[]; + groupCount?: number; + useInsertIndex?: boolean; + immutable?: boolean; + disableCache?: boolean; + logError?: boolean; + skipCopying?: boolean; +} + +export interface ApplyChangesOptions { + keyExpr?: StoreKey; + immutable?: boolean; +} + +function hasKey(target: unknown, keyOrKeys: KeyExpr): boolean { + let keys: string[] = []; + if (isString(keyOrKeys)) { + keys = [keyOrKeys]; + } else if (Array.isArray(keyOrKeys)) { + keys = keyOrKeys.slice(); + } + + while (keys.length) { + const key = keys.shift(); + if (isDefined(key) && isObject(target) && key in target) { + return true; + } + } + + return false; +} + +function generateDataByKeyMap(keyInfo: KeyInfo, array: CachedArray): void { + if (keyInfo.key() && (!array._dataByKeyMap || array._dataByKeyMapLength !== array.length)) { + const dataByKeyMap: Record = {}; + const arrayLength = array.length; + for (let i = 0; i < arrayLength; i += 1) { + dataByKeyMap[JSON.stringify(keyInfo.keyOf(array[i]))] = array[i]; + } + + array._dataByKeyMap = dataByKeyMap; + array._dataByKeyMapLength = arrayLength; + } +} + +function getCacheValue(array: CachedArray, key: unknown): unknown { + return array._dataByKeyMap?.[JSON.stringify(key)]; +} + +function getHasKeyCacheValue(array: CachedArray, key: unknown): unknown { + if (array._dataByKeyMap) { + return array._dataByKeyMap[JSON.stringify(key)]; + } + + return true; +} + +function setDataByKeyMapValue(array: CachedArray, key: unknown, data: unknown): void { + if (array._dataByKeyMap) { + array._dataByKeyMap[JSON.stringify(key)] = data; + array._dataByKeyMapLength = (array._dataByKeyMapLength ?? 0) + (data ? 1 : -1); + } +} + +function indexByKey(keyInfo: KeyInfo, array: unknown[], key: unknown): number { + const keyExpr = keyInfo.key(); + + if (!getHasKeyCacheValue(array, key)) { + return -1; + } + + for (let i = 0, arrayLength = array.length; i < arrayLength; i += 1) { + if (keysEqual(keyExpr, keyInfo.keyOf(array[i]), key)) { + return i; + } + } + return -1; +} + +function findItems( + keyInfo: KeyInfo, + items: unknown[], + key: unknown, + groupCount: number, +): unknown[] | undefined { + if (groupCount) { + for (const item of items) { + const group: GroupItem = isObject(item) ? item : {}; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const childItems = group.items || group.collapsedItems || []; + const result = findItems(keyInfo, childItems, key, groupCount - 1); + if (result) { + return result; + } + } + } else if (indexByKey(keyInfo, items, key) >= 0) { + return items; + } + + return undefined; +} + +function getItems( + keyInfo: KeyInfo, + items: unknown[], + key: unknown, + groupCount: number | undefined, +): unknown[] { + if (groupCount) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return findItems(keyInfo, items, key, groupCount) || []; + } + + return items; +} + +function cloneInstanceWithChangedPaths( + instance: unknown, + changes: unknown, + clonedInstances: WeakMap = new WeakMap(), +): unknown { + if (isCollectionLike(instance)) { + return instance; + } + + const source: object = isObject(instance) ? instance : {}; + const result: object = isObject(instance) + ? Object.create(Object.getPrototypeOf(instance)) + : {}; + + if (isObject(instance)) { + clonedInstances.set(instance, result); + } + + const instanceWithoutPrototype = { ...source }; + deepExtendArraySafe(result, instanceWithoutPrototype, true, true, true); + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const name in instanceWithoutPrototype) { + const value: unknown = instanceWithoutPrototype[name]; + const change: unknown = isObject(changes) ? changes[name] : undefined; + + if (isObject(value) && !isPlainObject(value) + && isObject(change) && !clonedInstances.has(value)) { + result[name] = cloneInstanceWithChangedPaths(value, change, clonedInstances); + } + } + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const name in result) { + const prop: unknown = result[name]; + + if (isObject(prop) && clonedInstances.has(prop)) { + result[name] = clonedInstances.get(prop); + } + } + + return result; +} + +function createObjectWithChanges( + target: unknown, + changes?: unknown, +): Record { + const result = cloneInstanceWithChangedPaths(target, changes); + const extended: Record = deepExtendArraySafe(result, changes, true, true, true); + + return extended; +} + +function getErrorResult( + isBatch: boolean | undefined, + logError: boolean | undefined, + errorCode: string, +): DeferredObj | undefined { + if (!isBatch) { + const error: unknown = errors.Error(errorCode); + return rejectedPromise(error); + } + + if (logError) { + errors.log(errorCode); + } + + return undefined; +} + +function update( + keyInfo: KeyInfo, array: unknown[], key: unknown, data: unknown, +): DeferredObj; +function update( + keyInfo: KeyInfo, array: unknown[], key: unknown, data: unknown, + isBatch: true, immutable?: boolean, logError?: boolean, +): undefined; +function update( + keyInfo: KeyInfo, + array: unknown[], + key: unknown, + data: unknown, + isBatch?: boolean, + immutable?: boolean, + logError?: boolean, +): DeferredObj | undefined { + // eslint-disable-next-line @typescript-eslint/init-declarations + let target: unknown; + const extendComplexObject = true; + const keyExpr = keyInfo.key(); + + if (keyExpr) { + if (hasKey(data, keyExpr) && !keysEqual(keyExpr, key, keyInfo.keyOf(data))) { + return getErrorResult(isBatch, logError, 'E4017'); + } + + target = getCacheValue(array, key); + if (!target) { + const index = indexByKey(keyInfo, array, key); + if (index < 0) { + return getErrorResult(isBatch, logError, 'E4009'); + } + + target = array[index]; + + if (immutable === true && isDefined(target)) { + const newTarget = createObjectWithChanges(target, data); + array[index] = newTarget; + return isBatch ? undefined : trivialPromise(newTarget, key); + } + } + } else { + target = key; + } + + deepExtendArraySafe(target, data, extendComplexObject, false, true, true); + if (!isBatch) { + if (config().useLegacyStoreResult) { + return trivialPromise(key, data); + } + return trivialPromise(target, key); + } + + return undefined; +} + +function insert( + keyInfo: KeyInfo, array: unknown[], data: unknown, index?: number, +): DeferredObj; +function insert( + keyInfo: KeyInfo, array: unknown[], data: unknown, index: number | undefined, + isBatch: true, logError?: boolean, skipCopying?: boolean, +): undefined; +function insert( + keyInfo: KeyInfo, + array: unknown[], + data: unknown, + index?: number, + isBatch?: boolean, + logError?: boolean, + skipCopying?: boolean, +): DeferredObj | undefined { + // eslint-disable-next-line @typescript-eslint/init-declarations + let keyValue: unknown; + const keyExpr = keyInfo.key(); + + const obj: unknown = isPlainObject(data) && !skipCopying ? extend({}, data) : data; + + if (keyExpr) { + keyValue = keyInfo.keyOf(obj); + if (keyValue === undefined || (typeof keyValue === 'object' && isEmptyObject(keyValue))) { + if (!isString(keyExpr)) { + throw errors.Error('E4007'); + } + + const generatedKey = String(new Guid()); + + if (isObject(obj)) { + obj[keyExpr] = generatedKey; + } + keyValue = generatedKey; + } else if (array[indexByKey(keyInfo, array, keyValue)] !== undefined) { + return getErrorResult(isBatch, logError, 'E4008'); + } + } else { + keyValue = obj; + } + if (isDefined(index) && index >= 0) { + array.splice(index, 0, obj); + } else { + array.push(obj); + } + + setDataByKeyMapValue(array, keyValue, obj); + + if (!isBatch) { + return trivialPromise(config().useLegacyStoreResult ? data : obj, keyValue); + } + + return undefined; +} + +function remove( + keyInfo: KeyInfo, array: unknown[], key: unknown, +): DeferredObj; +function remove( + keyInfo: KeyInfo, array: unknown[], key: unknown, isBatch: true, logError?: boolean, +): undefined; +function remove( + keyInfo: KeyInfo, + array: unknown[], + key: unknown, + isBatch?: boolean, + logError?: boolean, +): DeferredObj | undefined { + const index = indexByKey(keyInfo, array, key); + if (index > -1) { + array.splice(index, 1); + setDataByKeyMapValue(array, key, null); + } + if (!isBatch) { + return trivialPromise(key); + } + if (index < 0) { + return getErrorResult(isBatch, logError, 'E4009'); + } + + return undefined; +} + +function applyBatch({ + keyInfo, data, changes, groupCount, useInsertIndex, immutable, + disableCache, logError, skipCopying, +}: ApplyBatchOptions): unknown[] { + const resultItems = immutable === true ? [...data] : data; + + changes.forEach((item) => { + const items = item.type === 'insert' + ? resultItems + : getItems(keyInfo, resultItems, item.key, groupCount); + + if (!disableCache) { + generateDataByKeyMap(keyInfo, items); + } + + const insertIndex = useInsertIndex && isDefined(item.index) ? item.index : -1; + + // eslint-disable-next-line default-case + switch (item.type) { + case 'update': + update(keyInfo, items, item.key, item.data, true, immutable, logError); + break; + case 'insert': + insert(keyInfo, items, item.data, insertIndex, true, logError, skipCopying); + break; + case 'remove': + remove(keyInfo, items, item.key, true, logError); + break; + } + }); + return resultItems; +} + +function applyChanges( + data: unknown[], + changes: StoreChange[], + options: ApplyChangesOptions = {}, +): unknown[] { + const { keyExpr = 'id', immutable = true } = options; + // @ts-expect-error core/utils/data.d.ts types compileGetter as `(expr: string) => unknown`, + // although it also accepts a compound key expression and returns a getter function + const keyGetter: Function = compileGetter(keyExpr); + const keyInfo: KeyInfo = { + key: () => keyExpr, + keyOf: (obj: unknown): unknown => keyGetter(obj), + }; + + return applyBatch({ + keyInfo, + data, + changes, + immutable, + disableCache: true, + logError: true, + }); +} + +export { + applyBatch, + applyChanges, + createObjectWithChanges, + indexByKey, + insert, + remove, + update, +}; diff --git a/packages/devextreme/js/__internal/data/custom_store.ts b/packages/devextreme/js/__internal/data/custom_store.ts index 90e720d4056c..77d22482a4c7 100644 --- a/packages/devextreme/js/__internal/data/custom_store.ts +++ b/packages/devextreme/js/__internal/data/custom_store.ts @@ -209,7 +209,7 @@ function runRawLoadWithQuery( const rawDataQuery = arrayQuery(rawData, { errorHandler: store._errorHandler }); const waitList: DeferredObj[] = []; - const result: { items?: unknown[]; totalCount?: unknown } = {}; + const result: { items?: unknown; totalCount?: unknown } = {}; if (!countOnly) { const itemsQuery = storeHelper.queryByOptions(rawDataQuery, loadOptions, false); @@ -217,7 +217,7 @@ function runRawLoadWithQuery( result.items = rawData.slice(0); } else { const itemsPromise: DeferredObj = itemsQuery.enumerate() - .done((asyncResult: unknown[]) => { + .done((asyncResult: unknown) => { result.items = asyncResult; }); waitList.push(itemsPromise); @@ -367,7 +367,6 @@ class CustomStore extends Store { _pushImpl(changes: StoreChange[]): void { if (this.__rawData) { - // @ts-expect-error array_utils is untyped: `applyBatch` destructures every option as required applyBatch({ keyInfo: this, data: this.__rawData, diff --git a/packages/devextreme/js/__internal/data/data_controller/data_controller.ts b/packages/devextreme/js/__internal/data/data_controller/data_controller.ts index 840781fd8302..3b4d5b4bf14f 100644 --- a/packages/devextreme/js/__internal/data/data_controller/data_controller.ts +++ b/packages/devextreme/js/__internal/data/data_controller/data_controller.ts @@ -65,7 +65,6 @@ class DataController { this._isSharedDataSource = true; this._dataSource = dataSourceOptions; } else { - // @ts-expect-error const normalizedDataSourceOptions = normalizeDataSourceOptions(dataSourceOptions); this._dataSource = new DataSource( extend(true, {}, {}, normalizedDataSourceOptions), diff --git a/packages/devextreme/js/__internal/data/data_source/data_source.ts b/packages/devextreme/js/__internal/data/data_source/data_source.ts index 72aae3df3fab..1f1cc8ee8fec 100644 --- a/packages/devextreme/js/__internal/data/data_source/data_source.ts +++ b/packages/devextreme/js/__internal/data/data_source/data_source.ts @@ -28,6 +28,7 @@ import type { ChangedEvent, DataSourceEventName, EventOptionName, LoadOperation, LoadResult, NormalizedDataSourceOptions, StoreLoadOptions, } from './types'; +import type { Mapper } from './utils'; // Mirrors the coercion the global `isFinite` applies to non-numeric values. const isFiniteValue = (value: unknown): value is number => isFinite(Number(value)); @@ -58,11 +59,11 @@ export class DataSource { _onPushHandler: Function; - _aggregationTimeoutId?: number; + _aggregationTimeoutId?: ReturnType; _storeLoadOptions: StoreLoadOptions; - _mapFunc?: Function; + _mapFunc?: Mapper; _postProcessFunc?: Function; @@ -567,7 +568,6 @@ export class DataSource { dataSourceChanges = changingArgs.postProcessChanges(dataSourceChanges); } - // @ts-expect-error array_utils is untyped: `applyBatch` destructures every option as required applyBatch({ keyInfo: this.store(), data: items, diff --git a/packages/devextreme/js/__internal/data/data_source/m_utils.ts b/packages/devextreme/js/__internal/data/data_source/m_utils.ts deleted file mode 100644 index 40192c669e20..000000000000 --- a/packages/devextreme/js/__internal/data/data_source/m_utils.ts +++ /dev/null @@ -1,121 +0,0 @@ -import ArrayStore from '@js/common/data/array_store'; -import { CustomStore } from '@js/common/data/custom_store'; -import { normalizeSortingInfo } from '@js/common/data/utils'; -import ajaxUtils from '@js/core/utils/ajax'; -import { extend } from '@js/core/utils/extend'; -import { each, map } from '@js/core/utils/iterator'; -import { isPlainObject } from '@js/core/utils/type'; -import Store from '@js/data/abstract_store'; - -export const CANCELED_TOKEN = 'canceled'; - -export const isPending = (deferred) => deferred.state() === 'pending'; - -export const normalizeStoreLoadOptionAccessorArguments = (originalArguments) => { - // eslint-disable-next-line default-case - switch (originalArguments.length) { - case 0: - return undefined; - case 1: - return originalArguments[0]; - } - return [].slice.call(originalArguments); -}; - -const mapGroup = (group, level, mapper) => map(group, (item) => { - const { items, ...restItem } = item; - return { - ...restItem, - items: mapRecursive(item.items, level - 1, mapper), - }; -}); - -const mapRecursive = (items, level, mapper) => { - if (!Array.isArray(items)) return items; - return level ? mapGroup(items, level, mapper) : map(items, mapper); -}; - -export const mapDataRespectingGrouping = (items, mapper, groupInfo) => { - const level = groupInfo ? normalizeSortingInfo(groupInfo).length : 0; - - return mapRecursive(items, level, mapper); -}; - -export const normalizeLoadResult = (data, extra) => { - if (data?.data) { - extra = data; - data = data.data; - } - - if (!Array.isArray(data)) { - data = [data]; - } - - return { - data, - extra, - }; -}; - -const createCustomStoreFromLoadFunc = (options) => { - const storeConfig = {}; - - each(['useDefaultSearch', 'key', 'load', 'loadMode', 'cacheRawData', 'byKey', 'lookup', 'totalCount', 'insert', 'update', 'remove'], function () { - storeConfig[this] = options[this]; - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete options[this]; - }); - return new CustomStore(storeConfig); -}; - -const createStoreFromConfig = (storeConfig) => { - const alias = storeConfig.type; - - delete storeConfig.type; - // @ts-expect-error - return Store.create(alias, storeConfig); -}; - -const createCustomStoreFromUrl = (url, normalizationOptions) => new CustomStore({ - load: () => ajaxUtils.sendRequest({ url, dataType: 'json' }), - loadMode: normalizationOptions?.fromUrlLoadMode, -}); - -export const normalizeDataSourceOptions = (options, normalizationOptions) => { - let store; - - if (typeof options === 'string') { - options = { - paginate: false, - store: createCustomStoreFromUrl(options, normalizationOptions), - }; - } - - if (options === undefined) { - options = []; - } - - if (Array.isArray(options) || options instanceof Store) { - options = { store: options }; - } else { - options = extend({}, options); - } - - if (options.store === undefined) { - options.store = []; - } - - store = options.store; - - if ('load' in options) { - store = createCustomStoreFromLoadFunc(options); - } else if (Array.isArray(store)) { - store = new ArrayStore(store); - } else if (isPlainObject(store)) { - store = createStoreFromConfig(extend({}, store)); - } - - options.store = store; - - return options; -}; diff --git a/packages/devextreme/js/__internal/data/data_source/m_operation_manager.ts b/packages/devextreme/js/__internal/data/data_source/operation_manager.ts similarity index 53% rename from packages/devextreme/js/__internal/data/data_source/m_operation_manager.ts rename to packages/devextreme/js/__internal/data/data_source/operation_manager.ts index cfde35fffb6f..4cb21cb79b92 100644 --- a/packages/devextreme/js/__internal/data/data_source/m_operation_manager.ts +++ b/packages/devextreme/js/__internal/data/data_source/operation_manager.ts @@ -1,32 +1,24 @@ import { CANCELED_TOKEN } from '@js/common/data/data_source/utils'; +import type { DeferredObj } from '@js/core/utils/deferred'; export default class OperationManager { - constructor() { - // @ts-expect-error - this._counter = -1; - // @ts-expect-error - this._deferreds = {}; - } + _counter = -1; + + _deferreds: Record> = {}; - add(deferred) { - // @ts-expect-error - this._counter++; - // @ts-expect-error + add(deferred: DeferredObj): number { + this._counter += 1; this._deferreds[this._counter] = deferred; - // @ts-expect-error return this._counter; } - remove(operationId) { - // @ts-expect-error + remove(operationId: number): boolean { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete return delete this._deferreds[operationId]; } - cancel(operationId) { - // @ts-expect-error + cancel(operationId: number): boolean { if (operationId in this._deferreds) { - // @ts-expect-error this._deferreds[operationId].reject(CANCELED_TOKEN); return true; } @@ -34,13 +26,10 @@ export default class OperationManager { return false; } - cancelAll() { - // @ts-expect-error + cancelAll(): void { while (this._counter > -1) { - // @ts-expect-error this.cancel(this._counter); - // @ts-expect-error - this._counter--; + this._counter -= 1; } } } diff --git a/packages/devextreme/js/__internal/data/data_source/utils.ts b/packages/devextreme/js/__internal/data/data_source/utils.ts new file mode 100644 index 000000000000..d7a37ca492dc --- /dev/null +++ b/packages/devextreme/js/__internal/data/data_source/utils.ts @@ -0,0 +1,176 @@ +import ArrayStore from '@js/common/data/array_store'; +import { CustomStore } from '@js/common/data/custom_store'; +import { normalizeSortingInfo } from '@js/common/data/utils'; +import ajaxUtils from '@js/core/utils/ajax'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { isObject, isPlainObject } from '@js/core/utils/type'; +import Store from '@ts/data/abstract_store'; + +import type { NormalizedDataSourceOptions } from './types'; + +export const CANCELED_TOKEN = 'canceled'; + +export type Mapper = (item: unknown) => unknown; + +interface GroupItem { + items?: unknown[]; +} + +export interface NormalizationOptions { + fromUrlLoadMode?: string; +} + +interface DataSourceOptionsInput { + store?: unknown; +} + +export const isPending = (deferred: DeferredObj): boolean => deferred.state() === 'pending'; + +export const normalizeStoreLoadOptionAccessorArguments = ( + originalArguments: unknown[], +): unknown => { + switch (originalArguments.length) { + case 0: + return undefined; + case 1: + return originalArguments[0]; + default: + return originalArguments.slice(); + } +}; + +const mapRecursive = (items: unknown, level: number, mapper: Mapper): unknown => { + if (!Array.isArray(items)) return items; + + if (!level) { + return items.map(mapper); + } + + return items.map((item) => { + const groupItem: GroupItem = isObject(item) ? item : {}; + + return { + ...groupItem, + items: mapRecursive(groupItem.items, level - 1, mapper), + }; + }); +}; + +export const mapDataRespectingGrouping = ( + items: unknown[], + mapper: Mapper, + groupInfo?: unknown, +): unknown[] => { + const level = groupInfo ? normalizeSortingInfo(groupInfo).length : 0; + const mapped = mapRecursive(items, level, mapper); + + return Array.isArray(mapped) ? mapped : []; +}; + +export interface NormalizedLoadResult { + data: unknown[]; + extra: unknown; +} + +export const normalizeLoadResult = (data: unknown, extra?: unknown): NormalizedLoadResult => { + const loadResult: { data?: unknown } = isObject(data) ? data : {}; + + const resultData: unknown = loadResult.data ? loadResult.data : data; + const resultExtra: unknown = loadResult.data ? data : extra; + + return { + data: Array.isArray(resultData) ? resultData : [resultData], + extra: resultExtra, + }; +}; + +const CUSTOM_STORE_OPTION_NAMES = [ + 'useDefaultSearch', 'key', 'load', 'loadMode', 'cacheRawData', 'byKey', + 'lookup', 'totalCount', 'insert', 'update', 'remove', +]; + +const createCustomStoreFromLoadFunc = (options: DataSourceOptionsInput): CustomStore => { + const storeConfig: Record = {}; + + CUSTOM_STORE_OPTION_NAMES.forEach((optionName) => { + storeConfig[optionName] = options[optionName]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete options[optionName]; + }); + + return new CustomStore(storeConfig); +}; + +const createStoreFromConfig = (storeConfig: Record): Store => { + const alias = String(storeConfig.type); + + delete storeConfig.type; + + const store: Store = Store.create(alias, storeConfig); + + return store; +}; + +const createCustomStoreFromUrl = ( + url: string, + normalizationOptions?: NormalizationOptions, +): CustomStore => new CustomStore({ + load: (): unknown => ajaxUtils.sendRequest({ url, dataType: 'json' }), + loadMode: normalizationOptions?.fromUrlLoadMode, +}); + +const resolveStore = (options: DataSourceOptionsInput): Store => { + if ('load' in options) { + return createCustomStoreFromLoadFunc(options); + } + + const { store } = options; + + if (Array.isArray(store)) { + return new ArrayStore(store); + } + if (isPlainObject(store)) { + return createStoreFromConfig(extend({}, store)); + } + + // Anything else is passed through the way it was before this module was typed: + // a value that is not a store fails later, in the data source itself. + // @ts-expect-error the `store` option is user-provided and is not necessarily a Store + const passedThrough: Store = store; + + return passedThrough; +}; + +export const normalizeDataSourceOptions = ( + options: unknown, + normalizationOptions?: NormalizationOptions, +): NormalizedDataSourceOptions => { + let source: unknown = options; + + if (typeof source === 'string') { + source = { + paginate: false, + store: createCustomStoreFromUrl(source, normalizationOptions), + }; + } + + if (source === undefined) { + source = []; + } + + const normalized: DataSourceOptionsInput = Array.isArray(source) || source instanceof Store + ? { store: source } + : extend({}, source); + + if (normalized.store === undefined) { + normalized.store = []; + } + + const store = resolveStore(normalized); + + return { + ...normalized, + store, + }; +}; diff --git a/packages/devextreme/js/__internal/data/endpoint_selector.ts b/packages/devextreme/js/__internal/data/endpoint_selector.ts new file mode 100644 index 000000000000..7726e93a3db4 --- /dev/null +++ b/packages/devextreme/js/__internal/data/endpoint_selector.ts @@ -0,0 +1,47 @@ +/* global Debug */ +import errors from '@js/core/errors'; +import { getWindow } from '@js/core/utils/window'; + +const window = getWindow(); + +interface Endpoint { + local: string; + production?: string; +} + +type EndpointConfig = Record; + +let isWinJsOrigin = false; +let isLocalOrigin = false; + +function isLocalHostName(url: string): boolean { + return /^(localhost$|127\.)/i.test(url); // TODO more precise check for 127.x.x.x IP +} + +class EndpointSelector { + config: EndpointConfig; + + constructor(config: EndpointConfig) { + this.config = config; + isWinJsOrigin = window.location.protocol === 'ms-appx:'; + isLocalOrigin = isLocalHostName(window.location.hostname); + } + + urlFor(key: string): string { + const bag = this.config[key]; + if (!bag) { + throw errors.Error('E0006'); + } + + if (bag.production) { + // @ts-expect-error `Debug` is a WinJS global that has no ambient declaration here + if ((isWinJsOrigin && !Debug.debuggerEnabled) || (!isWinJsOrigin && !isLocalOrigin)) { + return bag.production; + } + } + + return bag.local; + } +} + +export default EndpointSelector; diff --git a/packages/devextreme/js/__internal/data/m_errors.ts b/packages/devextreme/js/__internal/data/errors.ts similarity index 84% rename from packages/devextreme/js/__internal/data/m_errors.ts rename to packages/devextreme/js/__internal/data/errors.ts index 0268bd78af3d..076821979898 100644 --- a/packages/devextreme/js/__internal/data/m_errors.ts +++ b/packages/devextreme/js/__internal/data/errors.ts @@ -1,5 +1,6 @@ import coreErrors from '@js/core/errors'; import errorUtils from '@js/core/utils/error'; +import { isObject } from '@js/core/utils/type'; export const errors = errorUtils(coreErrors.ERROR_MESSAGES, { @@ -59,15 +60,20 @@ export const errors = errorUtils(coreErrors.ERROR_MESSAGES, { W4002: 'Data loading has failed for some cells due to the following error: {0}', }); + +export type DataErrorHandler = (error: unknown) => void; + // eslint-disable-next-line import/no-mutable-exports -export let errorHandler = null; -export const handleError = function (error) { +export let errorHandler: DataErrorHandler | null = null; + +export const handleError = function (error: unknown): void { /// #DEBUG - const id = error && '__id' in error ? error.__id : 'E4000'; + const id = isObject(error) && '__id' in error ? error.__id : 'E4000'; errors.log(id, error); /// #ENDDEBUG - // @ts-expect-error errorHandler?.(error); }; -// eslint-disable-next-line no-return-assign -export const setErrorHandler = (handler) => errorHandler = handler; + +export const setErrorHandler = (handler: DataErrorHandler | null): void => { + errorHandler = handler; +}; diff --git a/packages/devextreme/js/__internal/data/m_local_store.ts b/packages/devextreme/js/__internal/data/local_store.ts similarity index 61% rename from packages/devextreme/js/__internal/data/m_local_store.ts rename to packages/devextreme/js/__internal/data/local_store.ts index 14c956b01cd9..7bb8e4121ab8 100644 --- a/packages/devextreme/js/__internal/data/m_local_store.ts +++ b/packages/devextreme/js/__internal/data/local_store.ts @@ -3,14 +3,26 @@ import eventsEngine from '@js/common/core/events/core/events_engine'; import ArrayStore from '@js/common/data/array_store'; import { errors } from '@js/common/data/errors'; import domAdapter from '@js/core/dom_adapter'; +import type { DeferredObj } from '@js/core/utils/deferred'; import { getWindow } from '@js/core/utils/window'; +import type { ArrayStoreOptions } from '@ts/data/array_store'; import Store from './abstract_store'; const window = getWindow(); +export interface LocalStoreOptions extends ArrayStoreOptions { + name?: string; + immediate?: boolean; + flushInterval?: number; +} + +interface LocalStoreData { + _array: unknown[]; +} + class LocalStoreBackend { - _store: any; + _store: LocalStoreData; _dirty: boolean; @@ -18,7 +30,7 @@ class LocalStoreBackend { _key: string; - constructor(store, storeOptions) { + constructor(store: LocalStoreData, storeOptions: LocalStoreOptions) { this._store = store; this._dirty = !!storeOptions.data; @@ -31,14 +43,16 @@ class LocalStoreBackend { this.save(); - const immediate = this._immediate = storeOptions.immediate; + const immediate = storeOptions.immediate ?? false; + this._immediate = immediate; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const flushInterval = Math.max(100, storeOptions.flushInterval || 10 * 1000); if (!immediate) { const saveProxy = this.save.bind(this); setInterval(saveProxy, flushInterval); eventsEngine.on(window, 'beforeunload', saveProxy); - // @ts-expect-error + // @ts-expect-error `cordova` is injected by the Cordova container, `Window` has no such field if (window.cordova) { domAdapter.listen(domAdapter.getDocument(), 'pause', saveProxy, false); } @@ -66,16 +80,17 @@ class LocalStoreBackend { this._dirty = false; } - _loadImpl(): any { + _loadImpl(): unknown[] { const raw = window.localStorage.getItem(this._key); if (raw) { - return JSON.parse(raw); + const stored: unknown = JSON.parse(raw); + return Array.isArray(stored) ? stored : []; } return []; } - _saveImpl(array): void { + _saveImpl(array: unknown[]): void { if (!array.length) { window.localStorage.removeItem(this._key); } else { @@ -83,22 +98,21 @@ class LocalStoreBackend { } } } + class LocalStore extends ArrayStore { _backend: LocalStoreBackend; - _array: any; - - constructor(options) { - if (typeof options === 'string') { - options = { name: options }; - } else { - options = options || {}; - } + constructor(options?: LocalStoreOptions | string) { + const storeOptions: LocalStoreOptions = typeof options === 'string' + ? { name: options } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + : options || {}; - super(options); - this._array = options.data || []; + super(storeOptions); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this._array = storeOptions.data || []; - this._backend = new LocalStoreBackend(this, options); + this._backend = new LocalStoreBackend(this, storeOptions); this._backend.load(); } @@ -111,17 +125,17 @@ class LocalStore extends ArrayStore { this._backend.notifyChanged(); } - _insertImpl(values): any { + _insertImpl(values: unknown): DeferredObj { const b = this._backend; return super._insertImpl(values).done(b.notifyChanged.bind(b)); } - _updateImpl(key, values): any { + _updateImpl(key: unknown, values: unknown): DeferredObj { const b = this._backend; return super._updateImpl(key, values).done(b.notifyChanged.bind(b)); } - _removeImpl(key): any { + _removeImpl(key: unknown): DeferredObj { const b = this._backend; return super._removeImpl(key).done(b.notifyChanged.bind(b)); } diff --git a/packages/devextreme/js/__internal/data/m_array_utils.ts b/packages/devextreme/js/__internal/data/m_array_utils.ts deleted file mode 100644 index 22d11b7c9c69..000000000000 --- a/packages/devextreme/js/__internal/data/m_array_utils.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { errors } from '@js/common/data/errors'; -import { keysEqual, rejectedPromise, trivialPromise } from '@js/common/data/utils'; -import config from '@js/core/config'; -import Guid from '@js/core/guid'; -import { compileGetter } from '@js/core/utils/data'; -import { extend } from '@js/core/utils/extend'; -import { deepExtendArraySafe } from '@js/core/utils/object'; -import { - isDefined, isEmptyObject, isObject, isPlainObject, -} from '@js/core/utils/type'; -import { isCollectionLike } from '@ts/core/utils/m_object'; - -function hasKey(target, keyOrKeys) { - let key; - // @ts-expect-error - const keys = typeof keyOrKeys === 'string' ? keyOrKeys.split() : keyOrKeys.slice(); - - while (keys.length) { - key = keys.shift(); - if (key in target) { - return true; - } - } - - return false; -} - -function findItems(keyInfo, items, key, groupCount) { - let childItems; - let result; - - if (groupCount) { - for (let i = 0; i < items.length; i++) { - childItems = items[i].items || items[i].collapsedItems || []; - result = findItems(keyInfo, childItems || [], key, groupCount - 1); - if (result) { - return result; - } - } - } else if (indexByKey(keyInfo, items, key) >= 0) { - return items; - } -} - -function getItems(keyInfo, items, key, groupCount) { - if (groupCount) { - return findItems(keyInfo, items, key, groupCount) || []; - } - - return items; -} - -function generateDataByKeyMap(keyInfo, array) { - if (keyInfo.key() && (!array._dataByKeyMap || array._dataByKeyMapLength !== array.length)) { - const dataByKeyMap = {}; - const arrayLength = array.length; - for (let i = 0; i < arrayLength; i++) { - dataByKeyMap[JSON.stringify(keyInfo.keyOf(array[i]))] = array[i]; - } - - array._dataByKeyMap = dataByKeyMap; - array._dataByKeyMapLength = arrayLength; - } -} - -function getCacheValue(array, key) { - if (array._dataByKeyMap) { - return array._dataByKeyMap[JSON.stringify(key)]; - } -} - -function getHasKeyCacheValue(array, key) { - if (array._dataByKeyMap) { - return array._dataByKeyMap[JSON.stringify(key)]; - } - - return true; -} - -function setDataByKeyMapValue(array, key, data) { - if (array._dataByKeyMap) { - array._dataByKeyMap[JSON.stringify(key)] = data; - array._dataByKeyMapLength += data ? 1 : -1; - } -} - -function cloneInstanceWithChangedPaths(instance, changes, clonedInstances) { - if (isCollectionLike(instance)) { - return instance; - } - - clonedInstances = clonedInstances || new WeakMap(); - - const result = instance ? Object.create(Object.getPrototypeOf(instance)) : {}; - if (instance) { - clonedInstances.set(instance, result); - } - - const instanceWithoutPrototype = { ...instance }; - deepExtendArraySafe(result, instanceWithoutPrototype, true, true, true); - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const name in instanceWithoutPrototype) { - const value = instanceWithoutPrototype[name]; - const change = changes?.[name]; - - if (isObject(value) && !isPlainObject(value) && isObject(change) && !clonedInstances.has(value)) { - result[name] = cloneInstanceWithChangedPaths(value, change, clonedInstances); - } - } - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const name in result) { - const prop = result[name]; - - if (isObject(prop) && clonedInstances.has(prop)) { - result[name] = clonedInstances.get(prop); - } - } - - return result; -} - -function createObjectWithChanges(target, changes) { - // @ts-expect-error - const result = cloneInstanceWithChangedPaths(target, changes); - - return deepExtendArraySafe(result, changes, true, true, true); -} - -function applyBatch({ - keyInfo, data, changes, groupCount, useInsertIndex, immutable, disableCache, logError, skipCopying, -}) { - const resultItems = immutable === true ? [...data] : data; - - changes.forEach((item) => { - const items = item.type === 'insert' ? resultItems : getItems(keyInfo, resultItems, item.key, groupCount); - - !disableCache && generateDataByKeyMap(keyInfo, items); - // eslint-disable-next-line default-case - switch (item.type) { - case 'update': update(keyInfo, items, item.key, item.data, true, immutable, logError); break; - case 'insert': insert(keyInfo, items, item.data, useInsertIndex && isDefined(item.index) ? item.index : -1, true, logError, skipCopying); break; - case 'remove': remove(keyInfo, items, item.key, true, logError); break; - } - }); - return resultItems; -} - -function getErrorResult(isBatch, logError, errorCode) { - // @ts-expect-error - return !isBatch ? rejectedPromise(errors.Error(errorCode)) : logError && errors.log(errorCode); -} - -function applyChanges(data, changes, options = {}) { - // @ts-expect-error - const { keyExpr = 'id', immutable = true } = options; - const keyGetter = compileGetter(keyExpr); - const keyInfo = { - key: () => keyExpr, - // @ts-expect-error - keyOf: (obj) => keyGetter(obj), - }; - // @ts-expect-error - return applyBatch({ - keyInfo, - data, - changes, - immutable, - disableCache: true, - logError: true, - }); -} - -function update(keyInfo, array, key, data, isBatch, immutable, logError) { - let target; - const extendComplexObject = true; - const keyExpr = keyInfo.key(); - - if (keyExpr) { - if (hasKey(data, keyExpr) && !keysEqual(keyExpr, key, keyInfo.keyOf(data))) { - return getErrorResult(isBatch, logError, 'E4017'); - } - - target = getCacheValue(array, key); - if (!target) { - const index = indexByKey(keyInfo, array, key); - if (index < 0) { - return getErrorResult(isBatch, logError, 'E4009'); - } - - target = array[index]; - - if (immutable === true && isDefined(target)) { - const newTarget = createObjectWithChanges(target, data); - array[index] = newTarget; - // @ts-expect-error - return !isBatch && trivialPromise(newTarget, key); - } - } - } else { - target = key; - } - - deepExtendArraySafe(target, data, extendComplexObject, false, true, true); - if (!isBatch) { - if (config().useLegacyStoreResult) { - // @ts-expect-error - return trivialPromise(key, data); - } - // @ts-expect-error - return trivialPromise(target, key); - } -} - -function insert(keyInfo, array, data, index, isBatch, logError, skipCopying) { - let keyValue; - const keyExpr = keyInfo.key(); - - const obj = isPlainObject(data) && !skipCopying ? extend({}, data) : data; - - if (keyExpr) { - keyValue = keyInfo.keyOf(obj); - if (keyValue === undefined || typeof keyValue === 'object' && isEmptyObject(keyValue)) { - if (Array.isArray(keyExpr)) { - throw errors.Error('E4007'); - } - keyValue = obj[keyExpr] = String(new Guid()); - } else if (array[indexByKey(keyInfo, array, keyValue)] !== undefined) { - return getErrorResult(isBatch, logError, 'E4008'); - } - } else { - keyValue = obj; - } - if (index >= 0) { - array.splice(index, 0, obj); - } else { - array.push(obj); - } - - setDataByKeyMapValue(array, keyValue, obj); - - if (!isBatch) { - // @ts-expect-error - return trivialPromise(config().useLegacyStoreResult ? data : obj, keyValue); - } -} - -function remove(keyInfo, array, key, isBatch, logError) { - const index = indexByKey(keyInfo, array, key); - if (index > -1) { - array.splice(index, 1); - setDataByKeyMapValue(array, key, null); - } - if (!isBatch) { - // @ts-expect-error - return trivialPromise(key); - } if (index < 0) { - return getErrorResult(isBatch, logError, 'E4009'); - } -} - -function indexByKey(keyInfo, array, key) { - const keyExpr = keyInfo.key(); - - if (!getHasKeyCacheValue(array, key)) { - return -1; - } - - for (let i = 0, arrayLength = array.length; i < arrayLength; i++) { - if (keysEqual(keyExpr, keyInfo.keyOf(array[i]), key)) { - return i; - } - } - return -1; -} - -export { - applyBatch, - applyChanges, - createObjectWithChanges, - indexByKey, - insert, - remove, - update, -}; diff --git a/packages/devextreme/js/__internal/data/m_endpoint_selector.ts b/packages/devextreme/js/__internal/data/m_endpoint_selector.ts deleted file mode 100644 index 22f200d13ddd..000000000000 --- a/packages/devextreme/js/__internal/data/m_endpoint_selector.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* global Debug */ -import errors from '@js/core/errors'; -import { getWindow } from '@js/core/utils/window'; - -const window = getWindow(); - -let IS_WINJS_ORIGIN; -let IS_LOCAL_ORIGIN; - -function isLocalHostName(url) { - return /^(localhost$|127\.)/i.test(url); // TODO more precise check for 127.x.x.x IP -} - -const EndpointSelector = function (config) { - this.config = config; - IS_WINJS_ORIGIN = window.location.protocol === 'ms-appx:'; - IS_LOCAL_ORIGIN = isLocalHostName(window.location.hostname); -}; - -EndpointSelector.prototype = { - urlFor(key) { - const bag = this.config[key]; - if (!bag) { - throw errors.Error('E0006'); - } - - if (bag.production) { - // @ts-expect-error - if (IS_WINJS_ORIGIN && !Debug.debuggerEnabled || !IS_WINJS_ORIGIN && !IS_LOCAL_ORIGIN) { - return bag.production; - } - } - - return bag.local; - }, - -}; - -export default EndpointSelector; diff --git a/packages/devextreme/js/__internal/data/m_query.ts b/packages/devextreme/js/__internal/data/m_query.ts deleted file mode 100644 index c35f346ae08c..000000000000 --- a/packages/devextreme/js/__internal/data/m_query.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { queryImpl } from '@js/common/data/query_implementation'; - -const query = function () { - const impl = Array.isArray(arguments[0]) ? 'array' : 'remote'; - // @ts-expect-error - return queryImpl[impl].apply(this, arguments); -}; - -export default query; diff --git a/packages/devextreme/js/__internal/data/m_remote_query.ts b/packages/devextreme/js/__internal/data/m_remote_query.ts deleted file mode 100644 index 53fb79c54c50..000000000000 --- a/packages/devextreme/js/__internal/data/m_remote_query.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import arrayQueryImpl from '@js/common/data/array_query'; -import { errors, handleError } from '@js/common/data/errors'; -import queryAdapters from '@js/common/data/query_adapters'; -import { Deferred } from '@js/core/utils/deferred'; -import { each } from '@js/core/utils/iterator'; -import { isFunction } from '@js/core/utils/type'; - -const remoteQueryImpl = function (url, queryOptions, tasks) { - tasks = tasks || []; - queryOptions = queryOptions || {}; - - const createTask = function (name, args) { - return { name, args }; - }; - - const exec = function (executorTask) { - // @ts-expect-error - const d = new Deferred(); - let _adapterFactory; - let _adapter; - let _taskQueue; - let _currentTask; - let _mergedSortArgs; - - const rejectWithNotify = function (error) { - const handler = queryOptions.errorHandler; - if (handler) { - handler(error); - } - - handleError(error); - d.reject(error); - }; - - function mergeSortTask(task) { - // eslint-disable-next-line default-case - switch (task.name) { - case 'sortBy': - _mergedSortArgs = [task.args]; - return true; - - case 'thenBy': - if (!_mergedSortArgs) { - throw errors.Error('E4004'); - } - - _mergedSortArgs.push(task.args); - return true; - } - - return false; - } - - function unmergeSortTasks() { - const head = _taskQueue[0]; - const unmergedTasks = []; - - if (head && head.name === 'multiSort') { - _taskQueue.shift(); - each(head.args[0], function () { - // @ts-expect-error - unmergedTasks.push(createTask(unmergedTasks.length ? 'thenBy' : 'sortBy', this)); - }); - } - - _taskQueue = unmergedTasks.concat(_taskQueue); - } - - try { - _adapterFactory = queryOptions.adapter; - if (!isFunction(_adapterFactory)) { - _adapterFactory = queryAdapters[_adapterFactory]; - } - - _adapter = _adapterFactory(queryOptions); - - _taskQueue = [].concat(tasks).concat(executorTask); - - const { optimize } = _adapter; - if (optimize) optimize(_taskQueue); - - while (_taskQueue.length) { - /* eslint-disable-next-line prefer-destructuring */ - _currentTask = _taskQueue[0]; - - if (!mergeSortTask(_currentTask)) { - if (_mergedSortArgs) { - _taskQueue.unshift(createTask('multiSort', [_mergedSortArgs])); - _mergedSortArgs = null; - continue; - } - - if (String(_currentTask.name) !== 'enumerate') { - if (!_adapter[_currentTask.name] || _adapter[_currentTask.name].apply(_adapter, _currentTask.args) === false) { - break; - } - } - } - _taskQueue.shift(); - } - - unmergeSortTasks(); - - _adapter.exec(url) - .done((result, extra) => { - if (!_taskQueue.length) { - d.resolve(result, extra); - } else { - let clientChain = arrayQueryImpl(result, { - errorHandler: queryOptions.errorHandler, - }); - each(_taskQueue, function () { - clientChain = clientChain[this.name].apply(clientChain, this.args); - }); - clientChain - // @ts-expect-error - .done(d.resolve) - .fail(d.reject); - } - }) - .fail(rejectWithNotify); - } catch (x) { - rejectWithNotify(x); - } - - return d.promise(); - }; - - const query = {}; - - each( - ['sortBy', 'thenBy', 'filter', 'slice', 'select', 'groupBy'], - function () { - const name = String(this); - query[name] = function () { - return remoteQueryImpl(url, queryOptions, tasks.concat(createTask(name, arguments))); - }; - }, - ); - - each( - ['count', 'min', 'max', 'sum', 'avg', 'aggregate', 'enumerate'], - function () { - const name = String(this); - query[name] = function () { - return exec.call(this, createTask(name, arguments)); - }; - }, - ); - - return query; -}; - -export default remoteQueryImpl; diff --git a/packages/devextreme/js/__internal/data/m_store_helper.ts b/packages/devextreme/js/__internal/data/m_store_helper.ts deleted file mode 100644 index 9c30c9243f73..000000000000 --- a/packages/devextreme/js/__internal/data/m_store_helper.ts +++ /dev/null @@ -1,89 +0,0 @@ -import arrayQuery from '@js/common/data/array_query'; -import { normalizeSortingInfo } from '@js/common/data/utils'; -// @ts-expect-error -import { grep } from '@js/core/utils/common'; -import { extend } from '@js/core/utils/extend'; -import { each } from '@js/core/utils/iterator'; - -function multiLevelGroup(query, groupInfo) { - query = query.groupBy(groupInfo[0].selector); - - if (groupInfo.length > 1) { - query = query.select((g) => extend({}, g, { - items: multiLevelGroup(arrayQuery(g.items), groupInfo.slice(1)).toArray(), - })); - } - - return query; -} - -function arrangeSortingInfo(groupInfo, sortInfo) { - const filteredGroup = []; - each(groupInfo, (_, group) => { - const collision = grep(sortInfo, (sort) => group.selector === sort.selector); - - if (collision.length < 1) { - // @ts-expect-error - filteredGroup.push(group); - } - }); - return filteredGroup.concat(sortInfo); -} - -function queryByOptions(query, options, isCountQuery) { - options = options || {}; - - const { filter } = options; - - if (options?.langParams) { - query.setLangParams?.(options.langParams); - } - - if (filter) { - query = query.filter(filter); - } - - if (isCountQuery) { - return query; - } - - let { sort } = options; - const { select } = options; - let { group } = options; - const { skip } = options; - const { take } = options; - - if (group) { - group = normalizeSortingInfo(group); - group.keepInitialKeyOrder = !!options.group.keepInitialKeyOrder; - } - if (sort || group) { - sort = normalizeSortingInfo(sort || []); - if (group && !group.keepInitialKeyOrder) { - sort = arrangeSortingInfo(group, sort); - } - each(sort, function (index) { - query = query[index ? 'thenBy' : 'sortBy'](this.selector, this.desc, this.compare); - }); - } - - if (select) { - query = query.select(select); - } - - if (group) { - query = multiLevelGroup(query, group); - } - - if (take || skip) { - query = query.slice(skip || 0, take); - } - - return query; -} - -export default { - multiLevelGroup, - arrangeSortingInfo, - queryByOptions, -}; diff --git a/packages/devextreme/js/__internal/data/m_utils.ts b/packages/devextreme/js/__internal/data/m_utils.ts deleted file mode 100644 index ea0a65a494d8..000000000000 --- a/packages/devextreme/js/__internal/data/m_utils.ts +++ /dev/null @@ -1,304 +0,0 @@ -/* eslint-disable spellcheck/spell-checker */ -import domAdapter from '@js/core/dom_adapter'; -import { equalByValue } from '@js/core/utils/common'; -import { Deferred } from '@js/core/utils/deferred'; -import { map } from '@js/core/utils/iterator'; -import readyCallbacks from '@js/core/utils/ready_callbacks'; -import { isFunction } from '@js/core/utils/type'; -import { getWindow } from '@js/core/utils/window'; - -const ready = readyCallbacks.add; - -export const XHR_ERROR_UNLOAD = 'DEVEXTREME_XHR_ERROR_UNLOAD'; - -export const normalizeBinaryCriterion = function (crit) { - return [ - crit[0], - crit.length < 3 ? '=' : String(crit[1]).toLowerCase(), - crit.length < 2 ? true : crit[crit.length - 1], - ]; -}; - -export const normalizeSortingInfo = function (info) { - if (!Array.isArray(info)) { - info = [info]; - } - - return map(info, (i) => { - const result = { - selector: isFunction(i) || typeof i === 'string' ? i : i.getter || i.field || i.selector, - desc: !!(i.desc || String(i.dir).charAt(0).toLowerCase() === 'd'), - }; - if (i.compare) { - // @ts-expect-error - result.compare = i.compare; - } - return result; - }); -}; - -export const errorMessageFromXhr = (function () { - const textStatusMessages = { - timeout: 'Network connection timeout', - error: 'Unspecified network error', - parsererror: 'Unexpected server response', - }; - - /// #DEBUG - const textStatusDetails = { - timeout: 'possible causes: the remote host is not accessible, overloaded or is not included into the domain white-list when being run in the native container', - error: 'if the remote host is located on another domain, make sure it properly supports cross-origin resource sharing (CORS), or use the JSONP approach instead', - parsererror: 'the remote host did not respond with valid JSON data', - }; - /// #ENDDEBUG - - const explainTextStatus = function (textStatus) { - let result = textStatusMessages[textStatus]; - - if (!result) { - return textStatus; - } - - /// #DEBUG - result += ` (${textStatusDetails[textStatus]})`; - /// #ENDDEBUG - - return result; - }; - - // T542570, https://stackoverflow.com/a/18170879 - let unloading; - ready(() => { - const window = getWindow(); - domAdapter.listen(window, 'beforeunload', () => { unloading = true; }); - }); - - return function (xhr, textStatus) { - if (unloading) { - return XHR_ERROR_UNLOAD; - } - if (xhr.status < 400) { - return explainTextStatus(textStatus); - } - return xhr.statusText; - }; -}()); - -export const aggregators = { - count: { - seed: 0, - step(count) { return 1 + count; }, - }, - sum: { - seed: 0, - step(sum, item) { return sum + item; }, - }, - min: { - step(min, item) { return item < min ? item : min; }, - }, - max: { - step(max, item) { return item > max ? item : max; }, - }, - avg: { - seed: [0, 0], - step(pair, value) { - return [pair[0] + value, pair[1] + 1]; - }, - finalize(pair) { - return pair[1] ? pair[0] / pair[1] : NaN; - }, - }, -}; - -export const processRequestResultLock = (function () { - let lockCount = 0; - let lockDeferred; - - const obtain = function () { - if (lockCount === 0) { - // @ts-expect-error - lockDeferred = new Deferred(); - } - lockCount++; - }; - - const release = function () { - lockCount--; - if (lockCount < 1) { - lockDeferred.resolve(); - } - }; - - const promise = function () { - // @ts-expect-error - const deferred = lockCount === 0 ? new Deferred().resolve() : lockDeferred; - return deferred.promise(); - }; - - const reset = function () { - lockCount = 0; - if (lockDeferred) { - lockDeferred.resolve(); - } - }; - - return { - obtain, - release, - promise, - reset, - }; -}()); - -export function isDisjunctiveOperator(condition) { - return /^(or|\|\||\|)$/i.test(condition); -} - -export function isConjunctiveOperator(condition) { - return /^(and|&&|&)$/i.test(condition); -} - -export const keysEqual = function (keyExpr, key1, key2) { - if (Array.isArray(keyExpr)) { - const names = map(key1, (v, k) => k); - let name; - for (let i = 0; i < names.length; i++) { - name = names[i]; - if (!equalByValue(key1[name], key2[name], { strict: false })) { - return false; - } - } - return true; - } - - return equalByValue(key1, key2, { strict: false }); -}; - -const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const base64_encode = function (input) { - if (!Array.isArray(input)) { - input = stringToByteArray(String(input)); - } - - let result = ''; - - function getBase64Char(index) { - return BASE64_CHARS.charAt(index); - } - - for (let i = 0; i < input.length; i += 3) { - const octet1 = input[i]; - const octet2 = input[i + 1]; - const octet3 = input[i + 2]; - - result += map( - [ - octet1 >> 2, - ((octet1 & 3) << 4) | octet2 >> 4, - isNaN(octet2) ? 64 : ((octet2 & 15) << 2) | octet3 >> 6, - isNaN(octet3) ? 64 : octet3 & 63, - ], - getBase64Char, - ).join(''); - } - - return result; -}; - -function stringToByteArray(str) { - const bytes: number[] = []; - let code: number; - let i; - - for (i = 0; i < str.length; i++) { - code = str.charCodeAt(i); - - if (code < 128) { - bytes.push(code); - } else if (code < 2048) { - bytes.push(192 + (code >> 6), 128 + (code & 63)); - } else if (code < 65536) { - bytes.push(224 + (code >> 12), 128 + ((code >> 6) & 63), 128 + (code & 63)); - } else if (code < 2097152) { - bytes.push(240 + (code >> 18), 128 + ((code >> 12) & 63), 128 + ((code >> 6) & 63), 128 + (code & 63)); - } - } - return bytes; -} - -export const isUnaryOperation = function (crit) { - return crit[0] === '!' && Array.isArray(crit[1]); -}; - -const isGroupOperator = function (value) { - return value === 'and' || value === 'or'; -}; - -export const isUniformEqualsByOr = function (crit) { - if (crit.length > 2 && Array.isArray(crit[0]) && crit[1] === 'or' && typeof crit[0][0] === 'string' && crit[0][1] === '=') { - const [prop] = crit[0]; - return !crit.find((el, i) => (i % 2 !== 0 ? el !== 'or' - : !Array.isArray(el) || el.length !== 3 || el[0] !== prop || el[1] !== '=')); - } - return false; -}; - -export const isGroupCriterion = function (crit) { - const first = crit[0]; - const second = crit[1]; - - if (Array.isArray(first)) { - return true; - } - if (isFunction(first)) { - if (Array.isArray(second) || isFunction(second) || isGroupOperator(second)) { - return true; - } - } - - return false; -}; - -export const trivialPromise = function () { - // @ts-expect-error - const d = new Deferred(); - return d.resolve.apply(d, arguments).promise(); -}; - -export const rejectedPromise = function () { - // @ts-expect-error - const d = new Deferred(); - return d.reject.apply(d, arguments).promise(); -}; - -function throttle(func, timeout) { - let timeoutId; - return function () { - if (!timeoutId) { - timeoutId = setTimeout(() => { - timeoutId = undefined; - func.call(this); - }, isFunction(timeout) ? timeout() : timeout); - } - return timeoutId; - }; -} - -export function throttleChanges(func, timeout) { - let cache = []; - const throttled = throttle(function () { - func.call(this, cache); - cache = []; - }, timeout); - - return function (changes) { - if (Array.isArray(changes)) { - // @ts-expect-error - cache.push(...changes); - } - // @ts-expect-error - return throttled.call(this, cache); - }; -} diff --git a/packages/devextreme/js/__internal/data/odata/context.ts b/packages/devextreme/js/__internal/data/odata/context.ts index 6e070b4c937c..dbd3dadbde42 100644 --- a/packages/devextreme/js/__internal/data/odata/context.ts +++ b/packages/devextreme/js/__internal/data/odata/context.ts @@ -9,7 +9,7 @@ import { each } from '@js/core/utils/iterator'; import { isDefined, isPlainObject } from '@js/core/utils/type'; import type { StoreErrorHandler } from '@ts/data/abstract_store'; -import { errors, handleError } from '../m_errors'; +import { errors, handleError } from '../errors'; import { escapeServiceOperationParams, formatFunctionInvocationUrl } from './utils'; export type ServiceOperationParams = Record; diff --git a/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts b/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts deleted file mode 100644 index 6205484aa0a9..000000000000 --- a/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts +++ /dev/null @@ -1,363 +0,0 @@ -import queryAdapters from '@js/common/data/query_adapters'; -import config from '@js/core/config'; -import { extend } from '@js/core/utils/extend'; -import { each } from '@js/core/utils/iterator'; -import { isFunction } from '@js/core/utils/type'; - -import { errors } from '../m_errors'; -import { - isConjunctiveOperator, - isUnaryOperation, - normalizeBinaryCriterion, -} from '../m_utils'; -import { - convertPrimitiveValue, - generateExpand, - generateSelect, - sendRequest, - serializePropName, - serializeValue, -} from './utils'; - -const DEFAULT_PROTOCOL_VERSION = 4; -const STRING_FUNCTIONS = ['contains', 'notcontains', 'startswith', 'endswith']; - -const compileCriteria = (() => { - let protocolVersion; - let forceLowerCase; - let fieldTypes; - - const createBinaryOperationFormatter = (op) => (prop, val) => `${prop} ${op} ${val}`; - - const createStringFuncFormatter = (op, reverse) => (prop, val) => { - const bag = [op, '(']; - - if (forceLowerCase) { - prop = prop.indexOf('tolower(') === -1 ? `tolower(${prop})` : prop; - val = val.toLowerCase(); - } - - if (reverse) { - bag.push(val, ',', prop); - } else { - bag.push(prop, ',', val); - } - - bag.push(')'); - return bag.join(''); - }; - - const isStringFunction = function (name) { - return STRING_FUNCTIONS.some((funcName) => funcName === name); - }; - - const formatters = { - '=': createBinaryOperationFormatter('eq'), - '<>': createBinaryOperationFormatter('ne'), - '>': createBinaryOperationFormatter('gt'), - '>=': createBinaryOperationFormatter('ge'), - '<': createBinaryOperationFormatter('lt'), - '<=': createBinaryOperationFormatter('le'), - // @ts-expect-error - startswith: createStringFuncFormatter('startswith'), - // @ts-expect-error - endswith: createStringFuncFormatter('endswith'), - }; - - const formattersV2 = extend({}, formatters, { - /* eslint-disable spellcheck/spell-checker */ - contains: createStringFuncFormatter('substringof', true), - notcontains: createStringFuncFormatter('not substringof', true), - }); - - const formattersV4 = extend({}, formatters, { - // @ts-expect-error - contains: createStringFuncFormatter('contains'), - // @ts-expect-error - notcontains: createStringFuncFormatter('not contains'), - }); - - const compileBinary = (criteria) => { - criteria = normalizeBinaryCriterion(criteria); - - const op = criteria[1]; - const fieldName = criteria[0]; - const fieldType = fieldTypes && fieldTypes[fieldName]; - - if (fieldType && isStringFunction(op) && fieldType !== 'String') { - // @ts-expect-error - throw new errors.Error('E4024', op, fieldName, fieldType); - } - - const formatters = protocolVersion === 4 - ? formattersV4 - : formattersV2; - const formatter = formatters[op.toLowerCase()]; - - if (!formatter) { - throw errors.Error('E4003', op); - } - - let value = criteria[2]; - - if (fieldTypes?.[fieldName]) { - value = convertPrimitiveValue(fieldTypes[fieldName], value); - } - - return formatter( - serializePropName(fieldName), - serializeValue(value, protocolVersion, fieldTypes?.[fieldName]), - ); - }; - - const compileUnary = (criteria) => { - const op = criteria[0]; - const crit = compileCore(criteria[1]); - - if (op === '!') { - return `not (${crit})`; - } - - throw errors.Error('E4003', op); - }; - - const compileGroup = (criteria) => { - const bag = []; - let groupOperator; - let nextGroupOperator; - - each(criteria, function (index, criterion) { - if (Array.isArray(criterion)) { - if (bag.length > 1 && groupOperator !== nextGroupOperator) { - // @ts-expect-error - throw new errors.Error('E4019'); - } - // @ts-expect-error - bag.push(`(${compileCore(criterion)})`); - - groupOperator = nextGroupOperator; - nextGroupOperator = 'and'; - } else { - nextGroupOperator = isConjunctiveOperator(this) ? 'and' : 'or'; - } - }); - - return bag.join(` ${groupOperator} `); - }; - - const compileCore = (criteria) => { - if (Array.isArray(criteria[0])) { - return compileGroup(criteria); - } - - if (isUnaryOperation(criteria)) { - return compileUnary(criteria); - } - - return compileBinary(criteria); - }; - - return (criteria, version, types, filterToLower) => { - fieldTypes = types; - forceLowerCase = filterToLower ?? config().oDataFilterToLower; - protocolVersion = version; - - return compileCore(criteria); - }; -})(); - -const createODataQueryAdapter = (queryOptions) => { - /* eslint-disable @typescript-eslint/naming-convention */ - let _sorting = []; - const _criteria = []; - const _expand = queryOptions.expand; - let _select; - let _skip; - let _take; - let _countQuery; - - const _oDataVersion = queryOptions.version || DEFAULT_PROTOCOL_VERSION; - - const hasSlice = () => _skip || _take !== undefined; - - const hasFunction = (criterion) => { - for (let i = 0; i < criterion.length; i++) { - if (isFunction(criterion[i])) { - return true; - } - - if (Array.isArray(criterion[i]) && hasFunction(criterion[i])) { - return true; - } - } - return false; - }; - - const requestData = () => { - const result = {}; - - if (!_countQuery) { - if (_sorting.length) { - // @ts-expect-error - result.$orderby = _sorting.join(','); - } - if (_skip) { - // @ts-expect-error - result.$skip = _skip; - } - if (_take !== undefined) { - // @ts-expect-error - result.$top = _take; - } - // @ts-expect-error - result.$select = generateSelect(_oDataVersion, _select) || undefined; - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - result.$expand = generateExpand(_oDataVersion, _expand, _select) || undefined; - } - - if (_criteria.length) { - const criteria = _criteria.length < 2 ? _criteria[0] : _criteria; - const fieldTypes = queryOptions?.fieldTypes; - const filterToLower = queryOptions?.filterToLower; - // @ts-expect-error - result.$filter = compileCriteria(criteria, _oDataVersion, fieldTypes, filterToLower); - } - - if (_countQuery) { - // @ts-expect-error - result.$top = 0; - } - - if (queryOptions.requireTotalCount || _countQuery) { - // todo: tests!!! - if (_oDataVersion !== 4) { - // @ts-expect-error - result.$inlinecount = 'allpages'; - } else { - // @ts-expect-error - result.$count = 'true'; - } - } - - return result; - }; - - const tryLiftSelect = (tasks) => { - let selectIndex = -1; - for (let i = 0; i < tasks.length; i++) { - if (tasks[i].name === 'select') { - selectIndex = i; - break; - } - } - - if (selectIndex < 0 || !isFunction(tasks[selectIndex].args[0])) return; - - const nextTask = tasks[1 + selectIndex]; - if (!nextTask || nextTask.name !== 'slice') return; - - tasks[1 + selectIndex] = tasks[selectIndex]; - tasks[selectIndex] = nextTask; - }; - - return { - - optimize: tryLiftSelect, - - exec(url) { - return sendRequest( - _oDataVersion, - { - url, - params: extend(requestData(), queryOptions?.params), - }, - { - beforeSend: queryOptions.beforeSend, - jsonp: queryOptions.jsonp, - withCredentials: queryOptions.withCredentials, - countOnly: _countQuery, - processDatesAsUtc: queryOptions.processDatesAsUtc, - fieldTypes: queryOptions.fieldTypes, - isPaged: isFinite(_take), - }, - ); - }, - /* eslint-disable @typescript-eslint/no-invalid-void-type */ - multiSort(args): boolean | void { - let rules; - - if (hasSlice()) { - return false; - } - - for (let i = 0; i < args.length; i++) { - const getter = args[i][0]; - const desc = !!args[i][1]; - let rule; - - if (typeof getter !== 'string') { - return false; - } - - rule = serializePropName(getter); - if (desc) { - rule += ' desc'; - } - - rules = rules || []; - rules.push(rule); - } - - _sorting = rules; - }, - - slice(skipCount, takeCount): boolean | void { - if (hasSlice()) { - return false; - } - - _skip = skipCount; - _take = takeCount; - }, - - filter(criterion): boolean | void { - if (hasSlice()) { - return false; - } - - if (!Array.isArray(criterion)) { - criterion = [].slice.call(arguments); - } - - if (hasFunction(criterion)) { - return false; - } - - if (_criteria.length) { - // @ts-expect-error - _criteria.push('and'); - } - // @ts-expect-error - _criteria.push(criterion); - }, - - select(expr): boolean | void { - if (_select || isFunction(expr)) { - return false; - } - - if (!Array.isArray(expr)) { - expr = [].slice.call(arguments); - } - - _select = expr; - }, - // eslint-disable-next-line no-return-assign - count: () => _countQuery = true, - }; -}; - -queryAdapters.odata = createODataQueryAdapter; - -export const odata = createODataQueryAdapter; diff --git a/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts b/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts deleted file mode 100644 index e37f10217123..000000000000 --- a/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts +++ /dev/null @@ -1,72 +0,0 @@ -import '@js/common/data/odata/query_adapter'; - -import { sendRequest } from '@js/common/data/odata/utils'; - -const DEFAULT_PROTOCOL_VERSION = 4; - -export default class RequestDispatcher { - constructor(options) { - options = options || {}; - // @ts-expect-error - this._url = String(options.url).replace(/\/+$/, ''); - // @ts-expect-error - this._beforeSend = options.beforeSend; - // @ts-expect-error - this._jsonp = options.jsonp; - // @ts-expect-error - this._version = options.version || DEFAULT_PROTOCOL_VERSION; - // @ts-expect-error - this._withCredentials = options.withCredentials; - // @ts-expect-error - this._processDatesAsUtc = options.processDatesAsUtc ?? options.deserializeDates ?? false; - // @ts-expect-error - this._filterToLower = options.filterToLower; - } - - sendRequest(url, method, params, payload) { - return sendRequest( - this.version, - { - url, - method, - params: params || {}, - payload, - }, - { - // @ts-expect-error - beforeSend: this._beforeSend, - // @ts-expect-error - jsonp: this._jsonp, - // @ts-expect-error - withCredentials: this._withCredentials, - // @ts-expect-error - processDatesAsUtc: this._processDatesAsUtc, - }, - ); - } - - get version() { - // @ts-expect-error - return this._version; - } - - get beforeSend() { - // @ts-expect-error - return this._beforeSend; - } - - get url() { - // @ts-expect-error - return this._url; - } - - get jsonp() { - // @ts-expect-error - return this._jsonp; - } - - get filterToLower() { - // @ts-expect-error - return this._filterToLower; - } -} diff --git a/packages/devextreme/js/__internal/data/odata/query_adapter.ts b/packages/devextreme/js/__internal/data/odata/query_adapter.ts new file mode 100644 index 000000000000..932de8a1bef3 --- /dev/null +++ b/packages/devextreme/js/__internal/data/odata/query_adapter.ts @@ -0,0 +1,367 @@ +import queryAdapters from '@js/common/data/query_adapters'; +import config from '@js/core/config'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { extend } from '@js/core/utils/extend'; +import { isFunction, isString } from '@js/core/utils/type'; +import type { QueryAdapter, RemoteQueryOptions, RemoteTask } from '@ts/data/remote_query'; + +import { errors } from '../errors'; +import { + isConjunctiveOperator, + isUnaryOperation, + normalizeBinaryCriterion, +} from '../utils'; +import { + convertPrimitiveValue, + EdmLiteral, + generateExpand, + generateSelect, + sendRequest, + serializePropName, + serializeValue, +} from './utils'; + +const DEFAULT_PROTOCOL_VERSION = 4; +const STRING_FUNCTIONS = ['contains', 'notcontains', 'startswith', 'endswith']; + +type Formatter = (prop: string, val: string) => string; + +type FieldTypes = Record; + +interface ODataRequestParams { + [param: string]: unknown; + $orderby?: string; + $skip?: number; + $top?: number; + $select?: string; + $expand?: string; + $filter?: string; + // eslint-disable-next-line spellcheck/spell-checker + $inlinecount?: string; + $count?: string; +} + +const compileCriteria = (() => { + // eslint-disable-next-line @typescript-eslint/init-declarations + let protocolVersion: number; + // eslint-disable-next-line @typescript-eslint/init-declarations + let forceLowerCase: boolean | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations + let fieldTypes: FieldTypes | undefined; + + const createBinaryOperationFormatter = (op: string): Formatter => (prop, val) => `${prop} ${op} ${val}`; + + const createStringFuncFormatter = (op: string, reverse?: boolean): Formatter => (prop, val) => { + const bag = [op, '(']; + const propName = forceLowerCase && !prop.includes('tolower(') ? `tolower(${prop})` : prop; + const value = forceLowerCase ? val.toLowerCase() : val; + + if (reverse) { + bag.push(value, ',', propName); + } else { + bag.push(propName, ',', value); + } + + bag.push(')'); + return bag.join(''); + }; + + const isStringFunction = function (name: string): boolean { + return STRING_FUNCTIONS.some((funcName) => funcName === name); + }; + + const formatters: Record = { + '=': createBinaryOperationFormatter('eq'), + '<>': createBinaryOperationFormatter('ne'), + '>': createBinaryOperationFormatter('gt'), + '>=': createBinaryOperationFormatter('ge'), + '<': createBinaryOperationFormatter('lt'), + '<=': createBinaryOperationFormatter('le'), + startswith: createStringFuncFormatter('startswith'), + endswith: createStringFuncFormatter('endswith'), + }; + + /* eslint-disable spellcheck/spell-checker */ + const formattersV2: Record = { + ...formatters, + contains: createStringFuncFormatter('substringof', true), + notcontains: createStringFuncFormatter('not substringof', true), + }; + /* eslint-enable spellcheck/spell-checker */ + + /* eslint-disable spellcheck/spell-checker */ + const formattersV4: Record = { + ...formatters, + contains: createStringFuncFormatter('contains'), + notcontains: createStringFuncFormatter('not contains'), + }; + /* eslint-enable spellcheck/spell-checker */ + + const compileBinary = (criteria: unknown[]): string => { + const crit = normalizeBinaryCriterion(criteria); + + const [rawFieldName, op] = crit; + const fieldName = isString(rawFieldName) || rawFieldName instanceof EdmLiteral + ? rawFieldName + : String(rawFieldName); + const fieldType = fieldTypes?.[String(fieldName)]; + + if (fieldType && isStringFunction(op) && fieldType !== 'String') { + throw errors.Error('E4024', op, fieldName, fieldType); + } + + const criterionFormatters = protocolVersion === 4 + ? formattersV4 + : formattersV2; + const formatter = criterionFormatters[op.toLowerCase()]; + + if (!formatter) { + throw errors.Error('E4003', op); + } + + const value = fieldType ? convertPrimitiveValue(fieldType, crit[2]) : crit[2]; + + return formatter( + serializePropName(fieldName), + serializeValue(value, protocolVersion, fieldType), + ); + }; + + const compileUnary = (criteria: unknown[]): string => { + const op = criteria[0]; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const crit = compileCore(criteria[1]); + + if (op === '!') { + return `not (${crit})`; + } + + throw errors.Error('E4003', op); + }; + + const compileGroup = (criteria: unknown[]): string => { + const bag: string[] = []; + // eslint-disable-next-line @typescript-eslint/init-declarations + let groupOperator: string | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations + let nextGroupOperator: string | undefined; + + criteria.forEach((criterion) => { + if (Array.isArray(criterion)) { + if (bag.length > 1 && groupOperator !== nextGroupOperator) { + throw errors.Error('E4019'); + } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + bag.push(`(${compileCore(criterion)})`); + + groupOperator = nextGroupOperator; + nextGroupOperator = 'and'; + } else { + nextGroupOperator = isConjunctiveOperator(criterion) ? 'and' : 'or'; + } + }); + + return bag.join(` ${groupOperator} `); + }; + + const compileCore = (criteria: unknown): string => { + const criterion: unknown[] = Array.isArray(criteria) ? criteria : [criteria]; + + if (Array.isArray(criterion[0])) { + return compileGroup(criterion); + } + + if (isUnaryOperation(criterion)) { + return compileUnary(criterion); + } + + return compileBinary(criterion); + }; + + return ( + criteria: unknown, + version: number, + types: FieldTypes | undefined, + filterToLower: boolean | undefined, + ): string => { + fieldTypes = types; + forceLowerCase = filterToLower ?? config().oDataFilterToLower; + protocolVersion = version; + + return compileCore(criteria); + }; +})(); + +const createODataQueryAdapter = (queryOptions: RemoteQueryOptions): QueryAdapter => { + let sorting: string[] = []; + const criteria: unknown[] = []; + const { expand } = queryOptions; + // eslint-disable-next-line @typescript-eslint/init-declarations + let select: string[] | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations + let skip: number | undefined; + // eslint-disable-next-line @typescript-eslint/init-declarations + let take: number | undefined; + let countQuery = false; + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const oDataVersion = queryOptions.version || DEFAULT_PROTOCOL_VERSION; + + const hasSlice = (): boolean => !!skip || take !== undefined; + + const hasFunction = (criterion: unknown[]): boolean => criterion.some( + (item) => isFunction(item) || (Array.isArray(item) && hasFunction(item)), + ); + + const requestData = (): ODataRequestParams => { + const result: ODataRequestParams = {}; + + if (!countQuery) { + if (sorting.length) { + result.$orderby = sorting.join(','); + } + if (skip) { + result.$skip = skip; + } + if (take !== undefined) { + result.$top = take; + } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + result.$select = generateSelect(oDataVersion, select) || undefined; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + result.$expand = generateExpand(oDataVersion, expand, select) || undefined; + } + + if (criteria.length) { + const filterCriteria: unknown = criteria.length < 2 ? criteria[0] : criteria; + const { fieldTypes, filterToLower } = queryOptions; + result.$filter = compileCriteria(filterCriteria, oDataVersion, fieldTypes, filterToLower); + } + + if (countQuery) { + result.$top = 0; + } + + if (queryOptions.requireTotalCount || countQuery) { + // todo: tests!!! + if (oDataVersion !== 4) { + // eslint-disable-next-line spellcheck/spell-checker + result.$inlinecount = 'allpages'; + } else { + result.$count = 'true'; + } + } + + return result; + }; + + const tryLiftSelect = (tasks: RemoteTask[]): void => { + const selectIndex = tasks.findIndex((task) => task.name === 'select'); + + if (selectIndex < 0 || !isFunction(tasks[selectIndex].args[0])) return; + + const nextTask = tasks[1 + selectIndex]; + if (nextTask?.name !== 'slice') return; + + tasks[1 + selectIndex] = tasks[selectIndex]; + tasks[selectIndex] = nextTask; + }; + + return { + + optimize: tryLiftSelect, + + exec(url: string): DeferredObj { + return sendRequest( + oDataVersion, + { + url, + params: extend(requestData(), queryOptions.params), + }, + { + beforeSend: queryOptions.beforeSend, + jsonp: queryOptions.jsonp, + withCredentials: queryOptions.withCredentials, + countOnly: countQuery, + processDatesAsUtc: queryOptions.processDatesAsUtc, + fieldTypes: queryOptions.fieldTypes, + isPaged: isFinite(Number(take)), + }, + ); + }, + + multiSort(args: unknown[]): boolean | undefined { + const rules: string[] = []; + + if (hasSlice()) { + return false; + } + + for (const arg of args) { + const [getter, desc] = Array.isArray(arg) ? arg : []; + + if (typeof getter !== 'string') { + return false; + } + + rules.push(desc ? `${serializePropName(getter)} desc` : serializePropName(getter)); + } + + sorting = rules; + + return undefined; + }, + + slice(skipCount: number, takeCount: number): boolean | undefined { + if (hasSlice()) { + return false; + } + + skip = skipCount; + take = takeCount; + + return undefined; + }, + + filter(...args: unknown[]): boolean | undefined { + if (hasSlice()) { + return false; + } + + const [first] = args; + const criterion: unknown[] = Array.isArray(first) ? first : args.slice(); + + if (hasFunction(criterion)) { + return false; + } + + if (criteria.length) { + criteria.push('and'); + } + criteria.push(criterion); + + return undefined; + }, + + select(...args: unknown[]): boolean | undefined { + const [expr] = args; + + if (select || isFunction(expr)) { + return false; + } + + select = (Array.isArray(expr) ? expr : args.slice()).map(String); + + return undefined; + }, + + count(): boolean { + countQuery = true; + return countQuery; + }, + }; +}; + +queryAdapters.odata = createODataQueryAdapter; + +export const odata = createODataQueryAdapter; diff --git a/packages/devextreme/js/__internal/data/odata/request_dispatcher.ts b/packages/devextreme/js/__internal/data/odata/request_dispatcher.ts new file mode 100644 index 000000000000..36acb68ebdde --- /dev/null +++ b/packages/devextreme/js/__internal/data/odata/request_dispatcher.ts @@ -0,0 +1,91 @@ +import '@js/common/data/odata/query_adapter'; + +import { sendRequest } from '@js/common/data/odata/utils'; +import type { DeferredObj } from '@js/core/utils/deferred'; + +const DEFAULT_PROTOCOL_VERSION = 4; + +export interface RequestDispatcherOptions { + url?: string; + beforeSend?: Function; + jsonp?: boolean; + version?: number; + withCredentials?: boolean; + processDatesAsUtc?: boolean; + deserializeDates?: boolean; + filterToLower?: boolean; +} + +export default class RequestDispatcher { + _url: string; + + _beforeSend?: Function; + + _jsonp?: boolean; + + _version: number; + + _withCredentials?: boolean; + + _processDatesAsUtc: boolean; + + _filterToLower?: boolean; + + constructor(options?: RequestDispatcherOptions) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const dispatcherOptions: RequestDispatcherOptions = options || {}; + + this._url = String(dispatcherOptions.url).replace(/\/+$/, ''); + this._beforeSend = dispatcherOptions.beforeSend; + this._jsonp = dispatcherOptions.jsonp; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this._version = dispatcherOptions.version || DEFAULT_PROTOCOL_VERSION; + this._withCredentials = dispatcherOptions.withCredentials; + this._processDatesAsUtc = dispatcherOptions.processDatesAsUtc + ?? dispatcherOptions.deserializeDates ?? false; + this._filterToLower = dispatcherOptions.filterToLower; + } + + sendRequest( + url: string, + method?: string, + params?: Record | null, + payload?: unknown, + ): DeferredObj { + return sendRequest( + this.version, + { + url, + method, + params: params ?? {}, + payload, + }, + { + beforeSend: this._beforeSend, + jsonp: this._jsonp, + withCredentials: this._withCredentials, + processDatesAsUtc: this._processDatesAsUtc, + }, + ); + } + + get version(): number { + return this._version; + } + + get beforeSend(): Function | undefined { + return this._beforeSend; + } + + get url(): string { + return this._url; + } + + get jsonp(): boolean | undefined { + return this._jsonp; + } + + get filterToLower(): boolean | undefined { + return this._filterToLower; + } +} diff --git a/packages/devextreme/js/__internal/data/odata/store.ts b/packages/devextreme/js/__internal/data/odata/store.ts index 4464dc43d888..d02f3cf322fe 100644 --- a/packages/devextreme/js/__internal/data/odata/store.ts +++ b/packages/devextreme/js/__internal/data/odata/store.ts @@ -35,13 +35,6 @@ export interface ODataLoadOptions extends StoreLoadOptions { customQueryParams?: Record; } -// `RequestDispatcher` does not declare its own fields yet, so the members the store reads -// directly are described here until that class is typed. -type StoreRequestDispatcher = RequestDispatcher & { - _withCredentials?: unknown; - _processDatesAsUtc?: unknown; -}; - const expandKeyType = (key: StoreKey, keyType: string): FieldTypes => ({ [String(key)]: keyType }); const getProperty = (source: unknown, name: string): unknown => ( @@ -72,7 +65,7 @@ const mergeFieldTypesWithKeyType = ( }; class ODataStore extends Store { - _requestDispatcher: StoreRequestDispatcher; + _requestDispatcher: RequestDispatcher; _fieldTypes: FieldTypes; diff --git a/packages/devextreme/js/__internal/data/query.ts b/packages/devextreme/js/__internal/data/query.ts new file mode 100644 index 000000000000..f25f87f4734f --- /dev/null +++ b/packages/devextreme/js/__internal/data/query.ts @@ -0,0 +1,16 @@ +import { queryImpl } from '@js/common/data/query_implementation'; +import type { ArrayQuery, QueryOptions } from '@ts/data/array_query'; +import type { RemoteQuery, RemoteQueryOptions } from '@ts/data/remote_query'; + +function query(array: unknown[], queryOptions?: QueryOptions): ArrayQuery; +function query(url: string, queryOptions?: RemoteQueryOptions): RemoteQuery; +function query( + source: unknown[] | string, + queryOptions?: QueryOptions & RemoteQueryOptions, +): ArrayQuery | RemoteQuery { + return Array.isArray(source) + ? queryImpl.array(source, queryOptions) + : queryImpl.remote(source, queryOptions); +} + +export default query; diff --git a/packages/devextreme/js/__internal/data/query_implementation.ts b/packages/devextreme/js/__internal/data/query_implementation.ts index 2654903ad637..d7185d49d29e 100644 --- a/packages/devextreme/js/__internal/data/query_implementation.ts +++ b/packages/devextreme/js/__internal/data/query_implementation.ts @@ -1,5 +1,5 @@ import arrayQueryImpl from './array_query'; -import remoteQueryImpl from './m_remote_query'; +import remoteQueryImpl from './remote_query'; export const queryImpl = { array: arrayQueryImpl, diff --git a/packages/devextreme/js/__internal/data/remote_query.ts b/packages/devextreme/js/__internal/data/remote_query.ts new file mode 100644 index 000000000000..dbf79b08c62f --- /dev/null +++ b/packages/devextreme/js/__internal/data/remote_query.ts @@ -0,0 +1,218 @@ +import arrayQueryImpl from '@js/common/data/array_query'; +import { errors, handleError } from '@js/common/data/errors'; +import queryAdapters from '@js/common/data/query_adapters'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { Deferred } from '@js/core/utils/deferred'; +import { isFunction } from '@js/core/utils/type'; +import type { ArrayQuery, LangParams } from '@ts/data/array_query'; + +export interface RemoteTask { + name: string; + args: unknown[]; +} + +export interface QueryAdapter { + [taskName: string]: unknown; + optimize?: (tasks: RemoteTask[]) => void; + exec: (url: string) => DeferredObj; +} + +export interface RemoteQueryOptions { + adapter?: string | ((options: RemoteQueryOptions) => QueryAdapter); + errorHandler?: (error: unknown) => void; + langParams?: LangParams; + version?: number; + expand?: string | string[] | Function; + fieldTypes?: Record; + filterToLower?: boolean; + requireTotalCount?: boolean; + params?: Record; + beforeSend?: Function; + jsonp?: boolean; + withCredentials?: boolean; + processDatesAsUtc?: boolean; + deserializeDates?: boolean; +} + +export interface RemoteQuery { + sortBy: (...args: unknown[]) => RemoteQuery; + thenBy: (...args: unknown[]) => RemoteQuery; + filter: (...args: unknown[]) => RemoteQuery; + slice: (...args: unknown[]) => RemoteQuery; + select: (...args: unknown[]) => RemoteQuery; + groupBy: (...args: unknown[]) => RemoteQuery; + count: (...args: unknown[]) => DeferredObj; + min: (...args: unknown[]) => DeferredObj; + max: (...args: unknown[]) => DeferredObj; + sum: (...args: unknown[]) => DeferredObj; + avg: (...args: unknown[]) => DeferredObj; + aggregate: (...args: unknown[]) => DeferredObj; + enumerate: (...args: unknown[]) => DeferredObj; +} + +const createTask = function (name: string, args: unknown[]): RemoteTask { + return { name, args }; +}; + +const remoteQueryImpl = function ( + url: string, + options?: RemoteQueryOptions, + previousTasks?: RemoteTask[], +): RemoteQuery { + const tasks: RemoteTask[] = previousTasks ?? []; + const queryOptions: RemoteQueryOptions = options ?? {}; + + const exec = function (executorTask: RemoteTask): DeferredObj { + const d = Deferred(); + // eslint-disable-next-line @typescript-eslint/init-declarations + let mergedSortArgs: unknown[][] | undefined; + + const rejectWithNotify = function (error: unknown): void { + const handler = queryOptions.errorHandler; + if (handler) { + handler(error); + } + + handleError(error); + d.reject(error); + }; + + function mergeSortTask(task: RemoteTask): boolean { + switch (task.name) { + case 'sortBy': + mergedSortArgs = [task.args]; + return true; + + case 'thenBy': + if (!mergedSortArgs) { + throw errors.Error('E4004'); + } + + mergedSortArgs.push(task.args); + return true; + + default: + return false; + } + } + + function unmergeSortTasks(queue: RemoteTask[]): RemoteTask[] { + const head = queue[0]; + const unmergedTasks: RemoteTask[] = []; + + if (head?.name === 'multiSort') { + queue.shift(); + const [sortArgsList] = head.args; + if (Array.isArray(sortArgsList)) { + sortArgsList.forEach((sortArgs: unknown) => { + unmergedTasks.push(createTask( + unmergedTasks.length ? 'thenBy' : 'sortBy', + Array.isArray(sortArgs) ? sortArgs : [sortArgs], + )); + }); + } + } + + return unmergedTasks.concat(queue); + } + + function passTaskToAdapter(adapter: QueryAdapter, task: RemoteTask): boolean { + if (String(task.name) === 'enumerate') { + return true; + } + + const taskHandler = adapter[task.name]; + + return isFunction(taskHandler) && taskHandler.apply(adapter, task.args) !== false; + } + + function collectAdapterTasks(adapter: QueryAdapter, queue: RemoteTask[]): void { + while (queue.length) { + const currentTask = queue[0]; + + if (!mergeSortTask(currentTask)) { + if (mergedSortArgs) { + queue.unshift(createTask('multiSort', [mergedSortArgs])); + mergedSortArgs = undefined; + // eslint-disable-next-line no-continue + continue; + } + + if (!passTaskToAdapter(adapter, currentTask)) { + break; + } + } + queue.shift(); + } + } + + try { + const adapterFactory = isFunction(queryOptions.adapter) + ? queryOptions.adapter + : queryAdapters[String(queryOptions.adapter)]; + + const adapter: QueryAdapter = adapterFactory(queryOptions); + + let taskQueue: RemoteTask[] = [...tasks, executorTask]; + + const { optimize } = adapter; + if (optimize) optimize(taskQueue); + + collectAdapterTasks(adapter, taskQueue); + + taskQueue = unmergeSortTasks(taskQueue); + + adapter.exec(url) + .done((result: unknown, extra: unknown) => { + if (!taskQueue.length) { + d.resolve(result, extra); + } else { + // @ts-expect-error the adapter resolves with whatever the service returned + let clientChain: ArrayQuery = arrayQueryImpl(result, { + errorHandler: queryOptions.errorHandler, + }); + taskQueue.forEach((task) => { + const method = clientChain[task.name]; + if (isFunction(method)) { + clientChain = method.apply(clientChain, task.args); + } + }); + // @ts-expect-error the queue always ends with `enumerate`, so + // the chain ends with a Deferred rather than with a query + clientChain.done(d.resolve).fail(d.reject); + } + }) + .fail(rejectWithNotify); + } catch (x) { + rejectWithNotify(x); + } + + // @ts-expect-error DeferredObj typings: promise() is declared as a plain Promise + return d.promise(); + }; + + const chain = (name: string, args: unknown[]): RemoteQuery => remoteQueryImpl( + url, + queryOptions, + tasks.concat(createTask(name, args)), + ); + + return { + sortBy: (...args: unknown[]): RemoteQuery => chain('sortBy', args), + thenBy: (...args: unknown[]): RemoteQuery => chain('thenBy', args), + filter: (...args: unknown[]): RemoteQuery => chain('filter', args), + slice: (...args: unknown[]): RemoteQuery => chain('slice', args), + select: (...args: unknown[]): RemoteQuery => chain('select', args), + groupBy: (...args: unknown[]): RemoteQuery => chain('groupBy', args), + + count: (...args: unknown[]): DeferredObj => exec(createTask('count', args)), + min: (...args: unknown[]): DeferredObj => exec(createTask('min', args)), + max: (...args: unknown[]): DeferredObj => exec(createTask('max', args)), + sum: (...args: unknown[]): DeferredObj => exec(createTask('sum', args)), + avg: (...args: unknown[]): DeferredObj => exec(createTask('avg', args)), + aggregate: (...args: unknown[]): DeferredObj => exec(createTask('aggregate', args)), + enumerate: (...args: unknown[]): DeferredObj => exec(createTask('enumerate', args)), + }; +}; + +export default remoteQueryImpl; diff --git a/packages/devextreme/js/__internal/data/store_helper.ts b/packages/devextreme/js/__internal/data/store_helper.ts new file mode 100644 index 000000000000..93f46a79203f --- /dev/null +++ b/packages/devextreme/js/__internal/data/store_helper.ts @@ -0,0 +1,137 @@ +import arrayQuery from '@js/common/data/array_query'; +import type { SortingInfo } from '@js/common/data/utils'; +import { normalizeSortingInfo } from '@js/common/data/utils'; +import { extend } from '@js/core/utils/extend'; +import { isObject } from '@js/core/utils/type'; +import type { LangParams } from '@ts/data/array_query'; + +export interface DataQuery { + /* eslint-disable @typescript-eslint/method-signature-style */ + setLangParams?(langParams: LangParams): void; + filter(criteria: unknown): this; + sortBy(getter: unknown, desc?: unknown, compare?: unknown): this; + thenBy(getter: unknown, desc?: unknown, compare?: unknown): this; + select(getter: unknown): this; + slice(skip: number, take?: number): this; + groupBy(getter: unknown): this; + toArray(): unknown[]; + /* eslint-enable @typescript-eslint/method-signature-style */ +} + +interface GroupingInfo extends Array { + keepInitialKeyOrder?: boolean; +} + +export interface QueryByOptions { + filter?: unknown; + sort?: unknown; + select?: unknown; + group?: unknown; + skip?: number; + take?: number; + langParams?: LangParams; +} + +interface GroupResult { + items?: unknown[]; +} + +function multiLevelGroup( + query: TQuery, + groupInfo: SortingInfo[], +): TQuery { + let result = query.groupBy(groupInfo[0].selector); + + if (groupInfo.length > 1) { + result = result.select((group: unknown) => { + const { items }: GroupResult = isObject(group) ? group : {}; + + const merged: unknown = extend({}, group, { + items: multiLevelGroup(arrayQuery(items ?? []), groupInfo.slice(1)).toArray(), + }); + + return merged; + }); + } + + return result; +} + +function arrangeSortingInfo(groupInfo: SortingInfo[], sortInfo: SortingInfo[]): SortingInfo[] { + const filteredGroup = groupInfo.filter( + (group) => !sortInfo.some((sort) => group.selector === sort.selector), + ); + + return filteredGroup.concat(sortInfo); +} + +function queryByOptions( + query: TQuery, + options?: QueryByOptions, + isCountQuery?: boolean, +): TQuery { + const queryOptions: QueryByOptions = options ?? {}; + let result = query; + + const { filter } = queryOptions; + + if (queryOptions.langParams) { + result.setLangParams?.(queryOptions.langParams); + } + + if (filter) { + result = result.filter(filter); + } + + if (isCountQuery) { + return result; + } + + const { + sort, select, skip, take, + } = queryOptions; + + // eslint-disable-next-line @typescript-eslint/init-declarations + let group: GroupingInfo | undefined; + if (queryOptions.group) { + const groupOption: { keepInitialKeyOrder?: unknown } = isObject(queryOptions.group) + ? queryOptions.group + : {}; + + group = normalizeSortingInfo(queryOptions.group); + group.keepInitialKeyOrder = !!groupOption.keepInitialKeyOrder; + } + + if (sort || group) { + const sortInfo = normalizeSortingInfo(sort ?? []); + const sortRules = group && !group.keepInitialKeyOrder + ? arrangeSortingInfo(group, sortInfo) + : sortInfo; + + sortRules.forEach((rule, index) => { + result = index + ? result.thenBy(rule.selector, rule.desc, rule.compare) + : result.sortBy(rule.selector, rule.desc, rule.compare); + }); + } + + if (select) { + result = result.select(select); + } + + if (group) { + result = multiLevelGroup(result, group); + } + + if (take || skip) { + result = result.slice(skip ?? 0, take); + } + + return result; +} + +export default { + multiLevelGroup, + arrangeSortingInfo, + queryByOptions, +}; diff --git a/packages/devextreme/js/__internal/data/utils.ts b/packages/devextreme/js/__internal/data/utils.ts new file mode 100644 index 000000000000..3be3ae6daf20 --- /dev/null +++ b/packages/devextreme/js/__internal/data/utils.ts @@ -0,0 +1,360 @@ +/* eslint-disable spellcheck/spell-checker */ +import domAdapter from '@js/core/dom_adapter'; +import { equalByValue } from '@js/core/utils/common'; +import type { DeferredObj } from '@js/core/utils/deferred'; +import { Deferred } from '@js/core/utils/deferred'; +import { map } from '@js/core/utils/iterator'; +import readyCallbacks from '@js/core/utils/ready_callbacks'; +import { isFunction, isObject, isString } from '@js/core/utils/type'; +import { getWindow } from '@js/core/utils/window'; + +const ready = readyCallbacks.add; + +export const XHR_ERROR_UNLOAD = 'DEVEXTREME_XHR_ERROR_UNLOAD'; + +export type BinaryCriterion = [unknown, string, unknown]; + +export const normalizeBinaryCriterion = function (crit: unknown[]): BinaryCriterion { + return [ + crit[0], + crit.length < 3 ? '=' : String(crit[1]).toLowerCase(), + crit.length < 2 ? true : crit[crit.length - 1], + ]; +}; + +export type SortingSelector = string | Function; + +export interface SortingInfo { + selector: SortingSelector | undefined; + desc: boolean; + compare?: unknown; + isExpanded?: boolean; + groupInterval?: unknown; +} + +interface SortingDescriptor { + getter?: SortingSelector; + field?: SortingSelector; + selector?: SortingSelector; + desc?: unknown; + dir?: unknown; + compare?: unknown; +} + +export const normalizeSortingInfo = function (info: unknown): SortingInfo[] { + const infoArray: unknown[] = Array.isArray(info) ? info : [info]; + + return infoArray.map((item: unknown): SortingInfo => { + const descriptor: SortingDescriptor = isObject(item) ? item : {}; + const result: SortingInfo = { + selector: isFunction(item) || isString(item) + ? item + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + : descriptor.getter || descriptor.field || descriptor.selector, + desc: !!(descriptor.desc || String(descriptor.dir).charAt(0).toLowerCase() === 'd'), + }; + if (descriptor.compare) { + result.compare = descriptor.compare; + } + return result; + }); +}; + +export const errorMessageFromXhr = (function () { + const textStatusMessages: Record = { + timeout: 'Network connection timeout', + error: 'Unspecified network error', + parsererror: 'Unexpected server response', + }; + + /// #DEBUG + const textStatusDetails: Record = { + timeout: 'possible causes: the remote host is not accessible, overloaded or is not included into the domain white-list when being run in the native container', + error: 'if the remote host is located on another domain, make sure it properly supports cross-origin resource sharing (CORS), or use the JSONP approach instead', + parsererror: 'the remote host did not respond with valid JSON data', + }; + /// #ENDDEBUG + + const explainTextStatus = function (textStatus: string): string { + let result = textStatusMessages[textStatus]; + + if (!result) { + return textStatus; + } + + /// #DEBUG + result += ` (${textStatusDetails[textStatus]})`; + /// #ENDDEBUG + + return result; + }; + + // T542570, https://stackoverflow.com/a/18170879 + let unloading = false; + ready(() => { + const window = getWindow(); + domAdapter.listen(window, 'beforeunload', () => { unloading = true; }); + }); + + return function (xhr: unknown, textStatus: unknown): string { + if (unloading) { + return XHR_ERROR_UNLOAD; + } + + const status = isObject(xhr) && 'status' in xhr ? Number(xhr.status) : NaN; + + if (status < 400) { + return explainTextStatus(String(textStatus)); + } + + return isObject(xhr) && 'statusText' in xhr ? String(xhr.statusText) : ''; + }; +}()); + +type AggregatableValue = number | string | Date; + +export const aggregators = { + count: { + seed: 0, + step(count: number): number { return 1 + count; }, + }, + sum: { + seed: 0, + step(sum: number, item: number): number { return sum + item; }, + }, + min: { + step(min: AggregatableValue, item: AggregatableValue): AggregatableValue { + return item < min ? item : min; + }, + }, + max: { + step(max: AggregatableValue, item: AggregatableValue): AggregatableValue { + return item > max ? item : max; + }, + }, + avg: { + seed: [0, 0], + step(pair: [number, number], value: number): [number, number] { + return [pair[0] + value, pair[1] + 1]; + }, + finalize(pair: [number, number]): number { + return pair[1] ? pair[0] / pair[1] : NaN; + }, + }, +}; + +interface RequestResultLock { + obtain: () => void; + release: () => void; + promise: () => DeferredObj; + reset: () => void; +} + +export const processRequestResultLock: RequestResultLock = (function (): RequestResultLock { + let lockCount = 0; + // eslint-disable-next-line @typescript-eslint/init-declarations + let lockDeferred: DeferredObj | undefined; + + const obtain = function (): void { + if (lockCount === 0) { + lockDeferred = Deferred(); + } + lockCount += 1; + }; + + const release = function (): void { + lockCount -= 1; + if (lockCount < 1) { + lockDeferred?.resolve(); + } + }; + + const promise = function (): DeferredObj { + return lockCount === 0 || !lockDeferred + ? Deferred().resolve() + : lockDeferred; + }; + + const reset = function (): void { + lockCount = 0; + lockDeferred?.resolve(); + }; + + return { + obtain, + release, + promise, + reset, + }; +}()); + +export function isDisjunctiveOperator(condition: unknown): boolean { + return /^(or|\|\||\|)$/i.test(String(condition)); +} + +export function isConjunctiveOperator(condition: unknown): boolean { + return /^(and|&&|&)$/i.test(String(condition)); +} + +const isRecord = (value: unknown): value is Record => isObject(value); + +export const keysEqual = function (keyExpr: unknown, key1: unknown, key2: unknown): boolean { + if (Array.isArray(keyExpr)) { + const names: string[] = map(key1, (value: unknown, name: string): string => name); + const values1: Record = isRecord(key1) ? key1 : {}; + const values2: Record = isRecord(key2) ? key2 : {}; + + for (const name of names) { + if (!equalByValue(values1[name], values2[name], { strict: false })) { + return false; + } + } + return true; + } + + return equalByValue(key1, key2, { strict: false }); +}; + +const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + +/* eslint-disable no-bitwise */ + +function stringToByteArray(str: string): number[] { + const bytes: number[] = []; + + for (let i = 0; i < str.length; i += 1) { + const code = str.charCodeAt(i); + + if (code < 128) { + bytes.push(code); + } else if (code < 2048) { + bytes.push(192 + (code >> 6), 128 + (code & 63)); + } else if (code < 65536) { + bytes.push(224 + (code >> 12), 128 + ((code >> 6) & 63), 128 + (code & 63)); + } else if (code < 2097152) { + bytes.push( + 240 + (code >> 18), + 128 + ((code >> 12) & 63), + 128 + ((code >> 6) & 63), + 128 + (code & 63), + ); + } + } + return bytes; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const base64_encode = function (input: string | number[]): string { + const bytes: number[] = Array.isArray(input) ? input : stringToByteArray(String(input)); + + let result = ''; + + function getBase64Char(index: number): string { + return BASE64_CHARS.charAt(index); + } + + for (let i = 0; i < bytes.length; i += 3) { + const octet1 = bytes[i]; + const octet2 = bytes[i + 1]; + const octet3 = bytes[i + 2]; + + result += [ + octet1 >> 2, + ((octet1 & 3) << 4) | (octet2 >> 4), + isNaN(octet2) ? 64 : ((octet2 & 15) << 2) | (octet3 >> 6), + isNaN(octet3) ? 64 : octet3 & 63, + ].map(getBase64Char).join(''); + } + + return result; +}; + +/* eslint-enable no-bitwise */ + +export const isUnaryOperation = function (crit: unknown): boolean { + return Array.isArray(crit) && crit[0] === '!' && Array.isArray(crit[1]); +}; + +const isGroupOperator = function (value: unknown): boolean { + return value === 'and' || value === 'or'; +}; + +export const isUniformEqualsByOr = function (crit: unknown[]): boolean { + if (crit.length > 2 && Array.isArray(crit[0]) && crit[1] === 'or' && typeof crit[0][0] === 'string' && crit[0][1] === '=') { + const [prop] = crit[0]; + return !crit.find((el, i) => (i % 2 !== 0 ? el !== 'or' + : !Array.isArray(el) || el.length !== 3 || el[0] !== prop || el[1] !== '=')); + } + return false; +}; + +export const isGroupCriterion = function (crit: unknown): boolean { + if (!Array.isArray(crit)) { + return false; + } + + const first = crit[0]; + const second = crit[1]; + + if (Array.isArray(first)) { + return true; + } + if (isFunction(first)) { + if (Array.isArray(second) || isFunction(second) || isGroupOperator(second)) { + return true; + } + } + + return false; +}; + +export const trivialPromise = function (...args: T[]): DeferredObj { + const d = Deferred(); + // @ts-expect-error DeferredObj typings: promise() is declared as a plain Promise + return d.resolve(...args).promise(); +}; + +export const rejectedPromise = function (...args: T[]): DeferredObj { + const d = Deferred(); + // @ts-expect-error DeferredObj typings: promise() is declared as a plain Promise + return d.reject(...args).promise(); +}; + +type ThrottleTimeout = number | (() => number); + +type TimeoutId = ReturnType; + +function throttle( + func: (this: unknown) => void, + timeout: ThrottleTimeout, +): (this: unknown) => TimeoutId | undefined { + // eslint-disable-next-line @typescript-eslint/init-declarations + let timeoutId: TimeoutId | undefined; + return function (this: unknown): TimeoutId | undefined { + if (!timeoutId) { + timeoutId = setTimeout(() => { + timeoutId = undefined; + func.call(this); + }, isFunction(timeout) ? timeout() : timeout); + } + return timeoutId; + }; +} + +export function throttleChanges( + func: (this: unknown, changes: T[]) => void, + timeout: ThrottleTimeout, +): (this: unknown, changes: T[]) => TimeoutId | undefined { + let cache: T[] = []; + const throttled = throttle(function (this: unknown): void { + func.call(this, cache); + cache = []; + }, timeout); + + return function (this: unknown, changes: T[]): TimeoutId | undefined { + if (Array.isArray(changes)) { + cache.push(...changes); + } + return throttled.call(this); + }; +} diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts index 608c733636ba..fa6a5a10b27a 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping.ts @@ -105,6 +105,9 @@ const dataSourceAdapterExtender = (Base: ModuleType) => class groups[i].isExpanded = group[i].isExpanded; } } + // @ts-expect-error `normalizeSortingInfo()` types the selector as `string | Function`, + // which is wider than the public `KeySelector`; it is the same descriptor the data + // source itself returned above. dataSource.group(groups); that._grouping.foreachGroups((groupInfo, parents) => { if (groupIndex === undefined || groupIndex === parents.length - 1) { diff --git a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping_expanded.ts b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping_expanded.ts index 023624dca8b4..f153e81939df 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping_expanded.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/grouping/m_grouping_expanded.ts @@ -343,7 +343,6 @@ export class GroupingHelper extends GroupingHelperCore { if (groupCount) { let { data } = options; - // @ts-expect-error const query = dataQuery(data); storeHelper.multiLevelGroup(query, groups).enumerate().done((groupedData) => { data = groupedData; diff --git a/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts b/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts index baeb374a1b7b..b6d8e21f13df 100644 --- a/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts +++ b/packages/devextreme/js/__internal/grids/data_grid/summary/m_summary.ts @@ -117,7 +117,6 @@ const sortGroupsBySummaryCore = function (items, groups, sortByGroups) { let query; if (group && sorts && sorts.length) { - // @ts-expect-error query = dataQuery(items); each(sorts, function (index) { if (index === 0) { @@ -307,7 +306,6 @@ export const summaryDataSourceAdapterExtender = ( } private sortLastLevelGroupItems(items, groups, paths) { - // @ts-expect-error const groupedItems = storeHelper.multiLevelGroup(dataQuery(items), groups).toArray(); let result = []; diff --git a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts index aeab55d20f08..859b58d185bc 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/columns_controller/m_columns_controller.ts @@ -1823,7 +1823,6 @@ export class ColumnsController extends modules.Controller { } if (isPlainObject(dataSource) || (dataSource instanceof Store) || Array.isArray(dataSource)) { if (that.valueExpr) { - // @ts-expect-error const dataSourceOptions = normalizeDataSourceOptions(dataSource); dataSourceOptions.paginate = false; dataSource = new DataSource(dataSourceOptions); diff --git a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts index 4b135fbd8680..62db3e4c9f96 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/data_source_adapter/m_data_source_adapter.ts @@ -297,7 +297,6 @@ export default class DataSourceAdapter extends modules.Controller { this.resetPagesCache(true); if (this._cachedStoreData) { - // @ts-expect-error applyBatch({ keyInfo: store, data: this._cachedStoreData, @@ -365,7 +364,6 @@ export default class DataSourceAdapter extends modules.Controller { const getItemCount = () => (groupCount ? this.itemsCount() : this.items().length); const oldItemCount = getItemCount(); - // @ts-expect-error applyBatch({ keyInfo, data: this._items, @@ -374,7 +372,6 @@ export default class DataSourceAdapter extends modules.Controller { useInsertIndex: true, skipCopying: !this._needToCopyDataObject(), }); - // @ts-expect-error applyBatch({ keyInfo, data: dataSource.items(), diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts index 10d151fa7380..f25d8b75be75 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_filter/m_header_filter.ts @@ -79,13 +79,11 @@ function ungroupUTCDates(items, dateParts?, dates?) { export function convertDataFromUTCToLocal(data, column) { const dates = ungroupUTCDates(data); - // @ts-expect-error const query = dataQuery(dates); const group = gridCoreUtils.getHeaderFilterGroupParameters({ ...column, calculateCellValue: (date) => date, }); - // @ts-expect-error return storeHelper.queryByOptions(query, { group }).toArray(); } @@ -245,7 +243,6 @@ export class HeaderFilterController extends Modules.ViewController { if (!dataSource) return; if (isDefined(headerFilterDataSource) && !isFunction(headerFilterDataSource)) { - // @ts-expect-error options.dataSource = normalizeDataSourceOptions(headerFilterDataSource); } else if (column.lookup) { isLookup = true; @@ -275,6 +272,8 @@ export class HeaderFilterController extends Modules.ViewController { dataSource.customLoader.load(options).done(({ data }) => { const convertUTCDates = remoteGrouping && isUTCFormat(column.serializationFormat) && cutoffLevel > 3; if (convertUTCDates) { + // @ts-expect-error data/store_helper types `toArray()` as `unknown[]`; + // typing the grid rows is left to a later iteration data = convertDataFromUTCToLocal(data, column); } that._processGroupItems(data, null, null, { diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index ae057d8d7009..abdc6d0299b8 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -674,7 +674,6 @@ export default { lookupDataSourceOptions = lookupDataSourceOptions({}); } } - // @ts-expect-error return normalizeDataSourceOptions(lookupDataSourceOptions); }, diff --git a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts index f35045af3838..71faffb9cc83 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/search/m_search.ts @@ -106,9 +106,7 @@ const dataController = ( const filterValue = parseValue(column, text); if (lookup?.items) { - // @ts-expect-error dataQuery(lookup.items, { langParams }) - // @ts-expect-error .filter( column.createFilterExpression.call( { diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts index 4d43b738af41..b1e2ccb921c4 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import type { FilterDescriptor } from '@js/data'; import type DataSource from '@js/data/data_source'; -import { keysEqual } from '@ts/data/m_utils'; +import { keysEqual } from '@ts/data/utils'; import type { Constructor } from '../types'; import type { GridCoreNewBase } from '../widget'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts index ac96d84c87ac..0e5f18efaacb 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts @@ -6,7 +6,7 @@ import type { Store } from '@js/data'; import type { DataSourceLike } from '@js/data/data_source'; import DataSource from '@js/data/data_source'; import { normalizeDataSourceOptions } from '@js/data/data_source/utils'; -import { applyBatch } from '@ts/data/m_array_utils'; +import { applyBatch } from '@ts/data/array_utils'; import type { DataObject, @@ -35,7 +35,8 @@ export function normalizeDataSource( }; } - // TODO: research making second param not required + // @ts-expect-error the public DataSource constructor is declared with the public + // option types, while `normalizeDataSourceOptions()` returns the internal ones return new DataSource(normalizeDataSourceOptions(dataSourceLike, undefined)); } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts index 76ee06aa32f8..1072316e25ba 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/filtering/header_filter/legacy_header_filter.ts @@ -16,7 +16,7 @@ import { isDefined, isFunction, isObject } from '@js/core/utils/type'; import messageLocalization from '@js/localization/message'; import filteringUtils from '@js/ui/shared/filtering'; import { extend } from '@ts/core/utils/m_extend'; -import { normalizeDataSourceOptions as oldNormalizeDataSourceOptions } from '@ts/data/data_source/m_utils'; +import { normalizeDataSourceOptions as oldNormalizeDataSourceOptions } from '@ts/data/data_source/utils'; import { convertDataFromUTCToLocal, getFormatOptions, @@ -158,7 +158,6 @@ export const getDataSourceOptions = ( const options: any = {}; if (isDefined(headerFilterDataSource) && !isFunction(headerFilterDataSource)) { - // @ts-expect-error options.dataSource = oldNormalizeDataSourceOptions(headerFilterDataSource); } else { const cutoffLevel = Array.isArray(group) ? group.length - 1 : 0; diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts b/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts index b5b1683bc03f..ef1a61eaa54e 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/data_source/m_data_source.ts @@ -341,7 +341,6 @@ class PivotGridDataSource { createLocalOrRemoteStore(dataSourceOptions, notifyProgress) { const StoreConstructor = dataSourceOptions.remoteOperations || dataSourceOptions.paginate ? RemoteStore : LocalStore; - // @ts-expect-error return new StoreConstructor(extend(normalizeDataSourceOptions(dataSourceOptions), { onChanged: null, onLoadingChanged: null, diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts b/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts index 174166bf6091..4ee6f3179b67 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/local_store/m_local_store.ts @@ -424,7 +424,6 @@ const LocalStore = Class.inherit((function () { if (dataSource.store() instanceof CustomStore && filter) { filter = processFilter(filter, fieldSelectors); - // @ts-expect-error return dataQuery(dataSource.items()).filter(filter).toArray(); } diff --git a/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts b/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts index 3412120c14f2..b514df7df779 100644 --- a/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts +++ b/packages/devextreme/js/__internal/grids/pivot_grid/remote_store/m_remote_store.ts @@ -561,7 +561,6 @@ class RemoteStore { skip: 0, take: 20, }).done((data) => { - // @ts-expect-error const normalizedArguments = normalizeLoadResult(data); d.resolve(pivotGridUtils.discoverObjectFields(normalizedArguments.data, fields)); }).fail(d.reject); diff --git a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts index b76ec5607f45..d0a2002ba0b5 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/data_source_adapter/m_data_source_adapter.ts @@ -39,9 +39,7 @@ const getChildKeys = function (that, keys) { return childKeys; }; -// @ts-expect-error const applySorting = (data: any[], sort: any): any => queryByOptions( - // @ts-expect-error query(data), { sort, @@ -227,7 +225,6 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { result = result || []; for (let i = 0; i < data.length; i++) { - // @ts-expect-error const item = createObjectWithChanges(data[i]); key = this._keyGetter(item); @@ -479,7 +476,6 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { if (loadedData.length) { if (needLocalFiltering) { - // @ts-expect-error loadedData = query(loadedData).filter(filter).toArray(); } @@ -625,7 +621,6 @@ export class DataSourceAdapterTreeList extends DataSourceAdapter { public customizeLoadResultHandler(options) { const data = options.data = this._convertDataToPlainStructure(options.data); if (!options.remoteOperations.filtering && options.loadOptions.filter) { - // @ts-expect-error options.fullData = queryByOptions(query(options.data), { sort: options.loadOptions && options.loadOptions.sort }).toArray(); } this._updateHasItemsMap(options); diff --git a/packages/devextreme/js/__internal/scheduler/utils/loader/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/loader/utils.ts index ccf48a23701c..730fe0f87d52 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/loader/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/loader/utils.ts @@ -1,6 +1,6 @@ import type { DataSourceLike } from '@js/data/data_source'; import DataSource from '@js/data/data_source'; -import { normalizeDataSourceOptions } from '@ts/data/data_source/m_utils'; +import { normalizeDataSourceOptions } from '@ts/data/data_source/utils'; export const normalizeDataSource = ( dataSourceOptions: DataSourceLike | null | undefined, @@ -19,6 +19,8 @@ export const normalizeDataSource = ( ...options, }; + // @ts-expect-error the public DataSource constructor is declared with the public + // option types, while `normalizeDataSourceOptions()` returns the internal ones return new DataSource(result); }; diff --git a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts index 06d89a472973..85bcffe6447f 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.edit.ts @@ -277,7 +277,6 @@ class CollectionWidget< if (that._disposed) { return; } - // @ts-expect-error arguments const items = normalizeLoadResult(loadResult).data; dataController.applyMapFunction(items); diff --git a/packages/devextreme/js/__internal/ui/collection/collection_widget.live_update.ts b/packages/devextreme/js/__internal/ui/collection/collection_widget.live_update.ts index d1d0c0fdfd0b..a73e374f5136 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.live_update.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.live_update.ts @@ -204,7 +204,6 @@ class CollectionWidgetLiveUpdate< } else { const changedItem = items[indexByKey(keyInfo, items, change.key)]; if (changedItem) { - // @ts-expect-error ts-error update(keyInfo, items, change.key, change.data).done(() => { this._renderItem( items.indexOf(changedItem), @@ -224,7 +223,6 @@ class CollectionWidgetLiveUpdate< isPartialRefresh?: boolean, ): void { when( - // @ts-expect-error ts-error isPartialRefresh ?? insert(keyInfo, items, change.data, change.index), ).done(() => { this._beforeItemElementInserted(change); diff --git a/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts b/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts index 4e5370ec834f..b5a5feb230b7 100644 --- a/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts +++ b/packages/devextreme/js/__internal/ui/hierarchical_collection/data_adapter.ts @@ -691,8 +691,9 @@ class DataAdapter { lookForParents(matches, 0); if (this.options.sort) { + // @ts-expect-error data/store_helper types `toArray()` as `unknown[]`; + // typing the node collection is left to a later iteration matches = storeHelper - // @ts-expect-error ts-error .queryByOptions(query(matches), { sort: this.options.sort, langParams: this.options.langParams, diff --git a/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts index d6e370a5d233..a261105c07d6 100644 --- a/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.deferred.ts @@ -89,7 +89,6 @@ export default class DeferredStrategy< const queryParams = this._getQueryParams(); - // @ts-expect-error dataQuery return !!dataQuery([itemData], queryParams).filter(selectionFilter).toArray().length; } diff --git a/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts index 4e1ae4bd8b5d..81d5cd3ec7d5 100644 --- a/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.standard.ts @@ -139,11 +139,12 @@ export default class StandardStrategy< forceCombinedFilter, ); - let deselectedItems = []; + let deselectedItems: TItem[] = []; if (isDeselect) { const { selectedItems } = this.options; + // @ts-expect-error data/array_query types `toArray()` as `unknown[]`; + // typing the query result by item is left to a later iteration deselectedItems = combinedFilter && keys.length !== selectedItems.length - // @ts-expect-error dataQuery ? dataQuery(selectedItems).filter(combinedFilter).toArray() : selectedItems.slice(0); } diff --git a/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts b/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts index 932eb2111af9..1a7b9056d022 100644 --- a/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts +++ b/packages/devextreme/js/__internal/ui/selection/selection.strategy.ts @@ -199,7 +199,6 @@ export default class SelectionStrategy< if (localFilter && !isSelectAll) { filteredItems = filteredItems.filter(localFilter); } else if (needLoadAllData) { - // @ts-expect-error dataQuary filteredItems = dataQuery(filteredItems).filter(remoteFilter).toArray(); } diff --git a/packages/devextreme/js/__internal/ui/shared/ui.editor_factory_mixin.ts b/packages/devextreme/js/__internal/ui/shared/ui.editor_factory_mixin.ts index 0458f8f1c274..501d8e2b300b 100644 --- a/packages/devextreme/js/__internal/ui/shared/ui.editor_factory_mixin.ts +++ b/packages/devextreme/js/__internal/ui/shared/ui.editor_factory_mixin.ts @@ -180,7 +180,6 @@ function prepareLookupEditor(options): void { } if (isObject(dataSource) || Array.isArray(dataSource)) { - // @ts-expect-error ts- dataSource = normalizeDataSourceOptions(dataSource); if (isFilterRow) { postProcess = dataSource.postProcess; diff --git a/packages/devextreme/js/common/data/array_utils.js b/packages/devextreme/js/common/data/array_utils.js index 74993adf03d4..d3f8e91646bc 100644 --- a/packages/devextreme/js/common/data/array_utils.js +++ b/packages/devextreme/js/common/data/array_utils.js @@ -1 +1 @@ -export * from '../../__internal/data/m_array_utils'; +export * from '../../__internal/data/array_utils'; diff --git a/packages/devextreme/js/common/data/data_source/operation_manager.js b/packages/devextreme/js/common/data/data_source/operation_manager.js index 059f55b8e65f..6de10df1d202 100644 --- a/packages/devextreme/js/common/data/data_source/operation_manager.js +++ b/packages/devextreme/js/common/data/data_source/operation_manager.js @@ -1 +1 @@ -export { default } from '../../../__internal/data/data_source/m_operation_manager'; +export { default } from '../../../__internal/data/data_source/operation_manager'; diff --git a/packages/devextreme/js/common/data/data_source/utils.js b/packages/devextreme/js/common/data/data_source/utils.js index 8b2686832ca6..52e070da838e 100644 --- a/packages/devextreme/js/common/data/data_source/utils.js +++ b/packages/devextreme/js/common/data/data_source/utils.js @@ -1 +1 @@ -export * from '../../../__internal/data/data_source/m_utils'; +export * from '../../../__internal/data/data_source/utils'; diff --git a/packages/devextreme/js/common/data/endpoint_selector.js b/packages/devextreme/js/common/data/endpoint_selector.js index e613c011c0ce..ea7b56fad2d1 100644 --- a/packages/devextreme/js/common/data/endpoint_selector.js +++ b/packages/devextreme/js/common/data/endpoint_selector.js @@ -4,4 +4,4 @@ * @param1 options:Object * @hidden */ -export { default } from '../../__internal/data/m_endpoint_selector'; +export { default } from '../../__internal/data/endpoint_selector'; diff --git a/packages/devextreme/js/common/data/errors.js b/packages/devextreme/js/common/data/errors.js index f2ad9f675151..255b551e3774 100644 --- a/packages/devextreme/js/common/data/errors.js +++ b/packages/devextreme/js/common/data/errors.js @@ -88,4 +88,4 @@ * @name ErrorsData.W4002 */ -export * from '../../__internal/data/m_errors'; +export * from '../../__internal/data/errors'; diff --git a/packages/devextreme/js/common/data/local_store.js b/packages/devextreme/js/common/data/local_store.js index 2b903d32d98b..48ec3d1bcc87 100644 --- a/packages/devextreme/js/common/data/local_store.js +++ b/packages/devextreme/js/common/data/local_store.js @@ -1 +1 @@ -export { default } from '../../__internal/data/m_local_store'; +export { default } from '../../__internal/data/local_store'; diff --git a/packages/devextreme/js/common/data/odata/query_adapter.js b/packages/devextreme/js/common/data/odata/query_adapter.js index a56104665e6c..dc61288953ee 100644 --- a/packages/devextreme/js/common/data/odata/query_adapter.js +++ b/packages/devextreme/js/common/data/odata/query_adapter.js @@ -1 +1 @@ -export * from '../../../__internal/data/odata/m_query_adapter'; +export * from '../../../__internal/data/odata/query_adapter'; diff --git a/packages/devextreme/js/common/data/odata/request_dispatcher.js b/packages/devextreme/js/common/data/odata/request_dispatcher.js index 13fc3dc1b4c6..434e13d5a8eb 100644 --- a/packages/devextreme/js/common/data/odata/request_dispatcher.js +++ b/packages/devextreme/js/common/data/odata/request_dispatcher.js @@ -1 +1 @@ -export { default } from '../../../__internal/data/odata/m_request_dispatcher'; +export { default } from '../../../__internal/data/odata/request_dispatcher'; diff --git a/packages/devextreme/js/common/data/query.js b/packages/devextreme/js/common/data/query.js index b1e2c444e038..bbb50289ac52 100644 --- a/packages/devextreme/js/common/data/query.js +++ b/packages/devextreme/js/common/data/query.js @@ -1 +1 @@ -export { default } from '../../__internal/data/m_query'; +export { default } from '../../__internal/data/query'; diff --git a/packages/devextreme/js/common/data/remote_query.js b/packages/devextreme/js/common/data/remote_query.js index 592ebfe65d2c..51f3df760e1e 100644 --- a/packages/devextreme/js/common/data/remote_query.js +++ b/packages/devextreme/js/common/data/remote_query.js @@ -1 +1 @@ -export { default } from '../../__internal/data/m_remote_query'; +export { default } from '../../__internal/data/remote_query'; diff --git a/packages/devextreme/js/common/data/store_helper.js b/packages/devextreme/js/common/data/store_helper.js index bb0fca23c7d1..5b66fa3425b1 100644 --- a/packages/devextreme/js/common/data/store_helper.js +++ b/packages/devextreme/js/common/data/store_helper.js @@ -1 +1 @@ -export { default } from '../../__internal/data/m_store_helper'; +export { default } from '../../__internal/data/store_helper'; diff --git a/packages/devextreme/js/common/data/utils.js b/packages/devextreme/js/common/data/utils.js index a59212919375..ba1bc28b4c72 100644 --- a/packages/devextreme/js/common/data/utils.js +++ b/packages/devextreme/js/common/data/utils.js @@ -3,7 +3,7 @@ import { compileGetter, compileSetter } from '../../core/utils/data'; /** * @name Utils */ -export * from '../../__internal/data/m_utils'; +export * from '../../__internal/data/utils'; export { compileGetter,