{
loadCollections,
useCurrentFields,
useNewId,
+ useCancelAction,
+ useUpdateCollectionActionAndRefreshCM,
}}
/>
diff --git a/packages/core/client/src/collection-manager/Configuration/OverridingCollectionField.tsx b/packages/core/client/src/collection-manager/Configuration/OverridingCollectionField.tsx
new file mode 100644
index 0000000000..792397a96d
--- /dev/null
+++ b/packages/core/client/src/collection-manager/Configuration/OverridingCollectionField.tsx
@@ -0,0 +1,180 @@
+import { ArrayTable } from '@formily/antd';
+import { ISchema, useForm } from '@formily/react';
+import { uid } from '@formily/shared';
+import cloneDeep from 'lodash/cloneDeep';
+import set from 'lodash/set';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useAPIClient, useRequest } from '../../api-client';
+import { useRecord, RecordProvider } from '../../record-provider';
+import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
+import { useCancelAction } from '../action-hooks';
+import { useCollectionManager } from '../hooks';
+import { IField } from '../interfaces/types';
+import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
+import * as components from './components';
+
+const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
+ if (!schema) {
+ return;
+ }
+ const properties = cloneDeep(schema.properties) as any;
+ properties.name['x-disabled'] = true;
+
+ if (schema.hasDefaultValue === true) {
+ properties['defaultValue'] = cloneDeep(schema.default.uiSchema);
+ properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
+ properties['defaultValue']['x-decorator'] = 'FormItem';
+ }
+ return {
+ type: 'object',
+ properties: {
+ [uid()]: {
+ type: 'void',
+ 'x-component': 'Action.Drawer',
+ 'x-decorator': 'Form',
+ 'x-decorator-props': {
+ useValues(options) {
+ return useRequest(
+ () =>
+ Promise.resolve({
+ data: cloneDeep(schema.default),
+ }),
+ options,
+ );
+ },
+ },
+ title: `${compile(record.__parent?.title)} - ${compile('{{ t("Override field") }}')}`,
+ properties: {
+ summary: {
+ type: 'void',
+ 'x-component': 'FieldSummary',
+ 'x-component-props': {
+ schemaKey: schema.name,
+ },
+ },
+ // @ts-ignore
+ ...properties,
+ footer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer.Footer',
+ properties: {
+ action1: {
+ title: '{{ t("Cancel") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ useAction: '{{ useCancelAction }}',
+ },
+ },
+ action2: {
+ title: '{{ t("Submit") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ type: 'primary',
+ useAction: '{{ useOverridingCollectionField }}',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+};
+
+const useOverridingCollectionField = () => {
+ const form = useForm();
+ const { refreshCM } = useCollectionManager();
+ const ctx = useActionContext();
+ const { refresh } = useResourceActionContext();
+ const { resource } = useResourceContext();
+ return {
+ async run() {
+ await form.submit();
+ const values = cloneDeep(form.values);
+ if (values.autoCreateReverseField) {
+ } else {
+ delete values.reverseField;
+ }
+ delete values.autoCreateReverseField;
+ const { uiSchema } = values;
+ delete values.collectionName;
+ delete values.key;
+ await resource.create({
+ values: {
+ ...values,
+ uiSchema: { title: uiSchema.title, type: uiSchema.type, 'x-component': uiSchema['x-component'] },
+ },
+ });
+ ctx.setVisible(false);
+ await form.reset();
+ refresh();
+ await refreshCM();
+ },
+ };
+};
+
+export const OverridingCollectionField = (props) => {
+ const record = useRecord();
+ return
;
+};
+
+export const OverridingFieldAction = (props) => {
+ const { scope, getContainer, item: record, children } = props;
+ const { getInterface } = useCollectionManager();
+ const [visible, setVisible] = useState(false);
+ const [schema, setSchema] = useState({});
+ const api = useAPIClient();
+ const { t } = useTranslation();
+ const compile = useCompile();
+ const [data, setData] = useState
({});
+
+ return (
+
+
+ {
+ const { data } = await api.resource('collections.fields', record.collectionName).get({
+ filterByTk: record.name,
+ appends: ['uiSchema', 'reverseField'],
+ });
+ setData(data?.data);
+ const interfaceConf = getInterface(record.interface);
+ const defaultValues: any = cloneDeep(data?.data) || {};
+ if (!defaultValues?.reverseField) {
+ defaultValues.autoCreateReverseField = false;
+ defaultValues.reverseField = interfaceConf.default?.reverseField;
+ set(defaultValues.reverseField, 'name', `f_${uid()}`);
+ set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title);
+ }
+ const schema = getSchema(
+ {
+ ...interfaceConf,
+ default: defaultValues,
+ },
+ record,
+ compile,
+ getContainer,
+ );
+ setSchema(schema);
+ setVisible(true);
+ }}
+ >
+ {children || t('Override')}
+
+
+
+
+ );
+};
diff --git a/packages/core/client/src/collection-manager/Configuration/ViewInheritedField.tsx b/packages/core/client/src/collection-manager/Configuration/ViewInheritedField.tsx
new file mode 100644
index 0000000000..530dfbe4c6
--- /dev/null
+++ b/packages/core/client/src/collection-manager/Configuration/ViewInheritedField.tsx
@@ -0,0 +1,179 @@
+import { ArrayTable } from '@formily/antd';
+import { ISchema, useForm } from '@formily/react';
+import { uid } from '@formily/shared';
+import cloneDeep from 'lodash/cloneDeep';
+import set from 'lodash/set';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useAPIClient, useRequest } from '../../api-client';
+import { useRecord, RecordProvider } from '../../record-provider';
+import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
+import { useCancelAction } from '../action-hooks';
+import { useCollectionManager } from '../hooks';
+import { IField } from '../interfaces/types';
+import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
+import * as components from './components';
+
+const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
+ if (!schema) {
+ return;
+ }
+ const properties = cloneDeep(schema.properties) as any;
+ properties.name['x-disabled'] = true;
+ if (schema.hasDefaultValue === true) {
+ properties['defaultValue'] = cloneDeep(schema.default.uiSchema);
+ properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
+ properties['defaultValue']['x-decorator'] = 'FormItem';
+ }
+ return {
+ type: 'object',
+ properties: {
+ [uid()]: {
+ type: 'void',
+ 'x-component': 'Action.Drawer',
+ 'x-decorator': 'Form',
+ 'x-decorator-props': {
+ useValues(options) {
+ return useRequest(
+ () =>
+ Promise.resolve({
+ data: cloneDeep(schema.default),
+ }),
+ options,
+ );
+ },
+ },
+ title: `${compile(record.__parent?.title)} - ${compile('{{ t("View") }}')}`,
+ properties: {
+ summary: {
+ type: 'void',
+ 'x-component': 'FieldSummary',
+ 'x-component-props': {
+ schemaKey: schema.name,
+ },
+ },
+ // @ts-ignore
+ ...properties,
+ // footer: {
+ // type: 'void',
+ // 'x-component': 'Action.Drawer.Footer',
+ // properties: {
+ // action1: {
+ // title: '{{ t("Cancel") }}',
+ // 'x-component': 'Action',
+ // 'x-component-props': {
+ // useAction: '{{ useCancelAction }}',
+ // },
+ // },
+ // action2: {
+ // title: '{{ t("Submit") }}',
+ // 'x-component': 'Action',
+ // 'x-component-props': {
+ // type: 'primary',
+ // useAction: '{{ useVieInheritedCollectionField }}',
+ // },
+ // },
+ // },
+ // },
+ },
+ },
+ },
+ };
+};
+
+const useVieInheritedCollectionField = () => {
+ const form = useForm();
+ const { refreshCM } = useCollectionManager();
+ const ctx = useActionContext();
+ const { refresh } = useResourceActionContext();
+ const { resource } = useResourceContext();
+ return {
+ async run() {
+ await form.submit();
+ const values = cloneDeep(form.values);
+ if (values.autoCreateReverseField) {
+ } else {
+ delete values.reverseField;
+ }
+ delete values.autoCreateReverseField;
+ const { uiSchema } = values;
+ delete values.collectionName;
+ delete values.key;
+ await resource.create({
+ values: {
+ ...values,
+ uiSchema: { title: uiSchema.title, type: uiSchema.type, 'x-component': uiSchema['x-component'] },
+ },
+ });
+ ctx.setVisible(false);
+ await form.reset();
+ refresh();
+ await refreshCM();
+ },
+ };
+};
+
+export const ViewCollectionField = (props) => {
+ const record = useRecord();
+ return ;
+};
+
+export const ViewFieldAction = (props) => {
+ const { scope, getContainer, item: record, children } = props;
+ const { getInterface } = useCollectionManager();
+ const [visible, setVisible] = useState(false);
+ const [schema, setSchema] = useState({});
+ const api = useAPIClient();
+ const { t } = useTranslation();
+ const compile = useCompile();
+ const [data, setData] = useState({});
+
+ return (
+
+
+ {
+ const { data } = await api.resource('collections.fields', record.collectionName).get({
+ filterByTk: record.name,
+ appends: ['uiSchema', 'reverseField'],
+ });
+ setData(data?.data);
+ const interfaceConf = getInterface(record.interface);
+ const defaultValues: any = cloneDeep(data?.data) || {};
+ if (!defaultValues?.reverseField) {
+ defaultValues.autoCreateReverseField = false;
+ defaultValues.reverseField = interfaceConf.default?.reverseField;
+ set(defaultValues.reverseField, 'name', `f_${uid()}`);
+ set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title);
+ }
+ const schema = getSchema(
+ {
+ ...interfaceConf,
+ default: defaultValues,
+ },
+ record,
+ compile,
+ getContainer,
+ );
+ setSchema(schema);
+ setVisible(true);
+ }}
+ >
+ {children || t('View')}
+
+
+
+
+ );
+};
diff --git a/packages/core/client/src/collection-manager/Configuration/index.tsx b/packages/core/client/src/collection-manager/Configuration/index.tsx
index f7ead8c559..7e994d938f 100644
--- a/packages/core/client/src/collection-manager/Configuration/index.tsx
+++ b/packages/core/client/src/collection-manager/Configuration/index.tsx
@@ -7,6 +7,9 @@ export * from './EditFieldAction';
export * from './interfaces';
export * from './components';
export * from './CollectionFieldsTable';
+export * from './schemas/collections'
+export * from './OverridingCollectionField'
+export * from './ViewInheritedField'
registerValidateFormats({
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,
diff --git a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts
index eff4c464ee..695d697e74 100644
--- a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts
+++ b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts
@@ -80,10 +80,6 @@ export const collectionFieldSchema: ISchema = {
},
},
},
- // 'x-component': 'CollectionProvider',
- // 'x-component-props': {
- // collection,
- // },
properties: {
summary: {
type: 'void',
@@ -206,3 +202,37 @@ export const collectionFieldSchema: ISchema = {
},
},
};
+
+export const overridingSchema: ISchema = {
+ type: 'void',
+ title: '{{ t("Actions") }}',
+ 'x-component': 'Table.Column',
+ properties: {
+ actions: {
+ type: 'void',
+ 'x-component': 'Space',
+ 'x-component-props': {
+ split: '|',
+ },
+ properties: {
+ overriding: {
+ type: 'void',
+ title: '{{ t("Overriding") }}',
+ 'x-component': 'OverridingCollectionField',
+ 'x-component-props': {
+ type: 'primary',
+ },
+ },
+ view:{
+ type: 'void',
+ title: '{{ t("View") }}',
+ 'x-component': 'ViewCollectionField',
+ 'x-component-props': {
+ type: 'primary',
+ },
+ }
+ },
+ },
+ },
+};
+
diff --git a/packages/core/client/src/collection-manager/Configuration/schemas/collections.ts b/packages/core/client/src/collection-manager/Configuration/schemas/collections.ts
index 15c2ed0559..047928bb57 100644
--- a/packages/core/client/src/collection-manager/Configuration/schemas/collections.ts
+++ b/packages/core/client/src/collection-manager/Configuration/schemas/collections.ts
@@ -7,300 +7,336 @@ const compile = (source) => {
return Schema.compile(source, { t: i18n.t });
};
-export const collection: CollectionOptions = {
- name: 'collections',
- filterTargetKey: 'name',
- targetKey: 'name',
- fields: [
- {
- type: 'integer',
- name: 'title',
- interface: 'input',
- uiSchema: {
- title: '{{ t("Collection display name") }}',
- type: 'number',
- 'x-component': 'Input',
- required: true,
+export const getCollectionOptions = (database): CollectionOptions => {
+ return {
+ name: 'collections',
+ filterTargetKey: 'name',
+ targetKey: 'name',
+ fields: [
+ {
+ type: 'integer',
+ name: 'title',
+ interface: 'input',
+ uiSchema: {
+ title: '{{ t("Collection display name") }}',
+ type: 'number',
+ 'x-component': 'Input',
+ required: true,
+ },
},
- },
- {
- type: 'string',
- name: 'name',
- interface: 'input',
- uiSchema: {
- title: '{{ t("Collection name") }}',
+ {
type: 'string',
- 'x-component': 'Input',
- description:
- '{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.")}}',
+ name: 'name',
+ interface: 'input',
+ uiSchema: {
+ title: '{{ t("Collection name") }}',
+ type: 'string',
+ 'x-component': 'Input',
+ description:
+ '{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.")}}',
+ },
},
- },
- {
- type: 'hasMany',
- name: 'fields',
- target: 'fields',
- collectionName: 'collections',
- sourceKey: 'name',
- targetKey: 'name',
- uiSchema: {},
- },
- ],
+ {
+ type: 'hasMany',
+ name: 'fields',
+ target: 'fields',
+ collectionName: 'collections',
+ sourceKey: 'name',
+ targetKey: 'name',
+ uiSchema: {},
+ },
+ database === 'postgres' && {
+ type: 'hasMany',
+ name: 'inherits',
+ interface: 'select',
+ uiSchema: {
+ title: '{{ t("Inherits") }}',
+ type: 'string',
+ 'x-component': 'Select',
+ 'x-component-props': {
+ mode: 'multiple',
+ },
+ },
+ },
+ ],
+ };
};
-export const collectionSchema: ISchema = {
- type: 'object',
- properties: {
- block1: {
- type: 'void',
- 'x-collection': 'collections',
- 'x-decorator': 'ResourceActionProvider',
- 'x-decorator-props': {
- collection,
- request: {
- resource: 'collections',
- action: 'list',
- params: {
- pageSize: 50,
- filter: {
- 'hidden.$isFalsy': true,
- },
- sort: ['sort'],
- appends: [],
- },
+export const createCollectionProperties = {
+ title: {
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ },
+ name: {
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-validator': 'uid',
+ },
+ inherits: {
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-visible': '{{ enableInherits }}',
+ 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
+ },
+ footer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer.Footer',
+ properties: {
+ action1: {
+ title: '{{ t("Cancel") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ useAction: '{{ cm.useCancelAction }}',
},
},
- // 'x-component': 'CollectionProvider',
- // 'x-component-props': {
- // collection,
- // },
- properties: {
- actions: {
- type: 'void',
- 'x-component': 'ActionBar',
- 'x-component-props': {
- style: {
- marginBottom: 16,
- },
- },
- properties: {
- filter: {
- type: 'void',
- title: '{{ t("Filter") }}',
- default: {
- $and: [{ title: { $includes: '' } }, { name: { $includes: '' } }],
- },
- 'x-action': 'filter',
- 'x-component': 'Filter.Action',
- 'x-component-props': {
- icon: 'FilterOutlined',
- useProps: '{{ cm.useFilterActionProps }}',
- },
- 'x-align': 'left',
- },
- delete: {
- type: 'void',
- title: '{{ t("Delete") }}',
- 'x-component': 'Action',
- 'x-component-props': {
- useAction: '{{ cm.useBulkDestroyActionAndRefreshCM }}',
- confirm: {
- title: "{{t('Delete record')}}",
- content: "{{t('Are you sure you want to delete it?')}}",
- },
- },
- },
- create: {
- type: 'void',
- title: '{{ t("Create collection") }}',
- 'x-component': 'Action',
- 'x-component-props': {
- type: 'primary',
- },
- properties: {
- drawer: {
- type: 'void',
- title: '{{ t("Create collection") }}',
- 'x-component': 'Action.Drawer',
- 'x-decorator': 'Form',
- 'x-decorator-props': {
- useValues: '{{ useCollectionValues }}',
- },
- properties: {
- title: {
- 'x-component': 'CollectionField',
- 'x-decorator': 'FormItem',
- },
- name: {
- 'x-component': 'CollectionField',
- 'x-decorator': 'FormItem',
- 'x-validator': 'uid',
- },
- footer: {
- type: 'void',
- 'x-component': 'Action.Drawer.Footer',
- properties: {
- action1: {
- title: '{{ t("Cancel") }}',
- 'x-component': 'Action',
- 'x-component-props': {
- useAction: '{{ cm.useCancelAction }}',
- },
- },
- action2: {
- title: '{{ t("Submit") }}',
- 'x-component': 'Action',
- 'x-component-props': {
- type: 'primary',
- useAction: '{{ cm.useCreateActionAndRefreshCM }}',
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- table: {
- type: 'void',
- 'x-uid': 'input',
- 'x-component': 'Table.Void',
- 'x-component-props': {
- rowKey: 'name',
- rowSelection: {
- type: 'checkbox',
- },
- useDataSource: '{{ cm.useDataSourceFromRAC }}',
- },
- properties: {
- column1: {
- type: 'void',
- 'x-decorator': 'Table.Column.Decorator',
- 'x-component': 'Table.Column',
- properties: {
- title: {
- 'x-component': 'CollectionField',
- 'x-read-pretty': true,
- },
- },
- },
- column2: {
- type: 'void',
- 'x-decorator': 'Table.Column.Decorator',
- 'x-component': 'Table.Column',
- properties: {
- name: {
- type: 'string',
- 'x-component': 'CollectionField',
- 'x-read-pretty': true,
- },
- },
- },
- column3: {
- type: 'void',
- title: '{{ t("Actions") }}',
- 'x-component': 'Table.Column',
- properties: {
- actions: {
- type: 'void',
- 'x-component': 'Space',
- 'x-component-props': {
- split: '|',
- },
- properties: {
- view: {
- type: 'void',
- title: '{{ t("Configure fields") }}',
- 'x-component': 'Action.Link',
- 'x-component-props': {},
- properties: {
- drawer: {
- type: 'void',
- 'x-component': 'Action.Drawer',
- 'x-reactions': (field) => {
- const i = field.path.segments[1];
- const table = field.form.getValuesIn(`table.${i}`);
- if (table) {
- field.title = `${compile(table.title)} - ${compile('{{ t("Configure fields") }}')}`;
- }
- },
- properties: {
- collectionFieldSchema,
- },
- },
- },
- },
- update: {
- type: 'void',
- title: '{{ t("Edit") }}',
- 'x-component': 'Action.Link',
- 'x-component-props': {
- type: 'primary',
- },
- properties: {
- drawer: {
- type: 'void',
- 'x-component': 'Action.Drawer',
- 'x-decorator': 'Form',
- 'x-decorator-props': {
- useValues: '{{ cm.useValuesFromRecord }}',
- },
- title: '{{ t("Edit collection") }}',
- properties: {
- title: {
- 'x-component': 'CollectionField',
- 'x-decorator': 'FormItem',
- },
- name: {
- 'x-component': 'CollectionField',
- 'x-decorator': 'FormItem',
- 'x-disabled': true,
- },
- footer: {
- type: 'void',
- 'x-component': 'Action.Drawer.Footer',
- properties: {
- action1: {
- title: '{{ t("Cancel") }}',
- 'x-component': 'Action',
- 'x-component-props': {
- useAction: '{{ cm.useCancelAction }}',
- },
- },
- action2: {
- title: '{{ t("Submit") }}',
- 'x-component': 'Action',
- 'x-component-props': {
- type: 'primary',
- useAction: '{{ cm.useUpdateCollectionActionAndRefreshCM }}',
- },
- },
- },
- },
- },
- },
- },
- },
- delete: {
- type: 'void',
- title: '{{ t("Delete") }}',
- 'x-component': 'Action.Link',
- 'x-component-props': {
- confirm: {
- title: "{{t('Delete record')}}",
- content: "{{t('Are you sure you want to delete it?')}}",
- },
- useAction: '{{ cm.useDestroyActionAndRefreshCM }}',
- },
- },
- },
- },
- },
- },
- },
+ action2: {
+ title: '{{ t("Submit") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ type: 'primary',
+ useAction: '{{ cm.useCreateActionAndRefreshCM }}',
},
},
},
},
};
+
+export const editCollectionProperties = {
+ title: {
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ },
+ name: {
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-disabled': true,
+ },
+ inherits: {
+ 'x-component': 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-disabled': true,
+ 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
+ },
+ footer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer.Footer',
+ properties: {
+ action1: {
+ title: '{{ t("Cancel") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ useAction: '{{ useCancelAction }}',
+ },
+ },
+ action2: {
+ title: '{{ t("Submit") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ type: 'primary',
+ useAction: '{{ useUpdateCollectionActionAndRefreshCM }}',
+ },
+ },
+ },
+ },
+};
+
+export const collectionSchema = (database): ISchema => {
+ return {
+ type: 'object',
+ properties: {
+ block1: {
+ type: 'void',
+ 'x-collection': 'collections',
+ 'x-decorator': 'ResourceActionProvider',
+ 'x-decorator-props': {
+ collection: getCollectionOptions(database),
+ request: {
+ resource: 'collections',
+ action: 'list',
+ params: {
+ pageSize: 50,
+ filter: {
+ 'hidden.$isFalsy': true,
+ },
+ sort: ['sort'],
+ appends: [],
+ },
+ },
+ },
+ // 'x-component': 'CollectionProvider',
+ // 'x-component-props': {
+ // collection,
+ // },
+ properties: {
+ actions: {
+ type: 'void',
+ 'x-component': 'ActionBar',
+ 'x-component-props': {
+ style: {
+ marginBottom: 16,
+ },
+ },
+ properties: {
+ filter: {
+ type: 'void',
+ title: '{{ t("Filter") }}',
+ default: {
+ $and: [{ title: { $includes: '' } }, { name: { $includes: '' } }],
+ },
+ 'x-action': 'filter',
+ 'x-component': 'Filter.Action',
+ 'x-component-props': {
+ icon: 'FilterOutlined',
+ useProps: '{{ cm.useFilterActionProps }}',
+ },
+ 'x-align': 'left',
+ },
+ delete: {
+ type: 'void',
+ title: '{{ t("Delete") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ useAction: '{{ cm.useBulkDestroyActionAndRefreshCM }}',
+ confirm: {
+ title: "{{t('Delete record')}}",
+ content: "{{t('Are you sure you want to delete it?')}}",
+ },
+ },
+ },
+ create: {
+ type: 'void',
+ title: '{{ t("Create collection") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ type: 'primary',
+ },
+ properties: {
+ drawer: {
+ type: 'void',
+ title: '{{ t("Create collection") }}',
+ 'x-component': 'Action.Drawer',
+ 'x-decorator': 'Form',
+ 'x-decorator-props': {
+ useValues: '{{ useCollectionValues }}',
+ },
+ properties: createCollectionProperties,
+ },
+ },
+ },
+ },
+ },
+ table: {
+ type: 'void',
+ 'x-uid': 'input',
+ 'x-component': 'Table.Void',
+ 'x-component-props': {
+ rowKey: 'name',
+ rowSelection: {
+ type: 'checkbox',
+ },
+ useDataSource: '{{ cm.useDataSourceFromRAC }}',
+ },
+ properties: {
+ column1: {
+ type: 'void',
+ 'x-decorator': 'Table.Column.Decorator',
+ 'x-component': 'Table.Column',
+ properties: {
+ title: {
+ 'x-component': 'CollectionField',
+ 'x-read-pretty': true,
+ },
+ },
+ },
+ column2: {
+ type: 'void',
+ 'x-decorator': 'Table.Column.Decorator',
+ 'x-component': 'Table.Column',
+ properties: {
+ name: {
+ type: 'string',
+ 'x-component': 'CollectionField',
+ 'x-read-pretty': true,
+ },
+ },
+ },
+ column3: {
+ type: 'void',
+ title: '{{ t("Actions") }}',
+ 'x-component': 'Table.Column',
+ properties: {
+ actions: {
+ type: 'void',
+ 'x-component': 'Space',
+ 'x-component-props': {
+ split: '|',
+ },
+ properties: {
+ view: {
+ type: 'void',
+ title: '{{ t("Configure fields") }}',
+ 'x-component': 'Action.Link',
+ 'x-component-props': {},
+ properties: {
+ drawer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer',
+ 'x-component-props': {
+ destroyOnClose: true,
+ },
+ 'x-reactions': (field) => {
+ const i = field.path.segments[1];
+ const table = field.form.getValuesIn(`table.${i}`);
+ if (table) {
+ field.title = `${compile(table.title)} - ${compile('{{ t("Configure fields") }}')}`;
+ }
+ },
+ properties: {
+ collectionFieldSchema,
+ },
+ },
+ },
+ },
+ update: {
+ type: 'void',
+ title: '{{ t("Edit") }}',
+ 'x-component': 'Action.Link',
+ 'x-component-props': {
+ type: 'primary',
+ },
+ properties: {
+ drawer: {
+ type: 'void',
+ 'x-component': 'Action.Drawer',
+ 'x-decorator': 'Form',
+ 'x-decorator-props': {
+ useValues: '{{ cm.useValuesFromRecord }}',
+ },
+ title: '{{ t("Edit collection") }}',
+ properties: editCollectionProperties,
+ },
+ },
+ },
+ delete: {
+ type: 'void',
+ title: '{{ t("Delete") }}',
+ 'x-component': 'Action.Link',
+ 'x-component-props': {
+ confirm: {
+ title: "{{t('Delete record')}}",
+ content: "{{t('Are you sure you want to delete it?')}}",
+ },
+ useAction: '{{ cm.useDestroyActionAndRefreshCM }}',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+};
diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts
index d73b79e80e..ecfaef4ed8 100644
--- a/packages/core/client/src/collection-manager/action-hooks.ts
+++ b/packages/core/client/src/collection-manager/action-hooks.ts
@@ -166,7 +166,7 @@ export const useCreateAction = () => {
return {
async run() {
await form.submit();
- await resource.create({ values: form.values });
+ await resource.create({ values: form.values});
ctx.setVisible(false);
await form.reset();
refresh();
diff --git a/packages/core/client/src/collection-manager/hooks/useCollection.ts b/packages/core/client/src/collection-manager/hooks/useCollection.ts
index c2da2258fc..5747e2343e 100644
--- a/packages/core/client/src/collection-manager/hooks/useCollection.ts
+++ b/packages/core/client/src/collection-manager/hooks/useCollection.ts
@@ -1,18 +1,38 @@
import { SchemaKey } from '@formily/react';
+import { reduce ,unionBy} from 'lodash';
import { useContext } from 'react';
import { useAPIClient } from '../../api-client';
import { CollectionContext } from '../context';
import { CollectionFieldOptions } from '../types';
+import { useCollectionManager } from './useCollectionManager';
export const useCollection = () => {
const collection = useContext(CollectionContext);
const api = useAPIClient();
const resource = api?.resource(collection?.name);
+ const { getParentCollections, getCurrentCollectionFields } = useCollectionManager();
+ const currentFields = collection.fields;
+ const inheritKeys = getParentCollections(collection.name);
+ const inheritedFields = reduce(
+ inheritKeys,
+ (result, value) => {
+ const arr = result;
+ return arr.concat(getCurrentCollectionFields(value));
+ },
+ [],
+ );
+ const totalFields = unionBy(currentFields?.concat(inheritedFields),'name').filter((v)=>{
+ return !v.isForeignKey
+ });
return {
...collection,
resource,
getField(name: SchemaKey): CollectionFieldOptions {
- return collection?.fields?.find((field) => field.name === name);
+ const fields = totalFields;
+ return fields?.find((field) => field.name === name);
},
+ fields: totalFields,
+ currentFields,
+ inheritedFields
};
};
diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts
index 3656434547..53e44c8781 100644
--- a/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts
+++ b/packages/core/client/src/collection-manager/hooks/useCollectionManager.ts
@@ -1,9 +1,32 @@
import { clone } from '@formily/shared';
import { useContext } from 'react';
+import { reduce, unionBy,uniq } from 'lodash';
import { CollectionManagerContext } from '../context';
+import { CollectionFieldOptions } from '../types';
export const useCollectionManager = () => {
const { refreshCM, service, interfaces, collections } = useContext(CollectionManagerContext);
+ const getInheritedFields = (name) => {
+ const inheritKeys = getParentCollections(name);
+ const inheritedFields = reduce(
+ inheritKeys,
+ (result, value) => {
+ const arr = result;
+ return arr.concat(collections?.find((collection) => collection.name === value)?.fields);
+ },
+ [],
+ );
+ return inheritedFields;
+ };
+
+ const getCollectionFields = (name: string): CollectionFieldOptions[] => {
+ const currentFields = collections?.find((collection) => collection.name === name)?.fields;
+ const inheritedFields = getInheritedFields(name);
+ const totalFields = unionBy(currentFields?.concat(inheritedFields) || [], 'name').filter((v:any) => {
+ return !v.isForeignKey;
+ });
+ return totalFields;
+ };
const getCollectionField = (name: string) => {
const [collectionName, fieldName] = name.split('.');
if (!fieldName) {
@@ -13,27 +36,68 @@ export const useCollectionManager = () => {
if (!collection) {
return;
}
- return collection?.fields?.find((field) => field.name === fieldName);
+ return getCollectionFields(collectionName)?.find((field) => field.name === fieldName);
};
+ const getParentCollections = (name) => {
+ const parents = [];
+ const getParents = (name) => {
+ const collection = collections?.find((collection) => collection.name === name);
+ if (collection) {
+ const { inherits } = collection;
+ if (inherits) {
+ for (let index = 0; index < inherits.length; index++) {
+ const collectionKey = inherits[index];
+ parents.push(collectionKey);
+ getParents(collectionKey);
+ }
+ }
+ }
+ return uniq(parents);
+ };
+
+ return getParents(name);
+ };
+
+ const getChildrenCollections = (name) => {
+ const childrens = [];
+ const getChildrens = (name) => {
+ const inheritCollections = collections.filter((v) => {
+ return v.inherits?.includes(name);
+ });
+ inheritCollections.forEach((v) => {
+ const collectionKey = v.name;
+ childrens.push(v);
+ return getChildrens(collectionKey);
+ });
+ return childrens;
+ };
+ return getChildrens(name);
+ };
+ const getCurrentCollectionFields = (name: string) => {
+ const collection = collections?.find((collection) => collection.name === name);
+ return collection?.fields || [];
+ };
+
return {
service,
interfaces,
collections,
+ getParentCollections,
+ getChildrenCollections,
refreshCM: () => refreshCM?.(),
get(name: string) {
return collections?.find((collection) => collection.name === name);
},
+ getInheritedFields,
+ getCollectionField,
+ getCollectionFields,
+ getCurrentCollectionFields,
getCollection(name: any) {
if (typeof name !== 'string') {
return name;
}
return collections?.find((collection) => collection.name === name);
},
- getCollectionFields(name: string) {
- const collection = collections?.find((collection) => collection.name === name);
- return collection?.fields || [];
- },
- getCollectionField,
getCollectionJoinField(name: string) {
if (!name) {
return;
@@ -58,5 +122,23 @@ export const useCollectionManager = () => {
getInterface(name: string) {
return interfaces[name] ? clone(interfaces[name]) : null;
},
+ getParentCollectionFields: (parentCollection, currentCollection) => {
+ const currentFields = collections?.find((collection) => collection.name === currentCollection)?.fields;
+ const parentFields = collections?.find((collection) => collection.name === parentCollection)?.fields;
+ const inheritKeys = getParentCollections(currentCollection);
+ const index = inheritKeys.indexOf(parentCollection);
+ let filterFields = currentFields;
+ if (index > 0) {
+ inheritKeys.splice(index);
+ inheritKeys.forEach((v) => {
+ filterFields = filterFields.concat(getCurrentCollectionFields(v));
+ });
+ }
+ return parentFields.filter((v) => {
+ return !filterFields.find((k) => {
+ return k.name === v.name;
+ });
+ });
+ },
};
};
diff --git a/packages/core/client/src/collection-manager/types.ts b/packages/core/client/src/collection-manager/types.ts
index 920d8cb24f..0925db7cf6 100644
--- a/packages/core/client/src/collection-manager/types.ts
+++ b/packages/core/client/src/collection-manager/types.ts
@@ -21,6 +21,7 @@ export interface CollectionOptions {
targetKey?: string;
sortable?: any;
fields?: FieldOptions[];
+ inherits?:string[];
}
export interface ICollectionProviderProps {
diff --git a/packages/core/client/src/database/CurrentDatabaseProvider.tsx b/packages/core/client/src/database/CurrentDatabaseProvider.tsx
new file mode 100644
index 0000000000..b7b219f93e
--- /dev/null
+++ b/packages/core/client/src/database/CurrentDatabaseProvider.tsx
@@ -0,0 +1,24 @@
+import React, { createContext, useContext } from 'react';
+import { Spin } from 'antd';
+import { useRequest } from '../api-client';
+
+export const CurrentDatabaseContext = createContext(null);
+
+export const useCurrentDatabase = () => {
+ return useContext(CurrentDatabaseContext);
+};
+export const CurrentDatabaseProvider = (props) => {
+ const result = useRequest({
+ url: 'app:getInfo',
+ });
+ if (result.loading) {
+ return ;
+ }
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/packages/core/client/src/database/index.ts b/packages/core/client/src/database/index.ts
new file mode 100644
index 0000000000..56e7d73693
--- /dev/null
+++ b/packages/core/client/src/database/index.ts
@@ -0,0 +1 @@
+export * from './CurrentDatabaseProvider';
diff --git a/packages/core/client/src/index.tsx b/packages/core/client/src/index.tsx
index 57c75634c9..889a9541e6 100644
--- a/packages/core/client/src/index.tsx
+++ b/packages/core/client/src/index.tsx
@@ -25,4 +25,5 @@ export * from './schema-templates';
export * from './settings-form';
export * from './system-settings';
export * from './user';
+export * from './database'
diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts
index ec6e93a511..4e7c6e9c0d 100644
--- a/packages/core/client/src/locale/en_US.ts
+++ b/packages/core/client/src/locale/en_US.ts
@@ -113,6 +113,7 @@ export default {
"Create collection": "Create collection",
"Collection display name": "Collection display name",
"Collection name": "Collection name",
+ "Inherits":"Inherits",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
"Storage type": "Storage type",
"Edit": "Edit",
@@ -120,9 +121,12 @@ export default {
"Configure fields": "Configure fields",
"Configure columns": "Configure columns",
"Edit field": "Edit field",
+ "Override":"Override",
+ "Override field":"Override field",
"Configure fields of {{title}}": "Configure fields of {{title}}",
"PK & FK fields": "PK & FK fields",
"Association fields": "Association fields",
+ "Parent collection fields":"Parent collection fields",
"System fields": "System fields",
"General fields": "General fields",
"Basic": "Basic",
diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts
index 885a598223..a9076b32b0 100644
--- a/packages/core/client/src/locale/ja_JP.ts
+++ b/packages/core/client/src/locale/ja_JP.ts
@@ -114,6 +114,8 @@ export default {
"Configure fields": "フィールドの設定",
"Configure columns": "カラムの設定",
"Edit field": "フィールドの編集",
+ "Override":"書き換え",
+ "Override field":"フィールドの上書き",
"Configure fields of {{title}}": "{{title}}のフィールド設定",
"Basic": "基本タイプ",
"Single line text": "一行テキスト",
diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts
index 91b12ab544..bb444746d4 100644
--- a/packages/core/client/src/locale/zh_CN.ts
+++ b/packages/core/client/src/locale/zh_CN.ts
@@ -117,6 +117,7 @@ export default {
"Create collection": "创建数据表",
"Collection display name": "数据表名称",
"Collection name": "数据表标识",
+ "Inherits":"继承",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
"Storage type": "存储类型",
"Edit": "编辑",
@@ -124,11 +125,14 @@ export default {
"Configure fields": "配置字段",
"Configure columns": "配置字段",
"Edit field": "编辑字段",
+ "Override":"重写",
+ "Override field":"重写字段",
"Configure fields of {{title}}": "「{{title}}」的字段配置",
"PK & FK fields": "主外键字段",
"Association fields": "关系字段",
"System fields": "系统字段",
"General fields": "普通字段",
+ "Parent collection fields":"父表字段",
"Basic": "基本类型",
"Single line text": "单行文本",
"Long text": "多行文本",
diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx
index ab0c2efdbc..043d77c54f 100644
--- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx
+++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx
@@ -7,6 +7,7 @@ import {
ACLRolesCheckProvider,
CurrentUser,
CurrentUserProvider,
+ CurrentDatabaseProvider,
findByUid,
findMenuItem,
RemoteCollectionManagerProvider,
@@ -17,7 +18,7 @@ import {
useDocumentTitle,
useRequest,
useRoute,
- useSystemSettings
+ useSystemSettings,
} from '../../../';
import { useCollectionManager } from '../../../collection-manager';
import { PoweredBy } from '../../../powered-by';
@@ -206,15 +207,17 @@ const InternalAdminLayout = (props: any) => {
export const AdminLayout = (props) => {
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx
index 76986a9a5c..f152c2180c 100644
--- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx
+++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx
@@ -13,7 +13,7 @@ const useLabelFields = (collectionName?: any) => {
const { getCollectionFields } = useCollectionManager();
const targetFields = getCollectionFields(collectionName);
return targetFields
- ?.filter?.((field) => field?.interface && !field?.target && field.type !== 'boolean')
+ ?.filter?.((field) => field?.interface && !field?.target && field.type !== 'boolean'&&!field.isForeignKey)
?.map?.((field) => {
return {
value: field.name,
diff --git a/packages/core/client/src/schema-initializer/SchemaInitializer.tsx b/packages/core/client/src/schema-initializer/SchemaInitializer.tsx
index 7aa7158b6d..a74df7b9d6 100644
--- a/packages/core/client/src/schema-initializer/SchemaInitializer.tsx
+++ b/packages/core/client/src/schema-initializer/SchemaInitializer.tsx
@@ -10,7 +10,7 @@ import {
SchemaInitializerButtonProps,
SchemaInitializerItemComponent,
SchemaInitializerItemOptions,
- SchemaInitializerItemProps
+ SchemaInitializerItemProps,
} from './types';
const defaultWrap = (s: ISchema) => s;
@@ -99,12 +99,11 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
}
});
};
- const menu = ;
+ const menu = ;
if (!designable && props.designable !== true) {
return null;
}
-
return (
{
const { t } = useTranslation();
+ const compile = useCompile();
const { insertPosition, component } = props;
+ const inheritFields = useInheritsFormItemInitializerFields();
+ const fieldItems:any[]=[
+ {
+ type: 'itemGroup',
+ title: t('Configure fields'),
+ children: useCustomFormItemInitializerFields(),
+ },
+ ]
+ if (inheritFields?.length > 0) {
+ inheritFields.forEach((inherit) => {
+ fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'itemGroup',
+ title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
+ children: Object.values(inherit)[0],
+ },
+ );
+ });
+ }
return (
{
const { t } = useTranslation();
const { insertPosition, component } = props;
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
+ const inheritFields = useInheritsFormItemInitializerFields();
+ const compile = useCompile();
+ const fieldItems: any[] = [
+ {
+ type: 'itemGroup',
+ title: t('Display fields'),
+ children: useFormItemInitializerFields(),
+ },
+ ];
+ if (inheritFields?.length > 0) {
+ inheritFields.forEach((inherit) => {
+ Object.values(inherit)[0].length&&fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'itemGroup',
+ title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
+ children: Object.values(inherit)[0],
+ },
+ );
+ });
+ }
+ associationFields.length > 0 &&
+ fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'itemGroup',
+ title: t('Display association fields'),
+ children: associationFields,
+ },
+ );
+ fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'item',
+ title: t('Add text'),
+ component: 'BlockInitializer',
+ schema: {
+ type: 'void',
+ 'x-editable': false,
+ 'x-decorator': 'FormItem',
+ 'x-designer': 'Markdown.Void.Designer',
+ 'x-component': 'Markdown.Void',
+ 'x-component-props': {
+ content: t('This is a demo text, **supports Markdown syntax**.'),
+ },
+ },
+ },
+ );
return (
(
- [
- {
- type: 'itemGroup',
- title: t('Display fields'),
- children: useFormItemInitializerFields(),
- },
- ],
- associationFields.length > 0
- ? [
- {
- type: 'divider',
- },
- {
- type: 'itemGroup',
- title: t('Display association fields'),
- children: associationFields,
- },
- ]
- : [],
- [
- {
- type: 'divider',
- },
- {
- type: 'item',
- title: t('Add text'),
- component: 'BlockInitializer',
- schema: {
- type: 'void',
- 'x-editable': false,
- 'x-decorator': 'FormItem',
- 'x-designer': 'Markdown.Void.Designer',
- 'x-component': 'Markdown.Void',
- 'x-component-props': {
- content: t('This is a demo text, **supports Markdown syntax**.'),
- },
- },
- },
- ],
- )}
+ items={fieldItems}
insertPosition={insertPosition}
component={component}
title={component ? null : t('Configure fields')}
diff --git a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx
index 02301bbcc6..ada8daec37 100644
--- a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx
@@ -2,58 +2,73 @@ import { union } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../SchemaInitializer';
-import { gridRowColWrap, useAssociatedFormItemInitializerFields, useFormItemInitializerFields } from '../utils';
+import { gridRowColWrap, useAssociatedFormItemInitializerFields, useFormItemInitializerFields ,useInheritsFormItemInitializerFields} from '../utils';
+import { useCompile } from '../../schema-component';
export const ReadPrettyFormItemInitializers = (props: any) => {
const { t } = useTranslation();
const { insertPosition, component } = props;
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
+ const inheritFields = useInheritsFormItemInitializerFields();
+ const compile = useCompile();
+ const fieldItems: any[] = [
+ {
+ type: 'itemGroup',
+ title: t('Display fields'),
+ children: useFormItemInitializerFields(),
+ },
+ ];
+ if (inheritFields?.length > 0) {
+ inheritFields.forEach((inherit) => {
+ fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'itemGroup',
+ title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
+ children: Object.values(inherit)[0],
+ },
+ );
+ });
+ }
+ associationFields.length > 0 &&
+ fieldItems.push([
+ {
+ type: 'divider',
+ },
+ {
+ type: 'itemGroup',
+ title: t('Display association fields'),
+ children: associationFields,
+ },
+ ]);
+ fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'item',
+ title: t('Add text'),
+ component: 'BlockInitializer',
+ schema: {
+ type: 'void',
+ 'x-editable': false,
+ 'x-decorator': 'FormItem',
+ 'x-designer': 'Markdown.Void.Designer',
+ 'x-component': 'Markdown.Void',
+ 'x-component-props': {
+ content: t('This is a demo text, **supports Markdown syntax**.'),
+ },
+ },
+ },
+ );
return (
(
- [
- {
- type: 'itemGroup',
- title: t('Display fields'),
- children: useFormItemInitializerFields(),
- },
- ],
- associationFields.length > 0
- ? [
- {
- type: 'divider',
- },
- {
- type: 'itemGroup',
- title: t('Display association fields'),
- children: associationFields,
- },
- ]
- : [],
- [
- {
- type: 'divider',
- },
- {
- type: 'item',
- title: t('Add text'),
- component: 'BlockInitializer',
- schema: {
- type: 'void',
- 'x-editable': false,
- 'x-decorator': 'FormItem',
- 'x-designer': 'Markdown.Void.Designer',
- 'x-component': 'Markdown.Void',
- 'x-component-props': {
- content: t('This is a demo text, **supports Markdown syntax**.'),
- },
- },
- },
- ],
- )}
+ items={fieldItems}
insertPosition={insertPosition}
component={component}
title={component ? null : t('Configure fields')}
diff --git a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx
index 31a6b8aa65..a4fed82adb 100644
--- a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx
@@ -104,6 +104,23 @@ const useRelationFields = () => {
return relationFields;
};
+const useInheritFields = () => {
+ const collection = useCollection();
+ const { getChildrenCollections } = useCollectionManager();
+ const childrenCollections = getChildrenCollections(collection.name);
+ return childrenCollections.map((c) => {
+ return {
+ key: c.key,
+ type: 'item',
+ title: c?.title || c.name,
+ component: 'RecordReadPrettyFormBlockInitializer',
+ icon: false,
+ targetCollection: c,
+ actionInitializers: 'CalendarFormActionInitializers',
+ };
+ });
+};
+
export const RecordBlockInitializers = (props: any) => {
const { t } = useTranslation();
const { insertPosition, component } = props;
@@ -134,6 +151,11 @@ export const RecordBlockInitializers = (props: any) => {
},
],
},
+ {
+ type: 'itemGroup',
+ title: '{{t("Children collection blocks")}}',
+ children: useInheritFields(),
+ },
{
type: 'itemGroup',
title: '{{t("Relationship blocks")}}',
diff --git a/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx
index ea9a72c94d..fbe00c3f93 100644
--- a/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx
+++ b/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx
@@ -1,34 +1,65 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../SchemaInitializer';
-import { itemsMerge, useAssociatedTableColumnInitializerFields, useTableColumnInitializerFields } from '../utils';
+import {
+ itemsMerge,
+ useAssociatedTableColumnInitializerFields,
+ useTableColumnInitializerFields,
+ useInheritsTableColumnInitializerFields,
+} from '../utils';
+import { useCompile } from '../../schema-component';
// 表格列配置
export const TableColumnInitializers = (props: any) => {
const { items = [] } = props;
const { t } = useTranslation();
const associatedFields = useAssociatedTableColumnInitializerFields();
- const fieldItems: any[] = [{
- type: 'itemGroup',
- title: t('Display fields'),
- children: useTableColumnInitializerFields(),
- }];
- if (associatedFields?.length > 0) {
- fieldItems.push({
- type: 'divider',
- }, {
+ const inheritFields = useInheritsTableColumnInitializerFields();
+ const compile = useCompile();
+ const fieldItems: any[] = [
+ {
type: 'itemGroup',
- title: t('Display association fields'),
- children: associatedFields,
- })
+ title: t('Display fields'),
+ children: useTableColumnInitializerFields(),
+ },
+ ];
+ if (inheritFields?.length > 0) {
+ inheritFields.forEach((inherit) => {
+ Object.values(inherit)[0].length &&
+ fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'itemGroup',
+ title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
+ children: Object.values(inherit)[0],
+ },
+ );
+ });
}
- fieldItems.push({
- type: 'divider',
- }, {
- type: 'item',
- title: t('Action column'),
- component: 'TableActionColumnInitializer',
- })
+ if (associatedFields?.length > 0) {
+ fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'itemGroup',
+ title: t('Display association fields'),
+ children: associatedFields,
+ },
+ );
+ }
+ fieldItems.push(
+ {
+ type: 'divider',
+ },
+ {
+ type: 'item',
+ title: t('Action column'),
+ component: 'TableActionColumnInitializer',
+ },
+ );
return (
{
},
};
}}
- items={itemsMerge(
- fieldItems,
- items,
- )}
+ items={itemsMerge(fieldItems, items)}
>
{t('Configure columns')}
diff --git a/packages/core/client/src/schema-initializer/items/CalendarBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/CalendarBlockInitializer.tsx
index aec48e1156..4c808ab5a3 100644
--- a/packages/core/client/src/schema-initializer/items/CalendarBlockInitializer.tsx
+++ b/packages/core/client/src/schema-initializer/items/CalendarBlockInitializer.tsx
@@ -12,7 +12,7 @@ import { DataBlockInitializer } from "./DataBlockInitializer";
export const CalendarBlockInitializer = (props) => {
const { insert } = props;
const { t } = useTranslation();
- const { getCollection } = useCollectionManager();
+ const { getCollectionFields } = useCollectionManager();
const options = useContext(SchemaOptionsContext);
return (
{
componentType={'Calendar'}
icon={}
onCreateBlockSchema={async ({ item }) => {
- const collection = getCollection(item.name);
- const stringFields = collection?.fields
+ const collectionFields = getCollectionFields(item.name);
+ const stringFields = collectionFields
?.filter((field) => field.type === 'string')
?.map((field) => {
return {
@@ -29,7 +29,7 @@ export const CalendarBlockInitializer = (props) => {
value: field.name,
};
});
- const dateFields = collection?.fields
+ const dateFields = collectionFields
?.filter((field) => field.type === 'date')
?.map((field) => {
return {
diff --git a/packages/core/client/src/schema-initializer/items/KanbanBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/KanbanBlockInitializer.tsx
index 3d78066d79..b3ae1f2587 100644
--- a/packages/core/client/src/schema-initializer/items/KanbanBlockInitializer.tsx
+++ b/packages/core/client/src/schema-initializer/items/KanbanBlockInitializer.tsx
@@ -13,7 +13,7 @@ import { SchemaComponent, SchemaComponentOptions } from "../../schema-component"
export const KanbanBlockInitializer = (props) => {
const { insert } = props;
const { t } = useTranslation();
- const { getCollection } = useCollectionManager();
+ const { getCollectionFields,getCollection } = useCollectionManager();
const options = useContext(SchemaOptionsContext);
const api = useAPIClient();
return (
@@ -22,8 +22,8 @@ export const KanbanBlockInitializer = (props) => {
componentType={'Kanban'}
icon={}
onCreateBlockSchema={async ({ item }) => {
- const collection = getCollection(item.name);
- const fields = collection?.fields
+ const collectionFields = getCollectionFields(item.name);
+ const fields = collectionFields
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
?.map((field) => {
return {
@@ -64,7 +64,7 @@ export const KanbanBlockInitializer = (props) => {
initialValues: {},
});
const sortName = `${values.groupField.value}_sort`;
- const exists = collection?.fields?.find((field) => field.name === sortName);
+ const exists = collectionFields?.find((field) => field.name === sortName);
if (!exists) {
await api.resource('collections.fields', item.name).create({
values: {
diff --git a/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx
index 4081d84bd2..5643ea0277 100644
--- a/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx
+++ b/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx
@@ -7,10 +7,17 @@ import { SchemaInitializer } from '../SchemaInitializer';
import { createReadPrettyFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
export const RecordReadPrettyFormBlockInitializer = (props) => {
- const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
-
+ const {
+ onCreateBlockSchema,
+ componentType,
+ createBlockSchema,
+ insert,
+ icon = true,
+ targetCollection,
+ ...others
+ } = props;
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
- const collection = useCollection();
+ const collection = targetCollection || useCollection();
const association = useBlockAssociationContext();
const { block } = useBlockRequestContext();
const actionInitializers =
@@ -18,7 +25,7 @@ export const RecordReadPrettyFormBlockInitializer = (props) => {
return (
}
+ icon={icon && }
{...others}
key={'123'}
onClick={async ({ item }) => {
diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts
index e7719d11d8..8934bdd61e 100644
--- a/packages/core/client/src/schema-initializer/utils.ts
+++ b/packages/core/client/src/schema-initializer/utils.ts
@@ -73,15 +73,15 @@ export const findTableColumn = (schema: Schema, key: string, action: string, dee
};
export const useTableColumnInitializerFields = () => {
- const { name, fields = [] } = useCollection();
+ const { currentFields = [] } = useCollection();
const { getInterface } = useCollectionManager();
- return fields
+ return currentFields
.filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey)
.map((field) => {
const interfaceConfig = getInterface(field.interface);
const schema = {
name: field.name,
- 'x-collection-field': `${name}.${field.name}`,
+ 'x-collection-field': `${field.name}`,
'x-component': 'CollectionField',
'x-read-pretty': true,
'x-component-props': {},
@@ -105,7 +105,6 @@ export const useTableColumnInitializerFields = () => {
export const useAssociatedTableColumnInitializerFields = () => {
const { name, fields } = useCollection();
const { getInterface, getCollectionFields } = useCollectionManager();
-
const groups = fields
?.filter((field) => {
return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface);
@@ -141,7 +140,6 @@ export const useAssociatedTableColumnInitializerFields = () => {
schema,
} as SchemaInitializerItemOptions;
});
-
return {
type: 'subMenu',
title: field.uiSchema?.title,
@@ -152,13 +150,51 @@ export const useAssociatedTableColumnInitializerFields = () => {
return groups;
};
+export const useInheritsTableColumnInitializerFields = () => {
+ const { name } = useCollection();
+ const { getInterface, getParentCollections, getCollection, getParentCollectionFields } = useCollectionManager();
+ const inherits = getParentCollections(name);
+ return inherits?.map((v) => {
+ const fields = getParentCollectionFields(v, name);
+ const targetCollection = getCollection(v);
+ return {
+ [targetCollection?.title]: fields
+ ?.filter((field) => {
+ return field?.interface;
+ })
+ .map((k) => {
+ const interfaceConfig = getInterface(k.interface);
+ const schema = {
+ name: `${k.name}`,
+ 'x-component': 'CollectionField',
+ 'x-read-pretty': true,
+ 'x-collection-field': `${k.name}`,
+ 'x-component-props': {},
+ };
+ return {
+ type: 'item',
+ title: k?.uiSchema?.title || k.name,
+ component: 'TableCollectionFieldInitializer',
+ find: findTableColumn,
+ remove: removeTableColumn,
+ schemaInitialize: (s) => {
+ interfaceConfig?.schemaInitialize?.(s, { field: k, readPretty: true, block: 'Table' });
+ },
+ field: k,
+ schema,
+ } as SchemaInitializerItemOptions;
+ }),
+ };
+ });
+};
+
export const useFormItemInitializerFields = (options?: any) => {
- const { name, fields } = useCollection();
+ const { name, currentFields } = useCollection();
const { getInterface } = useCollectionManager();
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
- return fields
+ return currentFields
?.filter((field) => field?.interface && !field?.isForeignKey)
?.map((field) => {
const interfaceConfig = getInterface(field.interface);
@@ -170,7 +206,7 @@ export const useFormItemInitializerFields = (options?: any) => {
'x-designer': 'FormItem.Designer',
'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField',
'x-decorator': 'FormItem',
- 'x-collection-field': `${name}.${field.name}`,
+ 'x-collection-field': `${field.name}`,
'x-component-props': {},
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
};
@@ -240,13 +276,52 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
return groups;
};
+export const useInheritsFormItemInitializerFields = (options?) => {
+ const { name } = useCollection();
+ const { getInterface, getParentCollections, getCollection, getParentCollectionFields } = useCollectionManager();
+ const inherits = getParentCollections(name);
+ return inherits?.map((v) => {
+ const fields = getParentCollectionFields(v, name);
+ const form = useForm();
+ const { readPretty = form.readPretty, block = 'Form' } = options || {};
+ const targetCollection = getCollection(v);
+ return {
+ [targetCollection.title]: fields
+ ?.filter((field) => field?.interface && !field?.isForeignKey)
+ ?.map((field) => {
+ const interfaceConfig = getInterface(field.interface);
+ const schema = {
+ type: 'string',
+ name: field.name,
+ title: field?.uiSchema?.title || field.name,
+ 'x-designer': 'FormItem.Designer',
+ 'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField',
+ 'x-decorator': 'FormItem',
+ 'x-collection-field': `${field.name}`,
+ 'x-component-props': {},
+ 'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
+ };
+ return {
+ type: 'item',
+ title: field?.uiSchema?.title || field.name,
+ component: 'CollectionFieldInitializer',
+ remove: removeGridFormItem,
+ schemaInitialize: (s) => {
+ interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty });
+ },
+ schema,
+ } as SchemaInitializerItemOptions;
+ }),
+ };
+ });
+};
export const useCustomFormItemInitializerFields = (options?: any) => {
- const { name, fields } = useCollection();
+ const { name, currentFields } = useCollection();
const { getInterface } = useCollectionManager();
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const remove = useRemoveGridFormItem();
- return fields
+ return currentFields
?.filter((field) => {
return field?.interface && !field?.uiSchema?.['x-read-pretty'];
})
@@ -259,7 +334,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => {
'x-designer': 'FormItem.Designer',
'x-component': 'AssignedField',
'x-decorator': 'FormItem',
- 'x-collection-field': `${name}.${field.name}`,
+ 'x-collection-field': `${field.name}`,
};
return {
type: 'item',
@@ -293,7 +368,7 @@ export const useCustomBulkEditFormItemInitializerFields = (options?: any) => {
'x-designer': 'FormItem.Designer',
'x-component': 'BulkEditField',
'x-decorator': 'FormItem',
- 'x-collection-field': `${name}.${field.name}`,
+ 'x-collection-field': `${field.name}`,
};
return {
type: 'item',
@@ -344,6 +419,7 @@ export const useCurrentSchema = (action: string, key: string, find = findSchema,
}
const { remove } = useDesignable();
const schema = find(fieldSchema, key, action);
+ console.log(fieldSchema, key, action);
return {
schema,
exists: !!schema,
diff --git a/packages/core/client/src/user/CurrentUser.tsx b/packages/core/client/src/user/CurrentUser.tsx
index 7346a064ed..13aebc73e5 100644
--- a/packages/core/client/src/user/CurrentUser.tsx
+++ b/packages/core/client/src/user/CurrentUser.tsx
@@ -1,5 +1,5 @@
import { Dropdown, Menu } from 'antd';
-import React, { createContext, useState } from 'react';
+import React, { createContext, useState, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { useAPIClient, useCurrentUserContext } from '..';
@@ -8,18 +8,15 @@ import { ChangePassword } from './ChangePassword';
import { EditProfile } from './EditProfile';
import { LanguageSettings } from './LanguageSettings';
import { SwitchRole } from './SwitchRole';
+import {useCurrentDatabase} from '../database/CurrentDatabaseProvider'
+
const ApplicationVersion = () => {
- const { data, loading } = useRequest({
- url: 'app:getInfo',
- });
- if (loading) {
- return null;
- }
+ const data=useCurrentDatabase();
return (
-
- Version {data?.data?.version}
-
+
+ Version {data?.data?.version}
+
);
};
diff --git a/packages/core/database/src/__tests__/inhertits/collection-inherits-sync.test.ts b/packages/core/database/src/__tests__/inhertits/collection-inherits-sync.test.ts
new file mode 100644
index 0000000000..8bddfe899c
--- /dev/null
+++ b/packages/core/database/src/__tests__/inhertits/collection-inherits-sync.test.ts
@@ -0,0 +1,38 @@
+import { Collection } from '../../collection';
+import Database from '../../database';
+import { InheritedCollection } from '../../inherited-collection';
+import { mockDatabase } from '../index';
+import pgOnly from './helper';
+
+pgOnly()('sync inherits', () => {
+ let db: Database;
+
+ beforeEach(async () => {
+ db = mockDatabase();
+ });
+
+ afterEach(async () => {
+ await db.close();
+ });
+
+ it('should update table fields', async () => {
+ const person = db.collection({
+ name: 'person',
+ fields: [{ type: 'string', name: 'name' }],
+ });
+
+ const student = db.collection({
+ name: 'student',
+ inherits: 'person',
+ });
+
+ await db.sync();
+
+ student.setField('score', { type: 'integer' });
+
+ await db.sync();
+
+ const studentTableInfo = await db.sequelize.getQueryInterface().describeTable(student.model.tableName);
+ expect(studentTableInfo.score).toBeDefined();
+ });
+});
diff --git a/packages/core/database/src/__tests__/inhertits/collection-inherits.test.ts b/packages/core/database/src/__tests__/inhertits/collection-inherits.test.ts
new file mode 100644
index 0000000000..9c3e50fd17
--- /dev/null
+++ b/packages/core/database/src/__tests__/inhertits/collection-inherits.test.ts
@@ -0,0 +1,706 @@
+import Database from '../../database';
+import { InheritedCollection } from '../../inherited-collection';
+import { mockDatabase } from '../index';
+import pgOnly from './helper';
+
+pgOnly()('collection inherits', () => {
+ let db: Database;
+
+ beforeEach(async () => {
+ db = mockDatabase();
+ await db.clean({ drop: true });
+ });
+
+ afterEach(async () => {
+ await db.close();
+ });
+
+ it('can update relation with child table', async () => {
+ const A = db.collection({
+ name: 'a',
+ fields: [
+ {
+ name: 'a-field',
+ type: 'string',
+ },
+ {
+ name: 'a1s',
+ type: 'hasMany',
+ target: 'a1',
+ foreignKey: 'aId',
+ },
+ ],
+ });
+
+ const A1 = db.collection({
+ name: 'a1',
+ inherits: ['a'],
+ fields: [
+ {
+ name: 'a1-field',
+ type: 'string',
+ },
+ ],
+ });
+
+ await db.sync();
+
+ await A.repository.create({
+ values: {
+ 'a-field': 'a-1',
+ },
+ });
+
+ let a11 = await A1.repository.create({
+ values: {
+ 'a1-field': 'a1-1',
+ 'a-field': 'a1-1',
+ },
+ });
+
+ const a12 = await A1.repository.create({
+ values: {
+ 'a1-field': 'a1-2',
+ 'a-field': 'a1-2',
+ },
+ });
+
+ await A1.repository.update({
+ filterByTk: a11.get('id'),
+ values: {
+ a1s: [{ id: a12.get('id') }],
+ },
+ });
+
+ a11 = await A1.repository.findOne({
+ filter: {
+ 'a1-field': 'a1-1',
+ },
+ appends: ['a1s'],
+ });
+
+ const a11a1s = a11.get('a1s');
+ expect(a11a1s[0].get('id')).toBe(a12.get('id'));
+ });
+
+ it('can create relation with child table', async () => {
+ const A = db.collection({
+ name: 'a',
+ fields: [
+ {
+ name: 'af',
+ type: 'string',
+ },
+ {
+ name: 'bs',
+ type: 'hasMany',
+ target: 'b',
+ },
+ ],
+ });
+
+ const B = db.collection({
+ name: 'b',
+ inherits: ['a'],
+ fields: [
+ {
+ name: 'bf',
+ type: 'string',
+ },
+ {
+ name: 'a',
+ type: 'belongsTo',
+ target: 'a',
+ },
+ ],
+ });
+
+ await db.sync();
+
+ const a1 = await B.repository.create({
+ values: {
+ af: 'a1',
+ bs: [{ bf: 'b1' }, { bf: 'b2' }],
+ },
+ });
+
+ expect(a1.get('bs').length).toBe(2);
+
+ const b1 = await B.repository.findOne({
+ filter: {
+ af: 'a1',
+ },
+ appends: ['bs'],
+ });
+
+ expect(b1.get('bs').length).toBe(2);
+ });
+
+ it('should inherit belongsToMany field', async () => {
+ db.collection({
+ name: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'tags',
+ type: 'belongsToMany',
+ },
+ ],
+ });
+
+ db.collection({
+ name: 'tags',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'person',
+ type: 'belongsToMany',
+ },
+ ],
+ });
+
+ db.collection({
+ name: 'students',
+ inherits: 'person',
+ fields: [
+ {
+ name: 'score',
+ type: 'integer',
+ },
+ ],
+ });
+
+ await db.sync();
+
+ await db.getCollection('students').repository.create({
+ values: {
+ name: 'John',
+ score: 100,
+ tags: [
+ {
+ name: 't1',
+ },
+ {
+ name: 't2',
+ },
+ {
+ name: 't3',
+ },
+ ],
+ },
+ });
+
+ await db.getCollection('person').repository.create({
+ values: {
+ name: 'Max',
+ tags: [
+ {
+ name: 't2',
+ },
+ {
+ name: 't4',
+ },
+ ],
+ },
+ });
+
+ const john = await db.getCollection('students').repository.findOne({
+ appends: ['tags'],
+ });
+
+ expect(john.get('name')).toBe('John');
+ expect(john.get('tags')).toHaveLength(3);
+
+ const max = await db.getCollection('person').repository.findOne({
+ appends: ['tags'],
+ filter: {
+ name: 'Max',
+ },
+ });
+
+ expect(max.get('name')).toBe('Max');
+ expect(max.get('tags')).toHaveLength(2);
+ });
+
+ it('should inherit hasMany field', async () => {
+ db.collection({
+ name: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ {
+ name: 'pets',
+ type: 'hasMany',
+ },
+ ],
+ });
+
+ db.collection({
+ name: 'pets',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ });
+
+ db.collection({
+ name: 'students',
+ inherits: 'person',
+ fields: [
+ {
+ name: 'score',
+ type: 'integer',
+ },
+ ],
+ });
+
+ await db.sync();
+
+ await db.getCollection('person').repository.create({
+ values: {
+ name: 'Max',
+ pets: [
+ {
+ name: 'doge1',
+ },
+ {
+ name: 'kitty1',
+ },
+ ],
+ },
+ });
+
+ await db.getCollection('students').repository.create({
+ values: {
+ name: 'John',
+ score: 100,
+ pets: [
+ {
+ name: 'doge',
+ },
+ {
+ name: 'kitty',
+ },
+ {
+ name: 'doge2',
+ },
+ ],
+ },
+ });
+
+ const john = await db.getCollection('students').repository.findOne({
+ appends: ['pets'],
+ });
+
+ expect(john.get('name')).toBe('John');
+ expect(john.get('pets')).toHaveLength(3);
+ });
+
+ it('can inherit from multiple collections', async () => {
+ const a = db.collection({
+ name: 'a',
+ fields: [{ type: 'string', name: 'a1' }],
+ });
+
+ const b = db.collection({
+ name: 'b',
+ fields: [{ type: 'string', name: 'b1' }],
+ });
+
+ const c = db.collection({
+ name: 'c',
+ inherits: ['a', 'b'],
+ fields: [
+ { type: 'integer', name: 'id', autoIncrement: true },
+ { type: 'string', name: 'c1' },
+ ],
+ });
+
+ await db.sync();
+
+ expect(c.getField('a1')).toBeTruthy();
+ expect(c.getField('b1')).toBeTruthy();
+ expect(c.getField('c1')).toBeTruthy();
+
+ const c1 = await c.repository.create({
+ values: {
+ a1: 'a1',
+ b1: 'b1',
+ c1: 'c1',
+ },
+ });
+
+ expect(c1.get('a1')).toBe('a1');
+ expect(c1.get('b1')).toBe('b1');
+ expect(c1.get('c1')).toBe('c1');
+
+ const a2 = await a.repository.create({
+ values: {
+ a1: 'a2',
+ },
+ });
+
+ expect(a2.get('id')).toEqual(2);
+ });
+
+ it('should update inherit field when parent field update', async () => {
+ db.collection({
+ name: 'person',
+ fields: [{ name: 'name', type: 'string', title: 'parent-name' }],
+ });
+
+ db.collection({
+ name: 'students',
+ inherits: 'person',
+ });
+
+ expect(db.getCollection('students').getField('name').get('title')).toBe('parent-name');
+
+ db.getCollection('person').setField('name', { type: 'string', title: 'new-name' });
+
+ expect(db.getCollection('person').getField('name').get('title')).toBe('new-name');
+ expect(db.getCollection('students').getField('name').get('title')).toBe('new-name');
+ });
+
+ it('should not replace child field when parent field update', async () => {
+ db.collection({
+ name: 'person',
+ fields: [{ name: 'name', type: 'string', title: 'parent-name' }],
+ });
+
+ db.collection({
+ name: 'students',
+ inherits: 'person',
+ fields: [{ name: 'name', type: 'string', title: 'student-name' }],
+ });
+
+ expect(db.getCollection('students').getField('name').get('title')).toBe('student-name');
+
+ db.getCollection('person').setField('name', { type: 'string', title: 'new-name' });
+
+ expect(db.getCollection('person').getField('name').get('title')).toBe('new-name');
+ expect(db.getCollection('students').getField('name').get('title')).toBe('student-name');
+ });
+
+ it('should replace child association target', async () => {
+ db.collection({
+ name: 'person',
+ fields: [
+ { name: 'name', type: 'string' },
+ { type: 'hasOne', name: 'profile' },
+ ],
+ });
+
+ db.collection({
+ name: 'profiles',
+ fields: [
+ { name: 'age', type: 'integer' },
+ {
+ type: 'belongsTo',
+ name: 'person',
+ },
+ ],
+ });
+
+ db.collection({
+ name: 'teachers',
+ inherits: 'person',
+ fields: [{ name: 'salary', type: 'integer' }],
+ });
+
+ db.collection({
+ name: 'students',
+ inherits: 'person',
+ fields: [
+ { name: 'score', type: 'integer' },
+ {
+ type: 'hasOne',
+ name: 'profile',
+ target: 'studentProfiles',
+ },
+ ],
+ });
+
+ db.collection({
+ name: 'studentProfiles',
+ fields: [{ name: 'grade', type: 'string' }],
+ });
+
+ await db.sync();
+
+ const student = await db.getCollection('students').repository.create({
+ values: {
+ name: 'foo',
+ score: 100,
+ profile: {
+ grade: 'A',
+ },
+ },
+ });
+
+ expect(student.get('profile').get('grade')).toBe('A');
+
+ const teacher = await db.getCollection('teachers').repository.create({
+ values: {
+ name: 'bar',
+ salary: 1000,
+ profile: {
+ age: 30,
+ },
+ },
+ });
+
+ expect(teacher.get('profile').get('age')).toBe(30);
+ });
+
+ it('should replace hasOne association field', async () => {
+ const person = db.collection({
+ name: 'person',
+ fields: [
+ { name: 'name', type: 'string' },
+ { type: 'hasOne', name: 'profile', target: 'profiles', foreignKey: 'person_id' },
+ ],
+ });
+
+ const profile = db.collection({
+ name: 'profiles',
+ fields: [
+ { name: 'age', type: 'integer' },
+ {
+ type: 'belongsTo',
+ name: 'person',
+ },
+ ],
+ });
+
+ const student = db.collection({
+ name: 'students',
+ inherits: 'person',
+ fields: [{ name: 'profile', type: 'hasOne', target: 'studentProfiles', foreignKey: 'student_id' }],
+ });
+
+ const studentProfile = db.collection({
+ name: 'studentProfiles',
+ fields: [{ name: 'score', type: 'integer' }],
+ });
+
+ await db.sync();
+
+ const student1 = await student.repository.create({
+ values: {
+ name: 'student-1',
+ profile: {
+ score: '100',
+ },
+ },
+ });
+
+ let person1 = await person.repository.findOne();
+ await person.repository
+ .relation('profile')
+ .of(person1.get('id'))
+ .create({
+ values: {
+ age: 30,
+ },
+ });
+
+ person1 = await person.repository.findOne({
+ appends: ['profile'],
+ });
+
+ expect(person1.get('profile').get('age')).toBe(30);
+
+ expect(student1.get('profile').get('score')).toBe(100);
+ });
+
+ it('should inherit hasOne association field', async () => {
+ const person = db.collection({
+ name: 'person',
+ fields: [
+ { name: 'name', type: 'string' },
+ { type: 'hasOne', name: 'profile' },
+ ],
+ });
+
+ const profile = db.collection({
+ name: 'profiles',
+ fields: [
+ { name: 'age', type: 'integer' },
+ {
+ type: 'belongsTo',
+ name: 'person',
+ },
+ ],
+ });
+
+ db.collection({
+ name: 'students',
+ inherits: 'person',
+ fields: [{ name: 'score', type: 'integer' }],
+ });
+
+ db.collection({
+ name: 'teachers',
+ inherits: 'person',
+ fields: [{ name: 'salary', type: 'integer' }],
+ });
+
+ await db.sync();
+
+ await db.getCollection('students').repository.create({
+ values: {
+ name: 'foo',
+ score: 100,
+ profile: {
+ age: 18,
+ },
+ },
+ });
+
+ await db.getCollection('teachers').repository.create({
+ values: {
+ name: 'bar',
+ salary: 1000,
+ profile: {
+ age: 30,
+ },
+ },
+ });
+
+ const studentFoo = await db.getCollection('students').repository.findOne({
+ appends: ['profile'],
+ });
+
+ const teacherBar = await db.getCollection('teachers').repository.findOne({
+ appends: ['profile'],
+ });
+
+ expect(studentFoo.get('profile').age).toBe(18);
+ expect(teacherBar.get('profile').age).toBe(30);
+ });
+
+ it('should inherit from Collection', async () => {
+ const person = db.collection({
+ name: 'person',
+ fields: [{ name: 'name', type: 'string' }],
+ });
+
+ const student = db.collection({
+ name: 'student',
+ inherits: 'person',
+ fields: [{ name: 'score', type: 'integer' }],
+ });
+
+ await db.sync();
+
+ const StudentRepository = student.repository;
+
+ await StudentRepository.create({
+ values: { name: 'student1' },
+ });
+
+ expect(await person.repository.count()).toBe(1);
+ });
+
+ it('should create inherited table', async () => {
+ const person = db.collection({
+ name: 'person',
+ fields: [{ name: 'name', type: 'string' }],
+ });
+
+ const student = db.collection({
+ name: 'student',
+ inherits: 'person',
+ fields: [{ name: 'score', type: 'integer' }],
+ });
+
+ await db.sync();
+
+ const studentTableInfo = await db.sequelize.getQueryInterface().describeTable(student.model.tableName);
+
+ expect(studentTableInfo.score).toBeDefined();
+ expect(studentTableInfo.name).toBeDefined();
+ expect(studentTableInfo.id).toBeDefined();
+ expect(studentTableInfo.createdAt).toBeDefined();
+ expect(studentTableInfo.updatedAt).toBeDefined();
+ });
+
+ it('should get parent fields', async () => {
+ const root = db.collection({
+ name: 'root',
+ fields: [{ name: 'rootField', type: 'string' }],
+ });
+
+ const parent1 = db.collection({
+ name: 'parent1',
+ inherits: 'root',
+ fields: [{ name: 'parent1Field', type: 'string' }],
+ });
+
+ const parent2 = db.collection({
+ name: 'parent2',
+ inherits: 'parent1',
+ fields: [{ name: 'parent2Field', type: 'string' }],
+ });
+
+ const parent21 = db.collection({
+ name: 'parent21',
+ fields: [{ name: 'parent21Field', type: 'string' }],
+ });
+
+ const child: InheritedCollection = db.collection({
+ name: 'child',
+ inherits: ['parent2', 'parent21'],
+ fields: [{ name: 'childField', type: 'string' }],
+ }) as InheritedCollection;
+
+ const parentFields = child.parentFields();
+ expect(parentFields.size).toBe(4);
+ });
+
+ it('should sync parent fields', async () => {
+ const person = db.collection({
+ name: 'person',
+ fields: [{ name: 'name', type: 'string' }],
+ });
+
+ const student = db.collection({
+ name: 'student',
+ inherits: 'person',
+ fields: [{ name: 'score', type: 'integer' }],
+ });
+
+ await db.sync();
+
+ expect(student.fields.get('name')).toBeDefined();
+
+ // add new field to parent
+ person.setField('age', { type: 'integer' });
+
+ await db.sync();
+
+ expect(student.fields.get('age')).toBeDefined();
+
+ const student1 = await db.getCollection('student').repository.create({
+ values: {
+ name: 'student1',
+ age: 10,
+ score: 100,
+ },
+ });
+
+ expect(student1.get('name')).toBe('student1');
+ expect(student1.get('age')).toBe(10);
+ });
+});
diff --git a/packages/core/database/src/__tests__/inhertits/helper.ts b/packages/core/database/src/__tests__/inhertits/helper.ts
new file mode 100644
index 0000000000..c9a195ae8a
--- /dev/null
+++ b/packages/core/database/src/__tests__/inhertits/helper.ts
@@ -0,0 +1,3 @@
+const pgOnly = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
+
+export default pgOnly;
diff --git a/packages/core/database/src/__tests__/inhertits/inherited-map.test.ts b/packages/core/database/src/__tests__/inhertits/inherited-map.test.ts
new file mode 100644
index 0000000000..873c934e8a
--- /dev/null
+++ b/packages/core/database/src/__tests__/inhertits/inherited-map.test.ts
@@ -0,0 +1,27 @@
+import InheritanceMap from '../../inherited-map';
+
+describe('InheritedMap', () => {
+ it('should setInherits', () => {
+ const map = new InheritanceMap();
+ map.setInheritance('b', 'a');
+
+ const nodeA = map.getNode('a');
+ const nodeB = map.getNode('b');
+
+ expect(nodeA.children.has(nodeB)).toBe(true);
+ expect(nodeB.parents.has(nodeA)).toBe(true);
+
+ expect(map.isParentNode('a')).toBe(true);
+ });
+
+ it('should get deep children', () => {
+ const map = new InheritanceMap();
+ map.setInheritance('b', 'a');
+ map.setInheritance('c', 'b');
+ map.setInheritance('c1', 'b');
+ map.setInheritance('d', 'c');
+
+ const children = map.getChildren('a');
+ expect(children.size).toBe(4);
+ });
+});
diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts
index d4beb55040..c37feb6108 100644
--- a/packages/core/database/src/collection.ts
+++ b/packages/core/database/src/collection.ts
@@ -22,6 +22,7 @@ export type CollectionSortable = string | boolean | { name?: string; scopeKey?:
export interface CollectionOptions extends Omit {
name: string;
tableName?: string;
+ inherits?: string[] | string;
filterTargetKey?: string;
fields?: FieldOptions[];
model?: string | ModelCtor;
@@ -74,7 +75,9 @@ export class Collection<
this.bindFieldEventListener();
this.modelInit();
+
this.db.modelCollection.set(this.model, this);
+ this.db.tableNameCollectionMap.set(this.model.tableName, this);
this.setFields(options.fields);
this.setRepository(options.repository);
@@ -181,9 +184,34 @@ export class Collection<
},
);
+ const oldField = this.fields.get(name);
+
+ if (oldField && oldField.options.inherit && options.type != oldField.options.type) {
+ throw new Error(
+ `Field type conflict: cannot set "${name}" to ${options.type}, parent "${name}" type is ${oldField.options.type}`,
+ );
+ }
+
this.removeField(name);
this.fields.set(name, field);
this.emit('field.afterAdd', field);
+
+ if (this.isParent()) {
+ for (const child of this.context.database.inheritanceMap.getChildren(this.name, {
+ deep: false,
+ })) {
+ const childCollection = this.db.getCollection(child);
+ const existField = childCollection.getField(name);
+
+ if (!existField || existField.options.inherit) {
+ childCollection.setField(name, {
+ ...options,
+ inherit: true,
+ });
+ }
+ }
+ }
+
return field;
}
@@ -371,6 +399,7 @@ export class Collection<
for (const associationKey in associations) {
const association = associations[associationKey];
modelNames.add(association.target.name);
+
if ((association).through) {
modelNames.add((association).through.model.name);
}
@@ -388,4 +417,12 @@ export class Collection<
await model.sync(syncOptions);
}
}
+
+ public isInherited() {
+ return false;
+ }
+
+ public isParent() {
+ return this.context.database.inheritanceMap.isParentNode(this.name);
+ }
}
diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts
index 07695e8951..72e5c22d92 100644
--- a/packages/core/database/src/database.ts
+++ b/packages/core/database/src/database.ts
@@ -56,6 +56,8 @@ import {
} from './types';
import { referentialIntegrityCheck } from './features/referential-integrity-check';
import ReferencesMap from './features/ReferencesMap';
+import { InheritedCollection } from './inherited-collection';
+import InheritanceMap from './inherited-map';
export interface MergeOptions extends merge.Options {}
@@ -148,7 +150,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
collections = new Map();
pendingFields = new Map();
modelCollection = new Map, Collection>();
+ tableNameCollectionMap = new Map();
+
referenceMap = new ReferencesMap();
+ inheritanceMap = new InheritanceMap();
modelHook: ModelHook;
version: DatabaseVersion;
@@ -293,9 +298,13 @@ export class Database extends EventEmitter implements AsyncEmitter {
): Collection {
this.emit('beforeDefineCollection', options);
- const collection = new Collection(options, {
- database: this,
- });
+ const collection = options.inherits
+ ? new InheritedCollection(options, {
+ database: this,
+ })
+ : new Collection(options, {
+ database: this,
+ });
this.collections.set(collection.name, collection);
@@ -429,6 +438,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
if (isMySQL) {
await this.sequelize.query('SET FOREIGN_KEY_CHECKS = 1', null);
}
+
return result;
}
diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts
index 281e48496f..a5ffd1d314 100644
--- a/packages/core/database/src/fields/field.ts
+++ b/packages/core/database/src/fields/field.ts
@@ -10,6 +10,7 @@ import {
import { Collection } from '../collection';
import { Database } from '../database';
import { ModelEventTypes } from '../types';
+import { InheritedCollection } from '../inherited-collection';
export interface FieldContext {
database: Database;
@@ -19,6 +20,7 @@ export interface FieldContext {
export interface BaseFieldOptions {
name?: string;
hidden?: boolean;
+
[key: string]: any;
}
@@ -32,8 +34,17 @@ export abstract class Field {
context: FieldContext;
database: Database;
collection: Collection;
+
[key: string]: any;
+ constructor(options?: any, context?: FieldContext) {
+ this.context = context;
+ this.database = context.database;
+ this.collection = context.collection;
+ this.options = options || {};
+ this.init();
+ }
+
get name() {
return this.options.name;
}
@@ -46,14 +57,6 @@ export abstract class Field {
return this.options.dataType;
}
- constructor(options?: any, context?: FieldContext) {
- this.context = context;
- this.database = context.database;
- this.collection = context.collection;
- this.options = options || {};
- this.init();
- }
-
async sync(syncOptions: SyncOptions) {
await this.collection.sync({
...syncOptions,
@@ -88,11 +91,18 @@ export abstract class Field {
}
async removeFromDb(options?: QueryInterfaceOptions) {
- if (!this.collection.model.rawAttributes[this.name]) {
+ const attribute = this.collection.model.rawAttributes[this.name];
+
+ if (!attribute) {
this.remove();
// console.log('field is not attribute');
return;
}
+
+ if (this.collection.isInherited() && (this.collection).parentFields().has(this.name)) {
+ return;
+ }
+
if ((this.collection.model as any)._virtualAttributes.has(this.name)) {
this.remove();
// console.log('field is virtual attribute');
@@ -168,7 +178,7 @@ export abstract class Field {
}
bind() {
- const { model } = this.context.collection;
+ const {model} = this.context.collection;
model.rawAttributes[this.name] = this.toSequelize();
// @ts-ignore
model.refreshAttributes();
@@ -178,7 +188,7 @@ export abstract class Field {
}
unbind() {
- const { model } = this.context.collection;
+ const {model} = this.context.collection;
model.removeAttribute(this.name);
if (this.options.index || this.options.unique) {
this.context.collection.removeIndex([this.name]);
@@ -188,7 +198,7 @@ export abstract class Field {
toSequelize(): any {
const opts = _.omit(this.options, ['name']);
if (this.dataType) {
- Object.assign(opts, { type: this.dataType });
+ Object.assign(opts, {type: this.dataType});
}
return opts;
}
diff --git a/packages/core/database/src/index.ts b/packages/core/database/src/index.ts
index 811d04eda3..16495fa453 100644
--- a/packages/core/database/src/index.ts
+++ b/packages/core/database/src/index.ts
@@ -1,5 +1,6 @@
export { DataTypes, ModelCtor, Op, SyncOptions } from 'sequelize';
export * from './collection';
+export * from './inherited-collection';
export * from './database';
export { Database as default } from './database';
export * from './fields';
@@ -14,4 +15,3 @@ export * from './relation-repository/multiple-relation-repository';
export * from './relation-repository/single-relation-repository';
export * from './repository';
export * from './update-associations';
-
diff --git a/packages/core/database/src/inherited-collection.ts b/packages/core/database/src/inherited-collection.ts
new file mode 100644
index 0000000000..41a49e1827
--- /dev/null
+++ b/packages/core/database/src/inherited-collection.ts
@@ -0,0 +1,73 @@
+import { Collection, CollectionContext, CollectionOptions } from './collection';
+import { default as lodash } from 'lodash';
+import { Field } from '.';
+
+export class InheritedCollection extends Collection {
+ parents?: Collection[];
+
+ constructor(options: CollectionOptions, context: CollectionContext) {
+ if (!options.inherits) {
+ throw new Error('InheritedCollection must have inherits option');
+ }
+
+ super(options, context);
+ this.setParents(options.inherits);
+ this.context.database.inheritanceMap.setInheritance(this.name, options.inherits);
+ this.setParentFields();
+ }
+
+ protected setParents(inherits: string | string[]) {
+ this.parents = lodash.castArray(inherits).map((name) => this.context.database.collections.get(name));
+ }
+
+ protected setParentFields() {
+ for (const [name, field] of this.parentFields()) {
+ if (!this.hasField(name)) {
+ this.setField(name, {
+ ...field.options,
+ inherit: true,
+ });
+ }
+ }
+ }
+
+ getParents() {
+ return this.parents;
+ }
+
+ parentFields() {
+ const fields = new Map();
+ for (const parent of this.parents) {
+ if (parent.isInherited()) {
+ for (const [name, field] of (parent).parentFields()) {
+ fields.set(name, field);
+ }
+ }
+
+ const parentFields = parent.fields;
+ for (const [name, field] of parentFields) {
+ fields.set(name, field);
+ }
+ }
+ return fields;
+ }
+
+ parentAttributes() {
+ const attributes = {};
+ for (const parent of this.parents) {
+ if (parent.isInherited()) {
+ Object.assign(attributes, (parent).parentAttributes());
+ }
+
+ const parentAttributes = (parent.model).tableAttributes;
+
+ Object.assign(attributes, parentAttributes);
+ }
+
+ return attributes;
+ }
+
+ isInherited() {
+ return true;
+ }
+}
diff --git a/packages/core/database/src/inherited-map.ts b/packages/core/database/src/inherited-map.ts
new file mode 100644
index 0000000000..2ece2af4de
--- /dev/null
+++ b/packages/core/database/src/inherited-map.ts
@@ -0,0 +1,81 @@
+import lodash from 'lodash';
+
+class TableNode {
+ name: string;
+ parents: Set;
+ children: Set;
+ constructor(name: string) {
+ this.name = name;
+ this.parents = new Set();
+ this.children = new Set();
+ }
+}
+
+export default class InheritanceMap {
+ nodes: Map = new Map();
+
+ getOrCreateNode(name: string) {
+ if (!this.nodes.has(name)) {
+ this.nodes.set(name, new TableNode(name));
+ }
+ return this.getNode(name);
+ }
+
+ getNode(name: string) {
+ return this.nodes.get(name);
+ }
+
+ setInheritance(name: string, inherits: string | string[]) {
+ const node = this.getOrCreateNode(name);
+ const parents = lodash.castArray(inherits).map((name) => this.getOrCreateNode(name));
+
+ node.parents = new Set(parents);
+
+ for (const parent of parents) {
+ parent.children.add(node);
+ }
+ }
+
+ isParentNode(name: string) {
+ const node = this.getNode(name);
+ return node && node.children.size > 0;
+ }
+
+ getChildren(name: string, options: { deep: boolean } = { deep: true }): Set {
+ const results = new Set();
+ const node = this.getNode(name);
+ if (!node) return results;
+
+ for (const child of node.children) {
+ results.add(child.name);
+ if (!options.deep) {
+ continue;
+ }
+
+ for (const grandchild of this.getChildren(child.name)) {
+ results.add(grandchild);
+ }
+ }
+
+ return results;
+ }
+
+ getParents(name: string, options: { deep: boolean } = { deep: true }): Set {
+ const results = new Set();
+ const node = this.getNode(name);
+ if (!node) return results;
+
+ for (const parent of node.parents) {
+ results.add(parent.name);
+ if (!options.deep) {
+ continue;
+ }
+
+ for (const grandparent of this.getParents(parent.name)) {
+ results.add(grandparent);
+ }
+ }
+
+ return results;
+ }
+}
diff --git a/packages/core/database/src/model.ts b/packages/core/database/src/model.ts
index 84f94b4799..ca912c5e9e 100644
--- a/packages/core/database/src/model.ts
+++ b/packages/core/database/src/model.ts
@@ -3,6 +3,8 @@ import { Model as SequelizeModel, ModelCtor } from 'sequelize';
import { Collection } from './collection';
import { Database } from './database';
import { Field } from './fields';
+import type { InheritedCollection } from './inherited-collection';
+import { SyncRunner } from './sync-runner';
interface IModel {
[key: string]: any;
@@ -145,4 +147,14 @@ export class Model 0) {
// @ts-ignore
const primaryKeyField = model.primaryKeyField || model.primaryKeyAttribute;
@@ -258,27 +260,38 @@ export class Repository {
- return model
- .findAll({
- ...omit(opts, ['limit', 'offset']),
- include: include,
- where,
- transaction,
- })
- .then((rows) => {
- return { rows, include };
- });
+ const options = {
+ ...omit(opts, ['limit', 'offset']),
+ include: include,
+ where,
+ transaction,
+ };
+
+ return model.findAll(options).then((rows) => {
+ return { rows, include };
+ });
}),
templateModel: ids[0].row,
});
+ } else {
+ rows = await model.findAll({
+ ...opts,
+ transaction,
+ });
}
- return await model.findAll({
- ...opts,
- transaction,
- });
+ if (this.collection.isParent()) {
+ for (const row of rows) {
+ const rowCollectionName = this.database.tableNameCollectionMap.get(row.get('__tableName')).name;
+ row.set('__collection', rowCollectionName, {
+ raw: true,
+ });
+ }
+ }
+
+ return rows;
}
/**
diff --git a/packages/core/database/src/sync-runner.ts b/packages/core/database/src/sync-runner.ts
new file mode 100644
index 0000000000..53926e2d13
--- /dev/null
+++ b/packages/core/database/src/sync-runner.ts
@@ -0,0 +1,117 @@
+import { InheritedCollection } from './inherited-collection';
+import lodash from 'lodash';
+import { Sequelize } from 'sequelize';
+
+export class SyncRunner {
+ static async syncInheritModel(model: any, options: any) {
+ const { transaction } = options;
+
+ const inheritedCollection = model.collection as InheritedCollection;
+ const db = inheritedCollection.context.database;
+ const dialect = db.sequelize.getDialect();
+
+ const queryInterface = db.sequelize.getQueryInterface();
+
+ if (dialect != 'postgres') {
+ throw new Error('Inherit model is only supported on postgres');
+ }
+
+ const parents = inheritedCollection.parents;
+
+ const parentTables = parents.map((parent) => parent.model.tableName);
+
+ const tableName = model.getTableName();
+
+ const attributes = model.tableAttributes;
+
+ const childAttributes = lodash.pickBy(attributes, (value) => {
+ return !value.inherit;
+ });
+
+ let maxSequenceVal = 0;
+ let maxSequenceName;
+
+ if (childAttributes.id && childAttributes.id.autoIncrement) {
+ for (const parent of parentTables) {
+ const sequenceNameResult = await queryInterface.sequelize.query(
+ `select pg_get_serial_sequence('"${parent}"', 'id')`,
+ {
+ transaction,
+ },
+ );
+ const sequenceName = sequenceNameResult[0][0]['pg_get_serial_sequence'];
+
+ const sequenceCurrentValResult = await queryInterface.sequelize.query(
+ `select last_value from ${sequenceName}`,
+ {
+ transaction,
+ },
+ );
+ const sequenceCurrentVal = sequenceCurrentValResult[0][0]['last_value'];
+
+ if (sequenceCurrentVal > maxSequenceVal) {
+ maxSequenceName = sequenceName;
+ maxSequenceVal = sequenceCurrentVal;
+ }
+ }
+ }
+
+ await this.createTable(tableName, childAttributes, options, model, parentTables);
+
+ const parentsDeep = Array.from(db.inheritanceMap.getParents(inheritedCollection.name)).map(
+ (parent) => db.getCollection(parent).model.tableName,
+ );
+
+ const sequenceTables = [...parentsDeep, tableName];
+
+ for (const sequenceTable of sequenceTables) {
+ await queryInterface.sequelize.query(
+ `alter table "${sequenceTable}" alter column id set default nextval('${maxSequenceName}')`,
+ {
+ transaction,
+ },
+ );
+ }
+
+ if (options.alter) {
+ const columns = await queryInterface.describeTable(tableName, options);
+
+ for (const columnName in childAttributes) {
+ if (!columns[columnName]) {
+ await queryInterface.addColumn(tableName, columnName, childAttributes[columnName], options);
+ }
+ }
+ }
+ }
+
+ static async createTable(tableName, attributes, options, model, parentTables) {
+ let sql = '';
+
+ options = { ...options };
+
+ if (options && options.uniqueKeys) {
+ lodash.forOwn(options.uniqueKeys, (uniqueKey) => {
+ if (uniqueKey.customIndex === undefined) {
+ uniqueKey.customIndex = true;
+ }
+ });
+ }
+
+ if (model) {
+ options.uniqueKeys = options.uniqueKeys || model.uniqueKeys;
+ }
+
+ const queryGenerator = model.queryGenerator;
+
+ attributes = lodash.mapValues(attributes, (attribute) => model.sequelize.normalizeAttribute(attribute));
+
+ attributes = queryGenerator.attributesToSQL(attributes, { table: tableName, context: 'createTable' });
+
+ sql = `${queryGenerator.createTableQuery(tableName, attributes, options)}`.replace(
+ ';',
+ ` INHERITS (${parentTables.map((t) => `"${t}"`).join(', ')});`,
+ );
+
+ return await model.sequelize.query(sql, options);
+ }
+}
diff --git a/packages/core/test/src/index.ts b/packages/core/test/src/index.ts
index 4f222fecb2..89d3610ccc 100644
--- a/packages/core/test/src/index.ts
+++ b/packages/core/test/src/index.ts
@@ -1,3 +1,5 @@
export { mockDatabase } from '@nocobase/database';
export * from './mockServer';
+const pgOnly: () => jest.Describe = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
+export { pgOnly };
diff --git a/packages/plugins/client/src/server.ts b/packages/plugins/client/src/server.ts
index c92b7d4d5a..79949efbf1 100644
--- a/packages/plugins/client/src/server.ts
+++ b/packages/plugins/client/src/server.ts
@@ -22,6 +22,7 @@ export class ClientPlugin extends Plugin {
this.app.acl.allow('app', 'getInfo');
this.app.acl.allow('app', 'getPlugins');
this.app.acl.allow('plugins', 'getPinned', 'loggedIn');
+ const dialect = this.app.db.sequelize.getDialect();
this.app.resource({
name: 'app',
actions: {
@@ -35,6 +36,9 @@ export class ClientPlugin extends Plugin {
lang = currentUser?.appLang;
}
ctx.body = {
+ database: {
+ dialect,
+ },
version: await ctx.app.version.get(),
lang,
};
diff --git a/packages/plugins/collection-manager/src/__tests__/http-api/collections.test.ts b/packages/plugins/collection-manager/src/__tests__/http-api/collections.test.ts
index ad24931cc5..57f07fb2a6 100644
--- a/packages/plugins/collection-manager/src/__tests__/http-api/collections.test.ts
+++ b/packages/plugins/collection-manager/src/__tests__/http-api/collections.test.ts
@@ -8,77 +8,67 @@ describe('collections repository', () => {
beforeEach(async () => {
app = await createApp();
agent = app.agent();
- await agent
- .resource('collections')
- .create({
- values: {
- name: 'tags',
- fields: [
- {
- name: 'title',
- type: 'string',
- },
- ],
- },
- });
- await agent
- .resource('collections')
- .create({
- values: {
- name: 'foos',
- fields: [
- {
- name: 'title',
- type: 'string',
- },
- ],
- },
- });
- await agent
- .resource('collections.fields', 'tags')
- .create({
- values: {
- name: 'foos',
- target: 'foos',
- type: 'belongsToMany',
- },
- });
- await agent
- .resource('collections')
- .create({
- values: {
- name: 'comments',
- fields: [
- {
- name: 'title',
- type: 'string',
- },
- ],
- },
- });
- await agent
- .resource('collections')
- .create({
- values: {
- name: 'posts',
- fields: [
- {
- name: 'title',
- type: 'string',
- },
- {
- name: 'comments',
- target: 'comments',
- type: 'hasMany',
- },
- {
- name: 'tags',
- target: 'tags',
- type: 'belongsToMany',
- },
- ],
- },
- });
+ await agent.resource('collections').create({
+ values: {
+ name: 'tags',
+ fields: [
+ {
+ name: 'title',
+ type: 'string',
+ },
+ ],
+ },
+ });
+ await agent.resource('collections').create({
+ values: {
+ name: 'foos',
+ fields: [
+ {
+ name: 'title',
+ type: 'string',
+ },
+ ],
+ },
+ });
+ await agent.resource('collections.fields', 'tags').create({
+ values: {
+ name: 'foos',
+ target: 'foos',
+ type: 'belongsToMany',
+ },
+ });
+ await agent.resource('collections').create({
+ values: {
+ name: 'comments',
+ fields: [
+ {
+ name: 'title',
+ type: 'string',
+ },
+ ],
+ },
+ });
+ await agent.resource('collections').create({
+ values: {
+ name: 'posts',
+ fields: [
+ {
+ name: 'title',
+ type: 'string',
+ },
+ {
+ name: 'comments',
+ target: 'comments',
+ type: 'hasMany',
+ },
+ {
+ name: 'tags',
+ target: 'tags',
+ type: 'belongsToMany',
+ },
+ ],
+ },
+ });
});
afterEach(async () => {
@@ -95,71 +85,57 @@ describe('collections repository', () => {
it('case 2', async () => {
const response = await app.agent().resource('posts').create();
const postId = response.body.data.id;
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 1',
- },
- });
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 2',
- },
- });
- const response2 = await agent
- .resource('posts')
- .list({
- filter: {
- 'comments.title': 'comment 1',
- },
- });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 1',
+ },
+ });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 2',
+ },
+ });
+ const response2 = await agent.resource('posts').list({
+ filter: {
+ 'comments.title': 'comment 1',
+ },
+ });
expect(response2.body.data[0].id).toBe(1);
});
it('case 3', async () => {
const response = await app.agent().resource('posts').create();
const postId = response.body.data.id;
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 1',
- },
- });
- const response2 = await agent
- .resource('posts')
- .list({
- filter: {
- 'comments.id': 3,
- },
- });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 1',
+ },
+ });
+ const response2 = await agent.resource('posts').list({
+ filter: {
+ 'comments.id': 3,
+ },
+ });
expect(response2.body.data.length).toBe(0);
});
it('case 4', async () => {
const response = await app.agent().resource('posts').create();
const postId = response.body.data.id;
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 1',
- },
- });
- const response2 = await agent
- .resource('posts')
- .list({
- filter: {
- $and: [
- {
- 'comments.title': 'comment 1',
- },
- ],
- },
- });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 1',
+ },
+ });
+ const response2 = await agent.resource('posts').list({
+ filter: {
+ $and: [
+ {
+ 'comments.title': 'comment 1',
+ },
+ ],
+ },
+ });
expect(response2.body.data[0].id).toBe(1);
});
@@ -174,126 +150,108 @@ describe('collections repository', () => {
});
it('case 6', async () => {
- const response = await agent
- .resource('posts')
- .create({
- values: {
- tags: [
- {},
- {
- title: 'Tag1',
- },
- ],
- },
- });
+ const response = await agent.resource('posts').create({
+ values: {
+ tags: [
+ {},
+ {
+ title: 'Tag1',
+ },
+ ],
+ },
+ });
const postId = response.body.data.id;
const response1 = await app.agent().resource('posts.tags', postId).list();
expect(response1.body.data.length).toBe(2);
});
it('case 7', async () => {
- const response = await agent
- .resource('posts')
- .create({
- values: {
- tags: [
- {},
- {
- title: 'Tag1',
- },
- ],
- },
- });
+ const response = await agent.resource('posts').create({
+ values: {
+ tags: [
+ {},
+ {
+ title: 'Tag1',
+ },
+ ],
+ },
+ });
const postId = response.body.data.id;
- const response1 = await agent
- .resource('posts.tags', postId)
- .list({
- filter: {
- title: 'Tag1',
- },
- });
+ const response1 = await agent.resource('posts.tags', postId).list({
+ filter: {
+ title: 'Tag1',
+ },
+ });
expect(response1.body.data.length).toBe(1);
});
it('case 8', async () => {
- const response = await agent
- .resource('posts')
- .create({
- values: {
- tags: [
- {},
- {
- title: 'Tag1',
- },
- {
- title: 'Tag2',
- },
- ],
- },
- });
+ const response = await agent.resource('posts').create({
+ values: {
+ tags: [
+ {},
+ {
+ title: 'Tag1',
+ },
+ {
+ title: 'Tag2',
+ },
+ ],
+ },
+ });
const postId = response.body.data.id;
- const response1 = await agent
- .resource('posts.tags', postId)
- .list({
- filter: {
- $or: [{ title: 'Tag1' }, { title: 'Tag2' }],
- },
- });
+ const response1 = await agent.resource('posts.tags', postId).list({
+ filter: {
+ $or: [{ title: 'Tag1' }, { title: 'Tag2' }],
+ },
+ });
expect(response1.body.data.length).toBe(2);
});
it('case 9', async () => {
- const response = await agent
- .resource('posts')
- .create({
- values: {
- tags: [
- {},
- {
- title: 'Tag1',
- },
- {
- title: 'Tag2',
- },
- ],
- },
- });
+ const response = await agent.resource('posts').create({
+ values: {
+ tags: [
+ {},
+ {
+ title: 'Tag1',
+ },
+ {
+ title: 'Tag2',
+ },
+ ],
+ },
+ });
const postId = response.body.data.id;
- const response1 = await agent
- .resource('posts.tags', postId)
- .list({
- filter: {
- $or: [{ title: 'Tag1' }, { title: 'Tag2' }],
- },
- });
+ const response1 = await agent.resource('posts.tags', postId).list({
+ filter: {
+ $or: [{ title: 'Tag1' }, { title: 'Tag2' }],
+ },
+ });
expect(response1.body.data.length).toBe(2);
});
it('case 10', async () => {
- const response = await agent
- .resource('posts')
- .create({
- values: {
- tags: [
- {},
- {
- title: 'Tag1',
- },
- {
- title: 'Tag2',
- },
- ],
- },
- });
+ const response = await agent.resource('posts').create({
+ values: {
+ tags: [
+ {},
+ {
+ title: 'Tag1',
+ },
+ {
+ title: 'Tag2',
+ },
+ ],
+ },
+ });
const postId = response.body.data.id;
- const response1 = await agent
- .resource('posts.tags', postId)
- .list({
- appends: ['foos'],
- page: 1,
- pageSize: 20,
- sort: ['-createdAt', '-id'],
- });
+ const response1 = await agent.resource('posts.tags', postId).list({
+ appends: ['foos'],
+ page: 1,
+ pageSize: 20,
+ sort: ['-createdAt', '-id'],
+ });
expect(response1.body.data[0]['id']).toEqual(3);
});
@@ -301,91 +259,73 @@ describe('collections repository', () => {
it('case 11', async () => {
const response = await app.agent().resource('posts').create();
const postId = response.body.data.id;
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 1',
- },
- });
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 2',
- },
- });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 1',
+ },
+ });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 2',
+ },
+ });
const response2 = await app.agent().resource('posts').create();
const postId2 = response2.body.data.id;
- await agent
- .resource('posts.comments', postId2)
- .create({
- values: {
- title: 'comment 2',
- },
- });
- await agent
- .resource('posts.comments', postId2)
- .create({
- values: {
- title: 'comment 2',
- },
- });
- const response3 = await agent
- .resource('posts')
- .list({
- filter: {
- $or: [
- {
- 'comments.title': 'comment 1',
- },
- {
- 'comments.title': 'comment 2',
- },
- ],
- },
- });
+ await agent.resource('posts.comments', postId2).create({
+ values: {
+ title: 'comment 2',
+ },
+ });
+ await agent.resource('posts.comments', postId2).create({
+ values: {
+ title: 'comment 2',
+ },
+ });
+ const response3 = await agent.resource('posts').list({
+ filter: {
+ $or: [
+ {
+ 'comments.title': 'comment 1',
+ },
+ {
+ 'comments.title': 'comment 2',
+ },
+ ],
+ },
+ });
expect(response3.body.data.length).toBe(2);
});
it('case 12', async () => {
const response = await app.agent().resource('posts').create();
const postId = response.body.data.id;
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 1',
- },
- });
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 2',
- },
- });
- await agent
- .resource('posts.comments', postId)
- .create({
- values: {
- title: 'comment 3',
- },
- });
- const response2 = await agent
- .resource('posts.comments', postId)
- .list({
- filter: {
- $or: [
- {
- title: 'comment 1',
- },
- {
- title: 'comment 2',
- },
- ],
- },
- });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 1',
+ },
+ });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 2',
+ },
+ });
+ await agent.resource('posts.comments', postId).create({
+ values: {
+ title: 'comment 3',
+ },
+ });
+ const response2 = await agent.resource('posts.comments', postId).list({
+ filter: {
+ $or: [
+ {
+ title: 'comment 1',
+ },
+ {
+ title: 'comment 2',
+ },
+ ],
+ },
+ });
expect(response2.body.data.length).toBe(2);
});
@@ -394,35 +334,27 @@ describe('collections repository', () => {
const tag1 = await tagRepository.create({ values: { title: 'tag1' } });
const tag2 = await tagRepository.create({ values: { title: 'tag2' } });
const tag3 = await tagRepository.create({ values: { title: 'tag3' } });
- await agent
- .resource('posts')
- .create({
- values: {
- tags: [tag1.get('id'), tag3.get('id')],
- },
- });
- await agent
- .resource('posts')
- .create({
- values: {
- tags: [tag2.get('id')],
- },
- });
- await agent
- .resource('posts')
- .create({
- values: {
- tags: [tag2.get('id'), tag3.get('id')],
- },
- });
+ await agent.resource('posts').create({
+ values: {
+ tags: [tag1.get('id'), tag3.get('id')],
+ },
+ });
+ await agent.resource('posts').create({
+ values: {
+ tags: [tag2.get('id')],
+ },
+ });
+ await agent.resource('posts').create({
+ values: {
+ tags: [tag2.get('id'), tag3.get('id')],
+ },
+ });
- const response1 = await agent
- .resource('posts')
- .list({
- filter: {
- $or: [{ 'tags.title': 'tag1' }, { 'tags.title': 'tag3' }],
- },
- });
+ const response1 = await agent.resource('posts').list({
+ filter: {
+ $or: [{ 'tags.title': 'tag1' }, { 'tags.title': 'tag3' }],
+ },
+ });
expect(response1.body.data.length).toBe(2);
});
});
diff --git a/packages/plugins/collection-manager/src/__tests__/http-api/inherited-collection.test.ts b/packages/plugins/collection-manager/src/__tests__/http-api/inherited-collection.test.ts
new file mode 100644
index 0000000000..6a4be7baf3
--- /dev/null
+++ b/packages/plugins/collection-manager/src/__tests__/http-api/inherited-collection.test.ts
@@ -0,0 +1,281 @@
+import { MockServer, pgOnly } from '@nocobase/test';
+import { createApp } from '..';
+
+pgOnly()('Inherited Collection', () => {
+ let app: MockServer;
+ let agent;
+
+ beforeEach(async () => {
+ app = await createApp();
+ agent = app.agent();
+ await agent.resource('collections').create({
+ values: {
+ name: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ },
+ });
+ });
+
+ afterEach(async () => {
+ await app.destroy();
+ });
+
+ it('should not replace field with difference type when add field', async () => {
+ let response = await agent.resource('collections').create({
+ values: {
+ name: 'students',
+ inherits: 'person',
+ fields: [],
+ },
+ });
+
+ expect(response.statusCode).toBe(200);
+
+ response = await agent.resource('fields').create({
+ values: {
+ collectionName: 'students',
+ name: 'name',
+ type: 'integer',
+ },
+ });
+
+ expect(response.statusCode).not.toBe(200);
+ });
+
+ it('should not replace field with difference type when create collection', async () => {
+ const response = await agent.resource('collections').create({
+ context: {},
+ values: {
+ name: 'students',
+ inherits: ['person'],
+ fields: [
+ {
+ name: 'name',
+ type: 'integer',
+ },
+ ],
+ },
+ });
+ expect(response.statusCode).toBe(500);
+ });
+
+ it('can create relation with child table', async () => {
+ await agent.resource('collections').create({
+ values: {
+ name: 'a',
+ fields: [
+ {
+ name: 'af',
+ type: 'string',
+ },
+ {
+ name: 'bs',
+ type: 'hasMany',
+ target: 'b',
+ },
+ ],
+ },
+ });
+
+ await agent.resource('collections').create({
+ values: {
+ name: 'b',
+ inherits: ['a'],
+ fields: [
+ {
+ name: 'bf',
+ type: 'string',
+ },
+ {
+ name: 'a',
+ type: 'belongsTo',
+ target: 'a',
+ },
+ ],
+ },
+ });
+
+ const res = await agent.resource('b').create({
+ values: {
+ af: 'a1',
+ bs: [{ bf: 'b1' }, { bf: 'b2' }],
+ },
+ });
+
+ expect(res.statusCode).toEqual(200);
+
+ const a1 = await agent.resource('b').list({
+ filter: {
+ af: 'a1',
+ },
+ appends: 'bs',
+ });
+
+ expect(a1.body.data[0].bs.length).toEqual(2);
+ });
+
+ it('can drop child replaced field', async () => {
+ await agent.resource('collections').create({
+ values: {
+ name: 'students',
+ inherits: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ },
+ });
+
+ const response = await agent.resource('collections.fields', 'students').destroy({
+ filter: {
+ name: 'name',
+ },
+ });
+
+ expect(response.status).toBe(200);
+ });
+
+ it('should reload collection when parent fields change', async () => {
+ await agent.resource('collections').create({
+ values: {
+ name: 'employee',
+ inherits: 'person',
+ fields: [
+ {
+ name: 'salary',
+ type: 'integer',
+ },
+ ],
+ },
+ });
+
+ await agent.resource('collections').create({
+ values: {
+ name: 'instructor',
+ inherits: 'employee',
+ fields: [
+ {
+ name: 'rank',
+ type: 'integer',
+ },
+ ],
+ },
+ });
+
+ const createInstructorResponse = await agent.resource('instructor').create({
+ values: {
+ name: 'foo',
+ salary: 1000,
+ rank: 100,
+ },
+ });
+
+ expect(createInstructorResponse.statusCode).toBe(200);
+
+ const employeeCollection = app.db.getCollection('employee');
+ expect(employeeCollection.fields.get('new-field')).not.toBeDefined();
+
+ // add new field to root collection
+ await agent.resource('fields').create({
+ values: {
+ collectionName: 'person',
+ name: 'age',
+ type: 'integer',
+ },
+ });
+
+ expect(employeeCollection.fields.get('age')).toBeDefined();
+
+ let listResponse = await agent.resource('employee').list();
+ expect(listResponse.statusCode).toBe(200);
+
+ expect(listResponse.body.data[0].age).toBeDefined();
+
+ listResponse = await agent.resource('instructor').list();
+ expect(listResponse.statusCode).toBe(200);
+
+ expect(listResponse.body.data[0].age).toBeDefined();
+ });
+
+ it('should create inherited collection', async () => {
+ const response = await agent.resource('collections').create({
+ values: {
+ name: 'students',
+ inherits: 'person',
+ fields: [
+ {
+ name: 'score',
+ type: 'integer',
+ },
+ ],
+ },
+ });
+
+ expect(response.statusCode).toBe(200);
+
+ const studentCollection = app.db.getCollection('students');
+ expect(studentCollection).toBeDefined();
+
+ const studentFieldsResponse = await agent.resource('fields').list({
+ filter: {
+ collectionName: 'students',
+ },
+ });
+
+ expect(studentFieldsResponse.statusCode).toBe(200);
+
+ const studentFields = studentFieldsResponse.body.data;
+ expect(studentFields.length).toBe(1);
+
+ const createStudentResponse = await agent.resource('students').create({
+ values: {
+ name: 'foo',
+ score: 100,
+ },
+ });
+
+ expect(createStudentResponse.statusCode).toBe(200);
+
+ const fooStudent = createStudentResponse.body.data;
+
+ expect(fooStudent.name).toBe('foo');
+
+ const studentList = await agent.resource('students').list();
+ expect(studentList.statusCode).toBe(200);
+ });
+
+ it('should know which child table row it is', async () => {
+ await agent.resource('collections').create({
+ values: {
+ name: 'students',
+ inherits: 'person',
+ fields: [
+ {
+ name: 'score',
+ type: 'integer',
+ },
+ ],
+ },
+ });
+
+ await agent.resource('students').create({
+ values: {
+ name: 'foo',
+ score: 100,
+ },
+ });
+
+ const personList = await agent.resource('person').list();
+
+ const person = personList.body.data[0];
+
+ expect(person['__collection']).toBe('students');
+ });
+});
diff --git a/packages/plugins/collection-manager/src/__tests__/inherits/inherited-collection.test.ts b/packages/plugins/collection-manager/src/__tests__/inherits/inherited-collection.test.ts
new file mode 100644
index 0000000000..60f9543842
--- /dev/null
+++ b/packages/plugins/collection-manager/src/__tests__/inherits/inherited-collection.test.ts
@@ -0,0 +1,320 @@
+import Database, { Repository } from '@nocobase/database';
+import Application from '@nocobase/server';
+import { createApp } from '..';
+import { pgOnly } from '@nocobase/test';
+
+pgOnly()('Inherited Collection', () => {
+ let db: Database;
+ let app: Application;
+
+ let collectionRepository: Repository;
+
+ let fieldsRepository: Repository;
+
+ beforeEach(async () => {
+ app = await createApp();
+
+ db = app.db;
+
+ collectionRepository = db.getCollection('collections').repository;
+ fieldsRepository = db.getCollection('fields').repository;
+ });
+
+ afterEach(async () => {
+ await app.destroy();
+ });
+
+ it("should not delete child's field when parent field delete that inherits from multiple table", async () => {
+ await collectionRepository.create({
+ values: {
+ name: 'b',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ },
+ context: {},
+ });
+
+ await collectionRepository.create({
+ values: {
+ name: 'c',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ },
+ context: {},
+ });
+
+ await collectionRepository.create({
+ values: {
+ name: 'a',
+ inherits: ['b', 'c'],
+ },
+ context: {},
+ });
+
+ await fieldsRepository.create({
+ values: {
+ collectionName: 'a',
+ name: 'name',
+ type: 'string',
+ },
+ });
+
+ await db.getCollection('fields').repository.destroy({
+ filter: {
+ collectionName: 'b',
+ name: 'name',
+ },
+ });
+
+ expect(
+ await db.getCollection('fields').repository.findOne({
+ filter: {
+ collectionName: 'a',
+ name: 'name',
+ },
+ }),
+ ).not.toBeNull();
+
+ await db.getCollection('fields').repository.destroy({
+ filter: {
+ collectionName: 'c',
+ name: 'name',
+ },
+ });
+
+ expect(
+ await db.getCollection('fields').repository.findOne({
+ filter: {
+ collectionName: 'a',
+ name: 'name',
+ },
+ }),
+ ).toBeNull();
+ });
+
+ it("should delete child's field when parent field deleted", async () => {
+ await collectionRepository.create({
+ values: {
+ name: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ },
+ context: {},
+ });
+
+ await collectionRepository.create({
+ values: {
+ name: 'students',
+ inherits: ['person'],
+ },
+ context: {},
+ });
+
+ await db.getCollection('fields').repository.create({
+ values: {
+ collectionName: 'students',
+ name: 'name',
+ type: 'string',
+ },
+ context: {},
+ });
+
+ await db.getCollection('fields').repository.create({
+ values: {
+ collectionName: 'students',
+ name: 'age',
+ type: 'integer',
+ },
+ context: {},
+ });
+
+ const childNameField = await db.getCollection('fields').repository.findOne({
+ filter: {
+ collectionName: 'students',
+ name: 'name',
+ },
+ });
+
+ expect(childNameField.get('overriding')).toBeTruthy();
+
+ await db.getCollection('fields').repository.destroy({
+ filter: {
+ collectionName: 'person',
+ name: 'name',
+ },
+ });
+
+ expect(
+ await db.getCollection('fields').repository.findOne({
+ filter: {
+ collectionName: 'students',
+ name: 'name',
+ },
+ }),
+ ).toBeNull();
+
+ expect(
+ await db.getCollection('fields').repository.findOne({
+ filter: {
+ collectionName: 'students',
+ name: 'age',
+ },
+ }),
+ ).not.toBeNull();
+
+ await db.getCollection('fields').repository.create({
+ values: {
+ collectionName: 'person',
+ name: 'age',
+ type: 'integer',
+ },
+ context: {},
+ });
+
+ await db.getCollection('fields').repository.destroy({
+ filter: {
+ collectionName: 'person',
+ name: 'age',
+ },
+ });
+
+ expect(
+ await db.getCollection('fields').repository.findOne({
+ filter: {
+ collectionName: 'person',
+ name: 'age',
+ },
+ }),
+ ).toBeNull();
+
+ expect(
+ await db.getCollection('fields').repository.findOne({
+ filter: {
+ collectionName: 'students',
+ name: 'age',
+ },
+ }),
+ ).not.toBeNull();
+ });
+
+ it('should not inherit with difference type', async () => {
+ const personCollection = await collectionRepository.create({
+ values: {
+ name: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ },
+ context: {},
+ });
+
+ let err;
+ try {
+ const studentCollection = await collectionRepository.create({
+ values: {
+ name: 'students',
+ inherits: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'integer',
+ },
+ ],
+ },
+ context: {},
+ });
+ } catch (e) {
+ err = e;
+ }
+
+ expect(err).toBeDefined();
+ expect(err.message.includes('type conflict')).toBeTruthy();
+ });
+
+ it('should replace parent collection field', async () => {
+ const personCollection = await collectionRepository.create({
+ values: {
+ name: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ },
+ context: {},
+ });
+
+ const studentCollection = await collectionRepository.create({
+ values: {
+ name: 'students',
+ inherits: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ title: '姓名',
+ },
+ ],
+ },
+ context: {},
+ });
+
+ const studentFields = await studentCollection.getFields();
+ expect(studentFields.length).toBe(1);
+ expect(studentFields[0].get('title')).toBe('姓名');
+ });
+
+ it('should remove parent collections field', async () => {
+ await collectionRepository.create({
+ values: {
+ name: 'person',
+ fields: [
+ {
+ name: 'name',
+ type: 'string',
+ },
+ ],
+ },
+ context: {},
+ });
+
+ await collectionRepository.create({
+ values: {
+ name: 'students',
+ fields: [
+ {
+ name: 'score',
+ type: 'integer',
+ },
+ ],
+ },
+ context: {},
+ });
+
+ const studentCollection = await db.getCollection('students');
+
+ console.log(studentCollection.fields);
+ await studentCollection.repository.create({
+ values: {
+ name: 'foo',
+ score: 100,
+ },
+ });
+ });
+});
diff --git a/packages/plugins/collection-manager/src/__tests__/sync.test.ts b/packages/plugins/collection-manager/src/__tests__/sync.test.ts
new file mode 100644
index 0000000000..ecb23ea6fd
--- /dev/null
+++ b/packages/plugins/collection-manager/src/__tests__/sync.test.ts
@@ -0,0 +1,57 @@
+import Database, { Collection as DBCollection } from '@nocobase/database';
+import Application from '@nocobase/server';
+import { createApp } from '.';
+
+describe('sync collection', () => {
+ let db: Database;
+ let app: Application;
+
+ beforeEach(async () => {
+ app = await createApp();
+ db = app.db;
+ });
+
+ afterEach(async () => {
+ await app.destroy();
+ });
+
+ it('should not remove column when async with drop false', async () => {
+ const getTableInfo = async (tableName: string) => {
+ const queryInterface = db.sequelize.getQueryInterface();
+ const tableInfo = await queryInterface.describeTable(tableName);
+ return tableInfo;
+ };
+
+ const c1 = db.collection({
+ name: 'c1',
+ fields: [{ type: 'string', name: 'f1' }],
+ });
+
+ await db.sync({
+ force: false,
+ alter: {
+ drop: false,
+ },
+ });
+
+ let tableInfo1 = await getTableInfo(c1.model.tableName);
+ expect(tableInfo1.f1).toBeTruthy();
+
+ c1.setField('f2', {
+ type: 'string',
+ });
+
+ c1.removeField('f1');
+
+ await db.sync({
+ force: false,
+ alter: {
+ drop: false,
+ },
+ });
+
+ let tableInfo2 = await getTableInfo(c1.model.tableName);
+ expect(tableInfo2.f2).toBeTruthy();
+ expect(tableInfo2.f1).toBeTruthy();
+ });
+});
diff --git a/packages/plugins/collection-manager/src/models/collection.ts b/packages/plugins/collection-manager/src/models/collection.ts
index 99212ef759..e797e5e8a8 100644
--- a/packages/plugins/collection-manager/src/models/collection.ts
+++ b/packages/plugins/collection-manager/src/models/collection.ts
@@ -1,6 +1,7 @@
import Database, { Collection, MagicAttributeModel } from '@nocobase/database';
import { SyncOptions, Transactionable } from 'sequelize';
import { FieldModel } from './field';
+import lodash from 'lodash';
interface LoadOptions extends Transactionable {
// TODO
@@ -19,25 +20,27 @@ export class CollectionModel extends MagicAttributeModel {
let collection: Collection;
+ const collectionOptions = {
+ ...this.get(),
+ fields: [],
+ };
+
if (this.db.hasCollection(name)) {
collection = this.db.getCollection(name);
+
if (skipExist) {
return collection;
}
- collection.updateOptions({
- ...this.get(),
- fields: [],
- });
+
+ collection.updateOptions(collectionOptions);
} else {
- collection = this.db.collection({
- ...this.get(),
- fields: [],
- });
+ collection = this.db.collection(collectionOptions);
}
if (!skipField) {
await this.loadFields({ transaction });
}
+
return collection;
}
@@ -83,6 +86,7 @@ export class CollectionModel extends MagicAttributeModel {
const collection = await this.load({
transaction: options?.transaction,
});
+
try {
await collection.sync({
force: false,
@@ -92,8 +96,73 @@ export class CollectionModel extends MagicAttributeModel {
...options,
});
} catch (error) {
+ console.error(error);
const name = this.get('name');
this.db.removeCollection(name);
+ throw error;
+ }
+ }
+
+ isInheritedModel() {
+ return this.get('inherits');
+ }
+
+ async findParents(options: Transactionable) {
+ const { transaction } = options;
+
+ const findModelParents = async (model: CollectionModel, carry = []) => {
+ if (!model.get('inherits')) {
+ return;
+ }
+ const parents = lodash.castArray(model.get('inherits'));
+
+ for (const parent of parents) {
+ const parentModel = (await this.db.getCollection('collections').repository.findOne({
+ filterByTk: parent,
+ transaction,
+ })) as CollectionModel;
+
+ carry.push(parentModel.get('name'));
+
+ await findModelParents(parentModel, carry);
+ }
+
+ return carry;
+ };
+
+ return findModelParents(this);
+ }
+
+ async parentFields(options: Transactionable) {
+ const { transaction } = options;
+
+ return this.db.getCollection('fields').repository.find({
+ filter: {
+ collectionName: { $in: await this.findParents({ transaction }) },
+ },
+ transaction,
+ });
+ }
+
+ // sync fields from parents
+ async syncParentFields(options: Transactionable) {
+ const { transaction } = options;
+
+ const ancestorFields = await this.parentFields({ transaction });
+
+ const selfFields = await this.getFields({ transaction });
+
+ const inheritedFields = ancestorFields.filter((field: FieldModel) => {
+ return (
+ !field.isAssociationField() &&
+ !selfFields.find((selfField: FieldModel) => selfField.get('name') == field.get('name'))
+ );
+ });
+
+ for (const inheritedField of inheritedFields) {
+ await this.createField(lodash.omit(inheritedField.toJSON(), ['key', 'collectionName', 'sort']), {
+ transaction,
+ });
}
}
}
diff --git a/packages/plugins/collection-manager/src/models/field.ts b/packages/plugins/collection-manager/src/models/field.ts
index 6cd16f860e..43ec7aebb3 100644
--- a/packages/plugins/collection-manager/src/models/field.ts
+++ b/packages/plugins/collection-manager/src/models/field.ts
@@ -67,15 +67,21 @@ export class FieldModel extends MagicAttributeModel {
return (this.constructor).database;
}
+ isAssociationField() {
+ return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(this.get('type'));
+ }
+
async load(loadOptions?: LoadOptions) {
const { skipExist = false } = loadOptions || {};
const collectionName = this.get('collectionName');
+
if (!this.db.hasCollection(collectionName)) {
return;
}
const collection = this.db.getCollection(collectionName);
const name = this.get('name');
+
if (skipExist && collection.hasField(name)) {
return collection.getField(name);
}
@@ -123,6 +129,7 @@ export class FieldModel extends MagicAttributeModel {
if (!field) {
return;
}
+
return field.removeFromDb({
transaction: options.transaction,
});
diff --git a/packages/plugins/collection-manager/src/server.ts b/packages/plugins/collection-manager/src/server.ts
index 7a0fd50394..5545c6db67 100644
--- a/packages/plugins/collection-manager/src/server.ts
+++ b/packages/plugins/collection-manager/src/server.ts
@@ -17,6 +17,7 @@ import {
} from './hooks';
import { CollectionModel, FieldModel } from './models';
+import { InheritedCollection } from '@nocobase/database';
export class CollectionManagerPlugin extends Plugin {
async beforeLoad() {
@@ -56,6 +57,20 @@ export class CollectionManagerPlugin extends Plugin {
// 要在 beforeInitOptions 之前处理
this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db));
this.app.db.on('fields.beforeCreate', beforeCreateForChildrenCollection(this.app.db));
+
+ this.app.db.on('fields.beforeCreate', async (model, options) => {
+ const collectionName = model.get('collectionName');
+ const collection = this.app.db.getCollection(collectionName);
+
+ if (!collection) {
+ return;
+ }
+
+ if (collection.isInherited() && (collection).parentFields().has(model.get('name'))) {
+ model.set('overriding', true);
+ }
+ });
+
this.app.db.on('fields.beforeCreate', async (model, options) => {
const type = model.get('type');
const fn = beforeInitOptions[type];
@@ -66,14 +81,16 @@ export class CollectionManagerPlugin extends Plugin {
this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db));
- this.app.db.on('collections.afterCreateWithAssociations', async (model, { context, transaction }) => {
- if (context) {
- await model.migrate({
- isNew: true,
- transaction,
- });
- }
- });
+ this.app.db.on(
+ 'collections.afterCreateWithAssociations',
+ async (model: CollectionModel, { context, transaction }) => {
+ if (context) {
+ await model.migrate({
+ transaction,
+ });
+ }
+ },
+ );
this.app.db.on('fields.afterCreate', async (model: FieldModel, { context, transaction }) => {
if (context) {
@@ -123,7 +140,7 @@ export class CollectionManagerPlugin extends Plugin {
// before field remove
this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db));
- this.app.db.on('fields.beforeDestroy', async (model, options) => {
+ this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => {
await model.remove(options);
});
@@ -131,6 +148,37 @@ export class CollectionManagerPlugin extends Plugin {
await model.remove(options);
});
+ this.app.db.on('fields.afterDestroy', async (model: FieldModel, options) => {
+ const { transaction } = options;
+ const collectionName = model.get('collectionName');
+ const childCollections = this.db.inheritanceMap.getChildren(collectionName);
+
+ const childShouldRemoveField = Array.from(childCollections).filter((item) => {
+ const parents = Array.from(this.db.inheritanceMap.getParents(item))
+ .map((parent) => {
+ const collection = this.db.getCollection(parent);
+ const field = collection.getField(model.get('name'));
+ return field;
+ })
+ .filter(Boolean);
+
+ return parents.length == 0;
+ });
+
+ await this.db.getCollection('fields').repository.destroy({
+ filter: {
+ name: model.get('name'),
+ collectionName: {
+ $in: childShouldRemoveField,
+ },
+ options: {
+ overriding: true,
+ },
+ },
+ transaction,
+ });
+ });
+
this.app.on('afterLoad', async (app, options) => {
if (options?.method === 'install') {
return;