Revert "refactor: datetime field support timezone, defaultToCurrentTime, and onUpdateToCurrentTime (#5012)"
Some checks failed
Build Docker Image / build-and-push (push) Waiting to run
Build Pro Image / build-and-push (push) Waiting to run
E2E / Build (push) Waiting to run
E2E / Core and plugins (push) Blocked by required conditions
E2E / plugin-workflow (push) Blocked by required conditions
E2E / plugin-workflow-approval (push) Blocked by required conditions
E2E / plugin-data-source-main (push) Blocked by required conditions
E2E / Comment on PR (push) Blocked by required conditions
NocoBase Backend Test / sqlite-test (20, false) (push) Waiting to run
NocoBase Backend Test / sqlite-test (20, true) (push) Waiting to run
NocoBase Backend Test / postgres-test (public, 20, nocobase, false) (push) Waiting to run
NocoBase Backend Test / postgres-test (public, 20, nocobase, true) (push) Waiting to run
NocoBase Backend Test / postgres-test (public, 20, public, false) (push) Waiting to run
NocoBase Backend Test / postgres-test (public, 20, public, true) (push) Waiting to run
NocoBase Backend Test / postgres-test (user_schema, 20, nocobase, false) (push) Waiting to run
NocoBase Backend Test / postgres-test (user_schema, 20, nocobase, true) (push) Waiting to run
NocoBase Backend Test / postgres-test (user_schema, 20, public, false) (push) Waiting to run
NocoBase Backend Test / postgres-test (user_schema, 20, public, true) (push) Waiting to run
NocoBase Backend Test / mysql-test (20, false) (push) Waiting to run
NocoBase Backend Test / mysql-test (20, true) (push) Waiting to run
NocoBase Backend Test / mariadb-test (20, false) (push) Waiting to run
NocoBase Backend Test / mariadb-test (20, true) (push) Waiting to run
Test on Windows / build (push) Waiting to run
NocoBase FrontEnd Test / frontend-test (18) (push) Has been cancelled

This reverts commit e232ed7582.
This commit is contained in:
Chareice 2024-08-19 11:03:10 +08:00
parent 0b5e43df8a
commit ded5f26c09
No known key found for this signature in database
24 changed files with 155 additions and 670 deletions

View File

@ -52,7 +52,6 @@ import {
UUIDFieldInterface, UUIDFieldInterface,
NanoidFieldInterface, NanoidFieldInterface,
UnixTimestampFieldInterface, UnixTimestampFieldInterface,
DateFieldInterface,
} from './interfaces'; } from './interfaces';
import { import {
GeneralCollectionTemplate, GeneralCollectionTemplate,
@ -174,7 +173,6 @@ export class CollectionPlugin extends Plugin {
UUIDFieldInterface, UUIDFieldInterface,
NanoidFieldInterface, NanoidFieldInterface,
UnixTimestampFieldInterface, UnixTimestampFieldInterface,
DateFieldInterface,
]); ]);
} }

View File

@ -7,8 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { Switch, Radio, Input } from 'antd'; import { Switch } from 'antd';
import React, { useEffect, useState } from 'react'; import React from 'react';
export const TargetKey = () => { export const TargetKey = () => {
return <div>Target key</div>; return <div>Target key</div>;
@ -50,37 +50,3 @@ export const ForeignKey2 = () => {
</div> </div>
); );
}; };
// 自定义 Radio 组件
export const CustomRadio = (props) => {
const { options, onChange } = props;
const [value, setValue] = useState(props.value);
useEffect(() => {
setValue(['server', 'client'].includes(props.value) ? props.value : 'custom');
}, [props.value]);
const handleRadioChange = (e) => {
setValue(e.target.value);
if (e.target.value !== 'custom') {
onChange?.(e.target.value);
}
};
return (
<Radio.Group onChange={handleRadioChange} value={value}>
{options.map((option) => (
<Radio key={option.value} value={option.value}>
{option.label}
{option.value === 'custom' && value === 'custom' ? (
<Input
style={{ width: 200, marginLeft: 10 }}
onChange={(e) => {
onChange?.(e.target.value);
}}
value={['server', 'client', 'custom'].includes(props.value) ? null : props.value}
/>
) : null}
</Radio>
))}
</Radio.Group>
);
};

View File

@ -1,60 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { dateTimeProps, defaultProps, operators } from './properties';
export class DateFieldInterface extends CollectionFieldInterface {
name = 'date';
type = 'object';
group = 'datetime';
order = 1;
title = '{{t("Date")}}';
sortable = true;
default = {
type: 'dateOnly',
uiSchema: {
type: 'string',
'x-component': 'DatePicker',
'x-component-props': {
dateOnly: true,
},
},
};
availableTypes = ['date', 'dateOnly', 'string'];
hasDefaultValue = true;
properties = {
...defaultProps,
'uiSchema.x-component-props.dateFormat': {
type: 'string',
title: '{{t("Date format")}}',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
default: 'YYYY-MM-DD',
enum: [
{
label: '{{t("Year/Month/Day")}}',
value: 'YYYY/MM/DD',
},
{
label: '{{t("Year-Month-Day")}}',
value: 'YYYY-MM-DD',
},
{
label: '{{t("Day/Month/Year")}}',
value: 'DD/MM/YYYY',
},
],
},
};
filterable = {
operators: operators.datetime,
};
titleUsable = true;
}

View File

@ -19,9 +19,6 @@ export class DatetimeFieldInterface extends CollectionFieldInterface {
sortable = true; sortable = true;
default = { default = {
type: 'date', type: 'date',
defaultToCurrentTime: false,
onUpdateToCurrentTime: false,
timezone: 'server',
uiSchema: { uiSchema: {
type: 'string', type: 'string',
'x-component': 'DatePicker', 'x-component': 'DatePicker',

View File

@ -46,4 +46,3 @@ export * from './sort';
export * from './uuid'; export * from './uuid';
export * from './nanoid'; export * from './nanoid';
export * from './unixTimestamp'; export * from './unixTimestamp';
export * from './date';

View File

@ -10,7 +10,6 @@
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { CustomRadio } from '../components';
export * as operators from './operators'; export * as operators from './operators';
export const type: ISchema = { export const type: ISchema = {
@ -226,29 +225,6 @@ export const reverseFieldProperties: Record<string, ISchema> = {
}; };
export const dateTimeProps: { [key: string]: ISchema } = { export const dateTimeProps: { [key: string]: ISchema } = {
timezone: {
type: 'string',
title: '{{t("Timezone")}}',
'x-component': CustomRadio,
'x-decorator': 'FormItem',
default: 'server',
'x-component-props': {
options: [
{
label: '{{t("None")}}',
value: 'server',
},
{
label: '{{t("Client\'s time zone")}}',
value: 'client',
},
{
label: '{{t("Custom")}}',
value: 'custom',
},
],
},
},
'uiSchema.x-component-props.dateFormat': { 'uiSchema.x-component-props.dateFormat': {
type: 'string', type: 'string',
title: '{{t("Date format")}}', title: '{{t("Date format")}}',
@ -277,10 +253,10 @@ export const dateTimeProps: { [key: string]: ISchema } = {
'x-content': '{{t("Show time")}}', 'x-content': '{{t("Show time")}}',
'x-reactions': [ 'x-reactions': [
`{{(field) => { `{{(field) => {
field.query('..[].timeFormat').take(f => { field.query('..[].timeFormat').take(f => {
f.display = field.value ? 'visible' : 'none'; f.display = field.value ? 'visible' : 'none';
}); });
}}}`, }}}`,
], ],
}, },
'uiSchema.x-component-props.timeFormat': { 'uiSchema.x-component-props.timeFormat': {
@ -300,18 +276,6 @@ export const dateTimeProps: { [key: string]: ISchema } = {
}, },
], ],
}, },
defaultToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Default value to current time")}}',
},
onUpdateToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Automatically update timestamp on update")}}',
},
}; };
export const dataSource: ISchema = { export const dataSource: ISchema = {

View File

@ -8,8 +8,8 @@
*/ */
import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface';
import { defaultProps, operators } from './properties'; import { dateTimeProps, defaultProps, operators } from './properties';
import { CustomRadio } from './components';
export class UnixTimestampFieldInterface extends CollectionFieldInterface { export class UnixTimestampFieldInterface extends CollectionFieldInterface {
name = 'unixTimestamp'; name = 'unixTimestamp';
type = 'object'; type = 'object';
@ -18,47 +18,21 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface {
title = '{{t("Unix Timestamp")}}'; title = '{{t("Unix Timestamp")}}';
sortable = true; sortable = true;
default = { default = {
type: 'unixTimestamp', type: 'bigInt',
accuracy: 'second',
timezone: 'server',
defaultToCurrentTime: false,
onUpdateToCurrentTime: false,
uiSchema: { uiSchema: {
type: 'number', type: 'number',
'x-component': 'UnixTimestamp', 'x-component': 'UnixTimestamp',
'x-component-props': { 'x-component-props': {
accuracy: 'second',
showTime: true, showTime: true,
}, },
}, },
}; };
availableTypes = ['integer', 'bigInt', 'unixTimestamp']; availableTypes = ['integer', 'bigInt'];
hasDefaultValue = false; hasDefaultValue = true;
properties = { properties = {
...defaultProps, ...defaultProps,
timezone: { 'uiSchema.x-component-props.accuracy': {
type: 'string',
title: '{{t("Timezone")}}',
'x-component': CustomRadio,
'x-decorator': 'FormItem',
default: 'server',
'x-component-props': {
options: [
{
label: '{{t("None")}}',
value: 'server',
},
{
label: '{{t("Client\'s time zone")}}',
value: 'client',
},
{
label: 'custom',
value: 'custom',
},
],
},
},
accuracy: {
type: 'string', type: 'string',
title: '{{t("Accuracy")}}', title: '{{t("Accuracy")}}',
'x-component': 'Radio.Group', 'x-component': 'Radio.Group',
@ -69,20 +43,6 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface {
{ value: 'second', label: '{{t("Second")}}' }, { value: 'second', label: '{{t("Second")}}' },
], ],
}, },
defaultToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Default value to current time")}}',
default: true,
},
onUpdateToCurrentTime: {
type: 'boolean',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-content': '{{t("Automatically update timestamp on update")}}',
default: true,
},
}; };
filterable = { filterable = {
operators: operators.number, operators: operators.number,

View File

@ -283,7 +283,7 @@
"Checkbox group": "复选框", "Checkbox group": "复选框",
"China region": "中国行政区", "China region": "中国行政区",
"Date & Time": "日期 & 时间", "Date & Time": "日期 & 时间",
"Datetime": "日期时间", "Datetime": "日期",
"Relation": "关系类型", "Relation": "关系类型",
"Link to": "关联", "Link to": "关联",
"Link to description": "用于快速创建表关系,可兼容大多数普通场景。适合非开发人员使用。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,将同时在目标数据表中生成当前数据表的关联字段。", "Link to description": "用于快速创建表关系,可兼容大多数普通场景。适合非开发人员使用。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,将同时在目标数据表中生成当前数据表的关联字段。",
@ -967,8 +967,5 @@
"Clear default value": "清除默认值", "Clear default value": "清除默认值",
"Open in new window": "新窗口打开", "Open in new window": "新窗口打开",
"Sorry, the page you visited does not exist.": "抱歉,你访问的页面不存在。", "Sorry, the page you visited does not exist.": "抱歉,你访问的页面不存在。",
"Template engine": "模板引擎", "Set Template Engine": "设置模板引擎"
"Default value to current time":"设置字段默认值为当前时间",
"Automatically update timestamp on update":"当记录更新时自动设置字段值为当前时间",
"Client's time zone":"客户端时区"
} }

View File

@ -78,20 +78,17 @@ export const mapDatePicker = function () {
return (props: any) => { return (props: any) => {
const format = getDefaultFormat(props) as any; const format = getDefaultFormat(props) as any;
const onChange = props.onChange; const onChange = props.onChange;
return { return {
...props, ...props,
format: format, format: format,
value: str2moment(props.value, props), value: str2moment(props.value, props),
onChange: (value: Dayjs | null, dateString) => { onChange: (value: Dayjs | null) => {
if (onChange) { if (onChange) {
if (!props.showTime && value) { if (!props.showTime && value) {
value = value.startOf('day'); value = value.startOf('day');
} }
if (props.dateOnly) { onChange(moment2str(value, props));
onChange(dateString);
} else {
onChange(moment2str(value, props));
}
} }
}, },
}; };

View File

@ -8,32 +8,57 @@
*/ */
import { connect, mapReadPretty } from '@formily/react'; import { connect, mapReadPretty } from '@formily/react';
import React from 'react'; import React, { useMemo } from 'react';
import { DatePicker } from '../date-picker'; import { DatePicker } from '../date-picker';
import dayjs from 'dayjs';
const toValue = (value: any, accuracy) => {
if (value) {
return timestampToDate(value, accuracy);
}
return null;
};
function timestampToDate(timestamp, accuracy = 'millisecond') {
if (accuracy === 'second') {
timestamp *= 1000; // 如果精确度是秒级则将时间戳乘以1000转换为毫秒级
}
return dayjs(timestamp);
}
function getTimestamp(date, accuracy = 'millisecond') {
if (accuracy === 'second') {
return dayjs(date).unix();
} else {
return dayjs(date).valueOf(); // 默认返回毫秒级时间戳
}
}
interface UnixTimestampProps { interface UnixTimestampProps {
value?: any; value?: number;
accuracy?: 'millisecond' | 'second';
onChange?: (value: number) => void; onChange?: (value: number) => void;
} }
export const UnixTimestamp = connect( export const UnixTimestamp = connect(
(props: UnixTimestampProps) => { (props: UnixTimestampProps) => {
const { value, onChange } = props; const { value, onChange, accuracy = 'second' } = props;
const v = useMemo(() => toValue(value, accuracy), [value, accuracy]);
return ( return (
<DatePicker <DatePicker
{...props} {...props}
value={value} value={v}
onChange={(v: any) => { onChange={(v: any) => {
if (onChange) { if (onChange) {
onChange(v); onChange(getTimestamp(v, accuracy));
} }
}} }}
/> />
); );
}, },
mapReadPretty((props) => { mapReadPretty((props) => {
const { value } = props; const { value, accuracy = 'second' } = props;
return <DatePicker.ReadPretty {...props} value={value} />; const v = useMemo(() => toValue(value, accuracy), [value, accuracy]);
return <DatePicker.ReadPretty {...props} value={v} />;
}), }),
); );

View File

@ -13,9 +13,11 @@ import { UnixTimestamp } from '@nocobase/client';
describe('UnixTimestamp', () => { describe('UnixTimestamp', () => {
it('renders without errors', async () => { it('renders without errors', async () => {
const { container } = await renderAppOptions({ const { container } = await renderAppOptions({
Component: UnixTimestamp as any, Component: UnixTimestamp,
props: {}, props: {
value: null, accuracy: 'millisecond',
},
value: 0,
}); });
expect(container).toMatchInlineSnapshot(` expect(container).toMatchInlineSnapshot(`
<div> <div>
@ -67,10 +69,78 @@ describe('UnixTimestamp', () => {
`); `);
}); });
it('millisecond', async () => {
await renderAppOptions({
Component: UnixTimestamp,
value: 1712819630000,
props: {
accuracy: 'millisecond',
},
});
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
});
});
it('second', async () => {
await renderAppOptions({
Component: UnixTimestamp,
value: 1712819630,
props: {
accuracy: 'second',
},
});
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
});
});
it('string', async () => {
await renderAppOptions({
Component: UnixTimestamp,
value: '2024-04-11',
props: {
accuracy: 'millisecond',
},
});
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveValue('2024-04-11');
});
});
it('change', async () => {
const onChange = vitest.fn();
await renderAppOptions({
Component: UnixTimestamp,
value: '2024-04-11',
onChange,
props: {
accuracy: 'millisecond',
},
});
await userEvent.click(screen.getByRole('textbox'));
await waitFor(() => {
expect(screen.queryByRole('table')).toBeInTheDocument();
});
await userEvent.click(document.querySelector('td[title="2024-04-12"]'));
await waitFor(() => {
expect(screen.getByRole('textbox')).toHaveValue('2024-04-12');
});
expect(onChange).toBeCalledWith(1712880000000);
});
it('read pretty', async () => { it('read pretty', async () => {
const { container } = await renderReadPrettyApp({ const { container } = await renderReadPrettyApp({
Component: UnixTimestamp as any, Component: UnixTimestamp,
value: '2024-04-11', value: '2024-04-11',
props: {
accuracy: 'millisecond',
},
}); });
expect(screen.getByText('2024-04-11')).toBeInTheDocument(); expect(screen.getByText('2024-04-11')).toBeInTheDocument();

View File

@ -1,42 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Database, mockDatabase } from '@nocobase/database';
describe('date only', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase({
timezone: '+08:00',
});
await db.clean({ drop: true });
});
afterEach(async () => {
await db.close();
});
it('should set date field with dateOnly', async () => {
db.collection({
name: 'tests',
fields: [{ name: 'date1', type: 'dateOnly' }],
});
await db.sync();
const item = await db.getRepository('tests').create({
values: {
date1: '2023-03-24',
},
});
expect(item.get('date1')).toBe('2023-03-24');
});
});

View File

@ -11,69 +11,6 @@ import { mockDatabase } from '../';
import { Database } from '../../database'; import { Database } from '../../database';
import { Repository } from '../../repository'; import { Repository } from '../../repository';
describe('timezone', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase({
timezone: '+08:00',
});
await db.clean({ drop: true });
});
afterEach(async () => {
await db.close();
});
describe('timezone', () => {
test('custom', async () => {
db.collection({
name: 'tests',
timestamps: false,
fields: [{ name: 'date1', type: 'date', timezone: '+06:00' }],
});
await db.sync();
const repository = db.getRepository('tests');
const instance = await repository.create({ values: { date1: '2023-03-24 00:00:00' } });
const date1 = instance.get('date1');
expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z');
});
test('client', async () => {
db.collection({
name: 'tests',
timestamps: false,
fields: [{ name: 'date1', type: 'date', timezone: 'client' }],
});
await db.sync();
const repository = db.getRepository('tests');
const instance = await repository.create({
values: { date1: '2023-03-24 01:00:00' },
context: {
timezone: '+01:00',
},
});
const date1 = instance.get('date1');
expect(date1.toISOString()).toEqual('2023-03-24T00:00:00.000Z');
});
test('server', async () => {
db.collection({
name: 'tests',
fields: [{ name: 'date1', type: 'date', timezone: 'server' }],
});
await db.sync();
const repository = db.getRepository('tests');
const instance = await repository.create({ values: { date1: '2023-03-24 08:00:00' } });
const date1 = instance.get('date1');
expect(date1.toISOString()).toEqual('2023-03-24T00:00:00.000Z');
});
});
});
describe('date-field', () => { describe('date-field', () => {
let db: Database; let db: Database;
let repository: Repository; let repository: Repository;
@ -93,80 +30,16 @@ describe('date-field', () => {
await db.close(); await db.close();
}); });
it('should set default to current time', async () => { const createExpectToBe = async (key, actual, expected) => {
const c1 = db.collection({ const instance = await repository.create({
name: 'test11',
fields: [
{
name: 'date1',
type: 'date',
defaultToCurrentTime: true,
},
],
});
await db.sync();
const instance = await c1.repository.create({});
const date1 = instance.get('date1');
expect(date1).toBeDefined();
});
it('should set to current time when update', async () => {
const c1 = db.collection({
name: 'test11',
fields: [
{
name: 'date1',
type: 'date',
onUpdateToCurrentTime: true,
},
{
name: 'title',
type: 'string',
},
],
});
await db.sync();
const instance = await c1.repository.create({
values: { values: {
title: 'test', [key]: actual,
}, },
}); });
return expect(instance.get(key).toISOString()).toEqual(expected);
const date1Val = instance.get('date1'); };
expect(date1Val).toBeDefined();
console.log('update');
await c1.repository.update({
values: {
title: 'test2',
},
filter: {
id: instance.get('id'),
},
});
await instance.reload();
const date1Val2 = instance.get('date1');
expect(date1Val2).toBeDefined();
expect(date1Val2.getTime()).toBeGreaterThan(date1Val.getTime());
});
test('create', async () => { test('create', async () => {
const createExpectToBe = async (key, actual, expected) => {
const instance = await repository.create({
values: {
[key]: actual,
},
});
return expect(instance.get(key).toISOString()).toEqual(expected);
};
// sqlite 时区不能自定义,只有 +00:00postgres 和 mysql 可以自定义 DB_TIMEZONE // sqlite 时区不能自定义,只有 +00:00postgres 和 mysql 可以自定义 DB_TIMEZONE
await createExpectToBe('date1', '2023-03-24', '2023-03-24T00:00:00.000Z'); await createExpectToBe('date1', '2023-03-24', '2023-03-24T00:00:00.000Z');
await createExpectToBe('date1', '2023-03-24T16:00:00.000Z', '2023-03-24T16:00:00.000Z'); await createExpectToBe('date1', '2023-03-24T16:00:00.000Z', '2023-03-24T16:00:00.000Z');

View File

@ -1,86 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Database, mockDatabase } from '@nocobase/database';
import moment from 'moment';
describe('unix timestamp field', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase();
await db.clean({ drop: true });
});
afterEach(async () => {
await db.close();
});
it('should set default to current time', async () => {
const c1 = db.collection({
name: 'test11',
fields: [
{
name: 'date1',
type: 'unixTimestamp',
defaultToCurrentTime: true,
},
],
});
await db.sync();
const instance = await c1.repository.create({});
const date1 = instance.get('date1');
expect(date1).toBeDefined();
console.log(instance.toJSON());
});
it('should set date value', async () => {
const c1 = db.collection({
name: 'test12',
fields: [
{
name: 'date1',
type: 'unixTimestamp',
},
],
});
await db.sync();
await c1.repository.create({
values: {
date1: '2021-01-01T00:00:00Z',
},
});
const item = await c1.repository.findOne();
const val = item.get('date1');
const date = moment(val).utc().format('YYYY-MM-DD HH:mm:ss');
expect(date).toBe('2021-01-01 00:00:00');
});
describe('timezone', () => {
test('custom', async () => {
db.collection({
name: 'tests',
timestamps: false,
fields: [{ name: 'date1', type: 'unixTimestamp', timezone: '+06:00' }],
});
await db.sync();
const repository = db.getRepository('tests');
const instance = await repository.create({ values: { date1: '2023-03-24 00:00:00' } });
const date1 = instance.get('date1');
expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z');
});
});
});

View File

@ -34,6 +34,7 @@ import {
import { SequelizeStorage, Umzug } from 'umzug'; import { SequelizeStorage, Umzug } from 'umzug';
import { Collection, CollectionOptions, RepositoryType } from './collection'; import { Collection, CollectionOptions, RepositoryType } from './collection';
import { CollectionFactory } from './collection-factory'; import { CollectionFactory } from './collection-factory';
import { CollectionGroupManager } from './collection-group-manager';
import { ImporterReader, ImportFileExtension } from './collection-importer'; import { ImporterReader, ImportFileExtension } from './collection-importer';
import DatabaseUtils from './database-utils'; import DatabaseUtils from './database-utils';
import ReferencesMap from './features/references-map'; import ReferencesMap from './features/references-map';
@ -41,6 +42,7 @@ import { referentialIntegrityCheck } from './features/referential-integrity-chec
import { ArrayFieldRepository } from './field-repository/array-field-repository'; import { ArrayFieldRepository } from './field-repository/array-field-repository';
import * as FieldTypes from './fields'; import * as FieldTypes from './fields';
import { Field, FieldContext, RelationField } from './fields'; import { Field, FieldContext, RelationField } from './fields';
import { checkDatabaseVersion } from './helpers';
import { InheritedCollection } from './inherited-collection'; import { InheritedCollection } from './inherited-collection';
import InheritanceMap from './inherited-map'; import InheritanceMap from './inherited-map';
import { InterfaceManager } from './interface-manager'; import { InterfaceManager } from './interface-manager';
@ -219,9 +221,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
} }
} }
// @ts-ignore
opts.rawTimezone = opts.timezone;
if (options.dialect === 'sqlite') { if (options.dialect === 'sqlite') {
delete opts.timezone; delete opts.timezone;
} else if (!opts.timezone) { } else if (!opts.timezone) {
@ -849,8 +848,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
* @internal * @internal
*/ */
async checkVersion() { async checkVersion() {
return true; return await checkDatabaseVersion(this);
// return await checkDatabaseVersion(this);
} }
/** /**

View File

@ -10,14 +10,8 @@
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import { BaseColumnFieldOptions, Field } from './field'; import { BaseColumnFieldOptions, Field } from './field';
const datetimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
function isValidDatetime(str) {
return datetimeRegex.test(str);
}
export class DateField extends Field { export class DateField extends Field {
get dataType(): any { get dataType() {
return DataTypes.DATE(3); return DataTypes.DATE(3);
} }
@ -39,59 +33,6 @@ export class DateField extends Field {
return props.gmt; return props.gmt;
} }
init() {
const { name, defaultToCurrentTime, onUpdateToCurrentTime, timezone } = this.options;
this.resolveTimeZone = (context) => {
// @ts-ignore
const serverTimeZone = this.database.options.rawTimezone;
if (timezone === 'server') {
return serverTimeZone;
}
if (timezone === 'client') {
return context?.timezone || serverTimeZone;
}
if (timezone) {
return timezone;
}
return serverTimeZone;
};
this.beforeSave = async (instance, options) => {
const value = instance.get(name);
if (!value && instance.isNewRecord && defaultToCurrentTime) {
instance.set(name, new Date());
return;
}
if (onUpdateToCurrentTime) {
instance.set(name, new Date());
return;
}
};
}
setter(value, options) {
if (value === null) {
return value;
}
if (value instanceof Date) {
return value;
}
if (typeof value === 'string' && isValidDatetime(value)) {
const dateTimezone = this.resolveTimeZone(options?.context);
const dateString = `${value} ${dateTimezone}`;
return new Date(dateString);
}
return value;
}
bind() { bind() {
super.bind(); super.bind();
@ -110,13 +51,6 @@ export class DateField extends Field {
// @ts-ignore // @ts-ignore
model.refreshAttributes(); model.refreshAttributes();
} }
this.on('beforeSave', this.beforeSave);
}
unbind() {
super.unbind();
this.off('beforeSave', this.beforeSave);
} }
} }

View File

@ -1,21 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { BaseColumnFieldOptions, Field } from './field';
import { DataTypes } from 'sequelize';
export class DateOnlyField extends Field {
get dataType(): any {
return DataTypes.DATEONLY;
}
}
export interface DateOnlyFieldOptions extends BaseColumnFieldOptions {
type: 'dateOnly';
}

View File

@ -56,7 +56,7 @@ export abstract class Field {
return this.options.type; return this.options.type;
} }
abstract get dataType(): any; abstract get dataType();
isRelationField() { isRelationField() {
return false; return false;
@ -171,13 +171,11 @@ export abstract class Field {
Object.assign(opts, { type: this.database.sequelize.normalizeDataType(this.dataType) }); Object.assign(opts, { type: this.database.sequelize.normalizeDataType(this.dataType) });
} }
Object.assign(opts, this.additionalSequelizeOptions());
return opts; return opts;
} }
additionalSequelizeOptions() { isSqlite() {
return {}; return this.database.sequelize.getDialect() === 'sqlite';
} }
typeToString() { typeToString() {

View File

@ -36,8 +36,6 @@ import { UUIDFieldOptions } from './uuid-field';
import { VirtualFieldOptions } from './virtual-field'; import { VirtualFieldOptions } from './virtual-field';
import { NanoidFieldOptions } from './nanoid-field'; import { NanoidFieldOptions } from './nanoid-field';
import { EncryptionField } from './encryption-field'; import { EncryptionField } from './encryption-field';
import { UnixTimestampFieldOptions } from './unix-timestamp-field';
import { DateOnlyFieldOptions } from './date-only-field';
export * from './array-field'; export * from './array-field';
export * from './belongs-to-field'; export * from './belongs-to-field';
@ -45,7 +43,6 @@ export * from './belongs-to-many-field';
export * from './boolean-field'; export * from './boolean-field';
export * from './context-field'; export * from './context-field';
export * from './date-field'; export * from './date-field';
export * from './date-only-field';
export * from './field'; export * from './field';
export * from './has-many-field'; export * from './has-many-field';
export * from './has-one-field'; export * from './has-one-field';
@ -64,7 +61,6 @@ export * from './uuid-field';
export * from './virtual-field'; export * from './virtual-field';
export * from './nanoid-field'; export * from './nanoid-field';
export * from './encryption-field'; export * from './encryption-field';
export * from './unix-timestamp-field';
export type FieldOptions = export type FieldOptions =
| BaseFieldOptions | BaseFieldOptions
@ -85,8 +81,6 @@ export type FieldOptions =
| SetFieldOptions | SetFieldOptions
| TimeFieldOptions | TimeFieldOptions
| DateFieldOptions | DateFieldOptions
| DateOnlyFieldOptions
| UnixTimestampFieldOptions
| UidFieldOptions | UidFieldOptions
| UUIDFieldOptions | UUIDFieldOptions
| NanoidFieldOptions | NanoidFieldOptions

View File

@ -1,60 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { DataTypes } from 'sequelize';
import { DateField } from './date-field';
import { BaseColumnFieldOptions } from './field';
export class UnixTimestampField extends DateField {
get dataType() {
return DataTypes.BIGINT;
}
additionalSequelizeOptions(): {} {
const { name } = this.options;
let { accuracy } = this.options;
if (this.options?.uiSchema['x-component-props']?.accuracy) {
accuracy = this.options?.uiSchema['x-component-props']?.accuracy;
}
if (!accuracy) {
accuracy = 'second';
}
let rationalNumber = 1000;
if (accuracy === 'millisecond') {
rationalNumber = 1;
}
return {
get() {
const value = this.getDataValue(name);
if (value === null || value === undefined) {
return value;
}
return new Date(value * rationalNumber);
},
set(value) {
if (value === null || value === undefined) {
this.setDataValue(name, value);
} else {
// date to unix timestamp
this.setDataValue(name, Math.floor(new Date(value).getTime() / rationalNumber));
}
},
};
}
}
export interface UnixTimestampFieldOptions extends BaseColumnFieldOptions {
type: 'unix-timestamp';
}

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import lodash from 'lodash'; import lodash, { isPlainObject } from 'lodash';
import { Model as SequelizeModel, ModelStatic } from 'sequelize'; import { Model as SequelizeModel, ModelStatic } from 'sequelize';
import { Collection } from './collection'; import { Collection } from './collection';
import { Database } from './database'; import { Database } from './database';
@ -50,21 +50,6 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
return await runner.runSync(options); return await runner.runSync(options);
} }
static callSetters(values, options) {
// map values
const result = {};
for (const key of Object.keys(values)) {
const field = this.collection.getField(key);
if (field && field.setter) {
result[key] = field.setter.call(field, values[key], options, values, key);
} else {
result[key] = values[key];
}
}
return result;
}
// TODO // TODO
public toChangedWithAssociations() { public toChangedWithAssociations() {
// @ts-ignore // @ts-ignore

View File

@ -573,7 +573,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
underscored: this.collection.options.underscored, underscored: this.collection.options.underscored,
}); });
const values = (this.model as typeof Model).callSetters(guard.sanitize(options.values || {}), options); const values = guard.sanitize(options.values || {});
const instance = await this.model.create<any>(values, { const instance = await this.model.create<any>(values, {
...options, ...options,
@ -645,7 +645,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
const guard = UpdateGuard.fromOptions(this.model, { ...options, underscored: this.collection.options.underscored }); const guard = UpdateGuard.fromOptions(this.model, { ...options, underscored: this.collection.options.underscored });
const values = (this.model as typeof Model).callSetters(guard.sanitize(options.values || {}), options); const values = guard.sanitize(options.values);
// NOTE: // NOTE:
// 1. better to be moved to separated API like bulkUpdate/updateMany // 1. better to be moved to separated API like bulkUpdate/updateMany

View File

@ -18,8 +18,8 @@ const postgres = {
name: 'string', name: 'string',
smallint: ['integer', 'sort'], smallint: ['integer', 'sort'],
integer: ['integer', 'unixTimestamp', 'sort'], integer: ['integer', 'sort'],
bigint: ['bigInt', 'unixTimestamp', 'sort'], bigint: ['bigInt', 'sort'],
decimal: 'decimal', decimal: 'decimal',
numeric: 'float', numeric: 'float',
real: 'float', real: 'float',
@ -61,11 +61,11 @@ const mysql = {
text: 'text', text: 'text',
mediumtext: 'text', mediumtext: 'text',
longtext: 'text', longtext: 'text',
int: ['integer', 'unixTimestamp', 'sort'], int: ['integer', 'sort'],
'int unsigned': ['integer', 'unixTimestamp', 'sort'], 'int unsigned': ['integer', 'sort'],
integer: ['integer', 'unixTimestamp', 'sort'], integer: ['integer', 'sort'],
bigint: ['bigInt', 'unixTimestamp', 'sort'], bigint: ['bigInt', 'sort'],
'bigint unsigned': ['bigInt', 'unixTimestamp', 'sort'], 'bigint unsigned': ['bigInt', 'sort'],
float: 'float', float: 'float',
double: 'float', double: 'float',
boolean: 'boolean', boolean: 'boolean',

View File

@ -213,19 +213,22 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
* @internal * @internal
*/ */
public perfHistograms = new Map<string, RecordableHistogram>(); public perfHistograms = new Map<string, RecordableHistogram>();
/**
* @internal
*/
public syncManager: SyncManager;
public requestLogger: Logger;
protected plugins = new Map<string, Plugin>(); protected plugins = new Map<string, Plugin>();
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance(); protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
protected _started: Date | null = null;
private _authenticated = false; private _authenticated = false;
private _maintaining = false; private _maintaining = false;
private _maintainingCommandStatus: MaintainingCommandStatus; private _maintainingCommandStatus: MaintainingCommandStatus;
private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null; private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null;
private _actionCommand: Command; private _actionCommand: Command;
/**
* @internal
*/
public syncManager: SyncManager;
public requestLogger: Logger;
private sqlLogger: Logger; private sqlLogger: Logger;
protected _logger: SystemLogger;
constructor(public options: ApplicationOptions) { constructor(public options: ApplicationOptions) {
super(); super();
@ -238,8 +241,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
} }
} }
protected _started: Date | null = null;
/** /**
* @experimental * @experimental
*/ */
@ -247,8 +248,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this._started; return this._started;
} }
protected _logger: SystemLogger;
get logger() { get logger() {
return this._logger; return this._logger;
} }