diff --git a/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts b/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts index 8e06ae78e816..4d8ffa980941 100644 --- a/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts +++ b/packages/devextreme/js/__internal/data/odata/m_query_adapter.ts @@ -106,7 +106,7 @@ const compileCriteria = (() => { return formatter( serializePropName(fieldName), - serializeValue(value, protocolVersion), + serializeValue(value, protocolVersion, fieldTypes?.[fieldName]), ); }; @@ -277,7 +277,7 @@ const createODataQueryAdapter = (queryOptions) => { jsonp: queryOptions.jsonp, withCredentials: queryOptions.withCredentials, countOnly: _countQuery, - deserializeDates: queryOptions.deserializeDates, + processDatesAsUtc: queryOptions.processDatesAsUtc, fieldTypes: queryOptions.fieldTypes, isPaged: isFinite(_take), }, diff --git a/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts b/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts index c1dbe9f8ca6b..e37f10217123 100644 --- a/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts +++ b/packages/devextreme/js/__internal/data/odata/m_request_dispatcher.ts @@ -18,7 +18,7 @@ export default class RequestDispatcher { // @ts-expect-error this._withCredentials = options.withCredentials; // @ts-expect-error - this._deserializeDates = options.deserializeDates; + this._processDatesAsUtc = options.processDatesAsUtc ?? options.deserializeDates ?? false; // @ts-expect-error this._filterToLower = options.filterToLower; } @@ -40,7 +40,7 @@ export default class RequestDispatcher { // @ts-expect-error withCredentials: this._withCredentials, // @ts-expect-error - deserializeDates: this._deserializeDates, + processDatesAsUtc: this._processDatesAsUtc, }, ); } diff --git a/packages/devextreme/js/__internal/data/odata/m_store.ts b/packages/devextreme/js/__internal/data/odata/m_store.ts index 9bf2c95863d0..6cbab5cf0882 100644 --- a/packages/devextreme/js/__internal/data/odata/m_store.ts +++ b/packages/devextreme/js/__internal/data/odata/m_store.ts @@ -106,7 +106,7 @@ const ODataStore = Store.inherit({ withCredentials: this._requestDispatcher._withCredentials, expand: loadOptions?.expand, requireTotalCount: loadOptions?.requireTotalCount, - deserializeDates: this._requestDispatcher._deserializeDates, + processDatesAsUtc: this._requestDispatcher._processDatesAsUtc, fieldTypes: this._fieldTypes, }; diff --git a/packages/devextreme/js/__internal/data/odata/m_utils.ts b/packages/devextreme/js/__internal/data/odata/m_utils.ts index cf043c0de3c9..00a58e780c79 100644 --- a/packages/devextreme/js/__internal/data/odata/m_utils.ts +++ b/packages/devextreme/js/__internal/data/odata/m_utils.ts @@ -210,7 +210,7 @@ const ajaxOptionsForRequest = (protocolVersion, request, options = {}) => { export const sendRequest = (protocolVersion, request, options) => { const { - deserializeDates, fieldTypes, countOnly, isPaged, + processDatesAsUtc, fieldTypes, countOnly, isPaged, } = options; // @ts-expect-error const d = new Deferred(); @@ -218,7 +218,7 @@ export const sendRequest = (protocolVersion, request, options) => { ajax.sendRequest(ajaxOptions).always((obj, textStatus) => { const transformOptions = { - deserializeDates, + processDatesAsUtc, fieldTypes, }; const tuple = interpretJsonFormat(obj, textStatus, transformOptions, ajaxOptions); @@ -387,14 +387,14 @@ const transformTypes = (obj, options = {}) => { transformTypes(obj[key], options); } else if (typeof value === 'string') { // @ts-expect-error - const { fieldTypes, deserializeDates } = options; + const { fieldTypes, processDatesAsUtc } = options; const canBeGuid = !fieldTypes || fieldTypes[key] !== 'String'; if (canBeGuid && GUID_REGEX.test(value)) { obj[key] = new Guid(value); } - if (deserializeDates !== false) { + if (processDatesAsUtc !== false) { if (VERBOSE_DATE_REGEX.exec(value)) { // @ts-expect-error const date = new Date(Number(RegExp.$1) + RegExp.$2 * 60 * 1000); @@ -415,7 +415,7 @@ export const serializePropName = (propName) => (propName instanceof EdmLiteral ? propName.valueOf() : propName.replace(/\./g, '/')); -const serializeValueV4 = (value) => { +const serializeValueV4 = (value, fieldType) => { if (value instanceof Date) { return formatISO8601(value, false, false); } @@ -423,12 +423,12 @@ const serializeValueV4 = (value) => { return value.valueOf(); } if (Array.isArray(value)) { - return `[${value.map((item) => serializeValueV4(item)).join(',')}]`; + return `[${value.map((item) => serializeValueV4(item, fieldType)).join(',')}]`; } - return serializeValueV2(value); + return serializeValueV2(value, fieldType); }; -const serializeValueV2 = (value) => { +const serializeValueV2 = (value, fieldType) => { if (value instanceof Date) { return serializeDate(value); } @@ -438,19 +438,22 @@ const serializeValueV2 = (value) => { if (value instanceof EdmLiteral) { return value.valueOf(); } + if (fieldType && ['Date', 'DateTimeOffset'].includes(fieldType)) { + return value; + } if (typeof value === 'string') { return serializeString(value); } return String(value); }; -export const serializeValue = (value, protocolVersion) => { +export const serializeValue = (value, protocolVersion, fieldType?) => { switch (protocolVersion) { case 2: case 3: - return serializeValueV2(value); + return serializeValueV2(value, fieldType); case 4: - return serializeValueV4(value); + return serializeValueV4(value, fieldType); default: throw errors.Error('E4002'); } }; @@ -480,6 +483,10 @@ export const keyConverters = { Single: (value) => (value instanceof EdmLiteral ? value : new EdmLiteral(`${value}f`)), Decimal: (value) => (value instanceof EdmLiteral ? value : new EdmLiteral(`${value}m`)), + + DateTimeOffset: (value) => value, + + Date: (value) => value, }; export const convertPrimitiveValue = (type, value) => { diff --git a/packages/devextreme/js/common/data.d.ts b/packages/devextreme/js/common/data.d.ts index 756cc76b9db7..935ecde3cd98 100644 --- a/packages/devextreme/js/common/data.d.ts +++ b/packages/devextreme/js/common/data.d.ts @@ -1130,8 +1130,16 @@ export type ODataContextOptions = { /** * @docid * @public + * @default false + * @deprecated ODataContextOptions.processDatesAsUtc */ deserializeDates?: boolean; + /** + * @docid ODataContextOptions.processDatesAsUtc + * @public + * @default false + */ + processDatesAsUtc?: boolean; /** * @docid * @public @@ -1232,8 +1240,16 @@ export type ODataStoreOptions< /** * @docid * @public + * @default false + * @deprecated ODataStoreOptions.processDatesAsUtc */ deserializeDates?: boolean; + /** + * @docid ODataStoreOptions.processDatesAsUtc + * @public + * @default false + */ + processDatesAsUtc?: boolean; /** * @docid * @type_function_param1 e:Error @@ -1247,7 +1263,9 @@ export type ODataStoreOptions< * @default {} * @public */ - fieldTypes?: any; + fieldTypes?: { + [fieldName: string]: 'String' | 'Int32' | 'Int64' | 'Guid' | 'Boolean' | 'Single' | 'Decimal' | 'Date' | 'DateTimeOffset'; + }; /** * @docid * @public diff --git a/packages/devextreme/testing/tests/DevExpress.data/odataQuery.tests.js b/packages/devextreme/testing/tests/DevExpress.data/odataQuery.tests.js index ae4afe0e4af7..1537d5a77b3a 100644 --- a/packages/devextreme/testing/tests/DevExpress.data/odataQuery.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.data/odataQuery.tests.js @@ -916,16 +916,27 @@ QUnit.test('Values are converted according to \'fieldTypes\' property', function fieldTypes: { id1: 'Int64', name: 'String', - total: 'Decimal' // T566307 + total: 'Decimal', // T566307 + date: 'Date', + dateTime: 'DateTimeOffset' } }) - .filter([['id1', '=', 123], 'and', ['id2', '=', 456], 'and', ['name', '=', 789], 'and', ['total', '=', null]]) + .filter([ + ['id1', '=', 123], 'and', + ['id2', '=', 456], 'and', + ['name', '=', 789], 'and', + ['total', '=', null], 'and', + ['date', '=', '2025-11-11'], 'and', + ['dateTime', '=', '2025-11-11T11:11:11.000Z'], 'and', + ['dateText', '=', '2025-11-11'], 'and', + ['dateTimeText', '=', '2025-11-11T11:11:11.000Z'], + ]) .enumerate() .fail(function() { assert.ok(false, MUST_NOT_REACH_MESSAGE); }) .done(function(r) { - assert.equal(r[0].data['$filter'], '(id1 eq 123L) and (id2 eq 456) and (name eq \'789\') and (total eq null)'); + assert.equal(r[0].data['$filter'], '(id1 eq 123L) and (id2 eq 456) and (name eq \'789\') and (total eq null) and (date eq 2025-11-11) and (dateTime eq 2025-11-11T11:11:11.000Z) and (dateText eq \'2025-11-11\') and (dateTimeText eq \'2025-11-11T11:11:11.000Z\')'); }) .always(done); }); diff --git a/packages/devextreme/testing/tests/DevExpress.data/odataStore.tests.js b/packages/devextreme/testing/tests/DevExpress.data/odataStore.tests.js index 35645c8287b6..e3c12cdabe10 100644 --- a/packages/devextreme/testing/tests/DevExpress.data/odataStore.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.data/odataStore.tests.js @@ -1718,7 +1718,7 @@ QUnit.test('Dates, on updating', function(assert) { }); QUnit.module('Deserialization', moduleConfig); -QUnit.test('Dates, disableable, ODataStore', function(assert) { +QUnit.test('Dates, default behaviour (processDatesAsUtc = false), ODataStore', function(assert) { assert.expect(2); const done = assert.async(); @@ -1733,7 +1733,7 @@ QUnit.test('Dates, disableable, ODataStore', function(assert) { responseText: { dateProperty: '1945-05-09T14:25:12.1234567Z' } }); - const store = new ODataStore({ version: 4, url: 'odata.org', deserializeDates: false }); + const store = new ODataStore({ version: 4, url: 'odata.org' }); const promises = [ store.load() .done(function(r) { @@ -1751,7 +1751,7 @@ QUnit.test('Dates, disableable, ODataStore', function(assert) { .always(done); }); -QUnit.test('Dates, disableable, ODataContext', function(assert) { +QUnit.test('Dates, default behaviour (processDatesAsUtc = false) and fieldType of dateProperty is undefined, ODataContext', function(assert) { assert.expect(4); const done = assert.async(); @@ -1774,10 +1774,9 @@ QUnit.test('Dates, disableable, ODataContext', function(assert) { const ctx = new ODataContext({ version: 4, url: 'odata.org', - deserializeDates: false, entities: { 'X': { name: 'name' }, - 'Y': { name: 'name', deserializeDates: true } + 'Y': { name: 'name', processDatesAsUtc: true } } }); @@ -1808,6 +1807,63 @@ QUnit.test('Dates, disableable, ODataContext', function(assert) { .always(done); }); +QUnit.test('Dates, processDatesAsUtc = true, ODataContext', function(assert) { + assert.expect(4); + + const done = assert.async(); + + ajaxMock.setup({ + url: 'odata.org/name', + responseText: { value: [{ dateProperty: '1945-05-09T14:25:12.1234567Z' }] } + }); + + ajaxMock.setup({ + url: 'odata.org/function()', + responseText: { dateProperty: '1945-05-09T14:25:12.1234567Z' } + }); + + ajaxMock.setup({ + url: 'odata.org/action', + responseText: { dateProperty: '1945-05-09T14:25:12.1234567Z' } + }); + + const ctx = new ODataContext({ + version: 4, + url: 'odata.org', + processDatesAsUtc: true, + entities: { + 'X': { name: 'name' }, + 'Y': { name: 'name' } + } + }); + + const promises = [ + ctx.get('function') + .done(function(r) { + assert.ok(isDate(r.dateProperty)); + }), + + ctx.invoke('action') + .done(function(r) { + assert.ok(isDate(r.dateProperty)); + }), + + ctx.X.load() + .done(function(r) { + assert.ok(isDate(r[0].dateProperty)); + }), + + ctx.Y.load() + .done(function(r) { + assert.ok(isDate(r[0].dateProperty)); + }) + ]; + + $.when.apply($, promises) + .fail(function() { assert.ok(false, MUST_NOT_REACH_MESSAGE); }) + .always(done); +}); + QUnit.module('JSONP support', moduleConfig); QUnit.test('load()', function(assert) { const done = assert.async(); diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index a42643d2366b..758093386e99 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -3801,8 +3801,13 @@ declare module DevExpress.common.data { }) => void; /** * [descr:ODataContextOptions.deserializeDates] + * @deprecated [depNote:ODataContextOptions.deserializeDates] */ deserializeDates?: boolean; + /** + * [descr:ODataContextOptions.processDatesAsUtc] + */ + processDatesAsUtc?: boolean; /** * [descr:ODataContextOptions.entities] */ @@ -3884,8 +3889,13 @@ declare module DevExpress.common.data { }) => void; /** * [descr:ODataStoreOptions.deserializeDates] + * @deprecated [depNote:ODataStoreOptions.deserializeDates] */ deserializeDates?: boolean; + /** + * [descr:ODataStoreOptions.processDatesAsUtc] + */ + processDatesAsUtc?: boolean; /** * [descr:ODataStoreOptions.errorHandler] */ @@ -3897,7 +3907,18 @@ declare module DevExpress.common.data { /** * [descr:ODataStoreOptions.fieldTypes] */ - fieldTypes?: any; + fieldTypes?: { + [fieldName: string]: + | 'String' + | 'Int32' + | 'Int64' + | 'Guid' + | 'Boolean' + | 'Single' + | 'Decimal' + | 'Date' + | 'DateTimeOffset'; + }; /** * [descr:ODataStoreOptions.filterToLower] */