mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-01 19:58:15 +08:00
feat: collection inheritance (#1069)
* chore: test * chore: inherited-collection class * feat: collection inherit * feat: collection inherit * feat: inhertis sync runner * test: get parents fields * feat: collection inherit style promote * feat: sync * feat: sync alter table * feat: pgOnly Test * fix: child collection create api * feat: replace parent field * chore: reload parent fields * test: reload collection test * feat: details are displayed according to conditions * fix: typo * feat: inheritance map class * chore: is parent node * feat: display where child row created from * fix: find with appends * feat: add parent collection fields * fix: create table * feat: load fields for all children * refactor: sync fields from parent * test: has one field inhertis * feat: replace child association target * feat: should not replace child field when parent field update * test: should update inherit field when parent field update * feat: only the blocks directly inherited from the current data are displayed * fix: inherit from multiple collections * feat: only the blocks directly inherited from the current data are displayed * fix: test * feat: parent collection expend * fix: test * test: belongsToMany inherits * test: belongsToMany inherits * feat: block display * feat: collection inherite * feat: collection inherite * feat: multiple inherits * fix: sync runner * feat: collection inherite * feat: collecton inherits * feat: cannot be modified after inheritance and saving * feat: collection inherit for graph * feat: collection inherits * fix: drop inhertied field * fix: should throw error when type conflit * feat: output inherited fields * feat: bulk update collection fields * feat: collection fields * feat: collection fields * test: create relation with child table * fix: test * fix: test * fix: test * feat: style impove * test: should not replace field with difference type * feat: add text * fix: throw error when replace field with difference type * feat: overriding * feat: kan bankanban group fields * feat: calendar block fields * feat: kan bankanban group fields * fix: test * feat: relationship fields * feat: should delete child's field when parent field deleted * feat: foreign key filter * fix: build error & multiple inherit destory field * fix: test * chore: disable error * feat: no recursive update associations (#1091) * feat: update associations * fix(collection-manager): should update uiSchema * chore: flip if * feat: mutile inherits * feat: db dialect * feat: inherits show by database * chore: git hash into docker image * fix: js gzip * fix: dockerfile * chore: error message * feat: overriding * feat: overriding * feat: overriding * feat: local * feat: filter fields by interface * fix: database logging env * test: replace hasOne target * feat: add view * feat: local * chore: enable error * fix: update docs Co-authored-by: katherinehhh <katherine_15995@163.com> Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
688387413d
commit
e991b2965a
19
.github/workflows/aliyun-container-registry.yml
vendored
19
.github/workflows/aliyun-container-registry.yml
vendored
@ -22,19 +22,15 @@ jobs:
|
||||
ports:
|
||||
- 4873:4873
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
-
|
||||
name: Docker meta
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
@ -45,20 +41,19 @@ jobs:
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ secrets.ALI_DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.ALI_DOCKER_USERNAME }}
|
||||
password: ${{ secrets.ALI_DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Build and push
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
build-args: |
|
||||
VERDACCIO_URL=http://localhost:4873/
|
||||
COMMIT_HASH=${GITHUB_SHA}
|
||||
push: true
|
||||
tags: ${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}
|
||||
|
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -1,7 +1,4 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
FROM node:16 as builder
|
||||
ARG VERDACCIO_URL=http://host.docker.internal:10104/
|
||||
ARG COMIT_HASH
|
||||
|
||||
RUN apt-get update && apt-get install -y jq
|
||||
WORKDIR /tmp
|
||||
@ -43,6 +44,8 @@ COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
|
||||
|
||||
WORKDIR /app/nocobase
|
||||
|
||||
RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMIT_HASH" >> /app/nocobase/storage/uploads/COMIT_HASH
|
||||
|
||||
COPY ./docker/nocobase/docker-entrypoint.sh /app/
|
||||
|
||||
CMD ["/app/docker-entrypoint.sh"]
|
||||
|
@ -9,7 +9,7 @@ server {
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/javascript application/xml;
|
||||
|
||||
location /storage/uploads/ {
|
||||
alias /app/nocobase/storage/uploads/;
|
||||
|
74
docs/zh-CN/welcome/release/inherits.md
Normal file
74
docs/zh-CN/welcome/release/inherits.md
Normal file
@ -0,0 +1,74 @@
|
||||
# v0.8.1: 数据表继承
|
||||
|
||||
数据表继承基于 [PostgreSQL 的 INHERITS 语法](https://www.postgresql.org/docs/current/tutorial-inheritance.html) 实现,仅限于 PostgreSQL 数据库安装的 NocoBase 时才会提供。
|
||||
|
||||
## 示例
|
||||
|
||||
我们从一个例子开始,假设要做一个教学系统,有三类用户:学生、家长和老师。
|
||||
|
||||
如果没有继承,要分别为三类用户建表:
|
||||
|
||||
- 学生:姓名、年龄、性别、身份证
|
||||
- 家长:姓名、年龄、性别、职业、学历
|
||||
- 老师:姓名、年龄、性别、教龄、已婚
|
||||
|
||||
有了数据表继承之后,共同的信息就可以提炼出来:
|
||||
|
||||
- 用户:姓名、年龄、性别
|
||||
- 学生:身份证
|
||||
- 家长:职业、学历
|
||||
- 老师:教龄、已婚
|
||||
|
||||
ER 图如下:
|
||||
|
||||
<img src="./inherits/er.svg" style="max-width: 700px;" />
|
||||
|
||||
注:子表 ID 和父表 ID 共享序列
|
||||
|
||||
## 配置数据表继承
|
||||
|
||||
Inherits 字段选择需要继承的数据表
|
||||
|
||||
<img src="./inherits/inherit.jpg" />
|
||||
|
||||
通过代码配置如下:
|
||||
|
||||
```ts
|
||||
db.collection({
|
||||
name: 'users',
|
||||
});
|
||||
|
||||
db.collection({
|
||||
name: 'students',
|
||||
inherits: 'users',
|
||||
});
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 继承的表并不能随意选择,主键必须是唯一序列,比如 uuid 或者所有继承线路上的表的 id 自增序列都用同一个
|
||||
- Inherits 参数不能被编辑
|
||||
- 如果有继承关系,被继承的父表不能被删除
|
||||
|
||||
## 数据表字段列表
|
||||
|
||||
字段列表里同步显示继承的父表字段,父表字段不可以修改,但可以重写(Override)
|
||||
|
||||
<img src="./inherits/inherit-fields.jpg" />
|
||||
|
||||
重写父表字段的注意事项:
|
||||
- 子表字段标识与父表字段一样时为重写
|
||||
- 重写字段的类型必须保持一致
|
||||
- 关系字段除了 target collection 以外的其他参数需要保持一致
|
||||
|
||||
## 父表的子表区块
|
||||
|
||||
在父表区块里可以配置子表的区块
|
||||
|
||||
<img src="./inherits/inherited-blocks.jpg" />
|
||||
|
||||
## 新增继承的父表字段的配置
|
||||
|
||||
当有继承的父表时,配置字段时,会提供从父表继承的字段的配置
|
||||
|
||||
<img src="./inherits/configure-fields.jpg" />
|
BIN
docs/zh-CN/welcome/release/inherits/configure-fields.jpg
Normal file
BIN
docs/zh-CN/welcome/release/inherits/configure-fields.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
4
docs/zh-CN/welcome/release/inherits/er.svg
Normal file
4
docs/zh-CN/welcome/release/inherits/er.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 25 KiB |
BIN
docs/zh-CN/welcome/release/inherits/form.jpg
Normal file
BIN
docs/zh-CN/welcome/release/inherits/form.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 246 KiB |
BIN
docs/zh-CN/welcome/release/inherits/inherit-fields.jpg
Normal file
BIN
docs/zh-CN/welcome/release/inherits/inherit-fields.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 313 KiB |
BIN
docs/zh-CN/welcome/release/inherits/inherit.jpg
Normal file
BIN
docs/zh-CN/welcome/release/inherits/inherit.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 231 KiB |
BIN
docs/zh-CN/welcome/release/inherits/inherited-blocks.jpg
Normal file
BIN
docs/zh-CN/welcome/release/inherits/inherited-blocks.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 142 KiB |
@ -1,7 +1,7 @@
|
||||
import { IDatabaseOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
|
||||
logging: process.env.DB_LOGGING == 'on' ? customLogger : false,
|
||||
dialect: process.env.DB_DIALECT as any,
|
||||
storage: process.env.DB_STORAGE,
|
||||
username: process.env.DB_USER,
|
||||
@ -12,3 +12,8 @@ export default {
|
||||
timezone: process.env.DB_TIMEZONE,
|
||||
tablePrefix: process.env.DB_TABLE_PREFIX,
|
||||
} as IDatabaseOptions;
|
||||
|
||||
function customLogger(queryString, queryObject) {
|
||||
console.log(queryString); // outputs a string
|
||||
console.log(queryObject.bind); // outputs an array
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { useField } from '@formily/react';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { Spin } from 'antd';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { RecordProvider } from '../record-provider';
|
||||
import { RecordProvider, useRecord } from '../record-provider';
|
||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||
import { useDesignable } from '../schema-component';
|
||||
import { useCollectionManager } from '../collection-manager';
|
||||
|
||||
export const FormBlockContext = createContext<any>({});
|
||||
|
||||
@ -46,10 +48,19 @@ const InternalFormBlockProvider = (props) => {
|
||||
};
|
||||
|
||||
export const FormBlockProvider = (props) => {
|
||||
const record = useRecord();
|
||||
const { __tableName } = record;
|
||||
const { getParentCollections } = useCollectionManager();
|
||||
const inheritCollections = getParentCollections(__tableName);
|
||||
const { designable } = useDesignable();
|
||||
const flag =
|
||||
!designable && __tableName && !inheritCollections.includes(props.collection) && __tableName !== props.collection;
|
||||
return (
|
||||
!flag && (
|
||||
<BlockProvider {...props}>
|
||||
<InternalFormBlockProvider {...props} />
|
||||
</BlockProvider>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,17 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PluginManager } from '../plugin-manager';
|
||||
import { ActionContext, SchemaComponent } from '../schema-component';
|
||||
import { AddCollectionField, AddFieldAction, ConfigurationTable, EditFieldAction,EditCollectionField } from './Configuration';
|
||||
import {
|
||||
AddCollectionField,
|
||||
AddFieldAction,
|
||||
ConfigurationTable,
|
||||
EditFieldAction,
|
||||
EditCollectionField,
|
||||
OverridingFieldAction,
|
||||
OverridingCollectionField,
|
||||
ViewCollectionField,
|
||||
ViewFieldAction,
|
||||
} from './Configuration';
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
@ -37,7 +47,20 @@ const schema2: ISchema = {
|
||||
export const CollectionManagerPane = () => {
|
||||
return (
|
||||
<Card bordered={false}>
|
||||
<SchemaComponent schema={schema2} components={{ ConfigurationTable, AddFieldAction, AddCollectionField, EditFieldAction,EditCollectionField }} />
|
||||
<SchemaComponent
|
||||
schema={schema2}
|
||||
components={{
|
||||
ConfigurationTable,
|
||||
AddFieldAction,
|
||||
AddCollectionField,
|
||||
EditFieldAction,
|
||||
EditCollectionField,
|
||||
OverridingCollectionField,
|
||||
OverridingFieldAction,
|
||||
ViewCollectionField,
|
||||
ViewFieldAction,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@ -68,7 +91,16 @@ export const CollectionManagerShortcut2 = () => {
|
||||
setVisible(true);
|
||||
}}
|
||||
/>
|
||||
<SchemaComponent schema={schema} components={{ ConfigurationTable, AddFieldAction, EditFieldAction }} />
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
components={{
|
||||
ConfigurationTable,
|
||||
AddFieldAction,
|
||||
EditFieldAction,
|
||||
OverridingFieldAction,
|
||||
ViewFieldAction,
|
||||
}}
|
||||
/>
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -3,52 +3,24 @@ import { ArrayField, Field } from '@formily/core';
|
||||
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { Table, TableColumnProps } from 'antd';
|
||||
import { default as classNames } from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RecordIndexProvider, RecordProvider, useCollectionManager, useRequest, useSchemaInitializer } from '../..';
|
||||
import { findIndex } from 'lodash';
|
||||
import {
|
||||
RecordIndexProvider,
|
||||
RecordProvider,
|
||||
useCollectionManager,
|
||||
useRequest,
|
||||
useSchemaInitializer,
|
||||
useRecord,
|
||||
useCompile,
|
||||
} from '../..';
|
||||
import { overridingSchema } from '../Configuration/schemas/collectionFields';
|
||||
|
||||
const isColumnComponent = (schema: Schema) => {
|
||||
return schema['x-component']?.endsWith('.Column') > -1;
|
||||
};
|
||||
|
||||
const useTableColumns = () => {
|
||||
const field = useField<ArrayField>();
|
||||
const schema = useFieldSchema();
|
||||
const { exists, render } = useSchemaInitializer(schema['x-initializer']);
|
||||
const columns = schema
|
||||
.reduceProperties((buf, s) => {
|
||||
if (isColumnComponent(s)) {
|
||||
return buf.concat([s]);
|
||||
}
|
||||
}, [])
|
||||
.map((s: Schema) => {
|
||||
return {
|
||||
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
|
||||
dataIndex: s.name,
|
||||
key: s.name,
|
||||
render: (v, record) => {
|
||||
const index = field.value?.indexOf(record);
|
||||
// console.log((Date.now() - start) / 1000);
|
||||
return (
|
||||
<RecordIndexProvider index={index}>
|
||||
<RecordProvider record={record}>
|
||||
<RecursionField schema={s} name={index} onlyRenderProperties />
|
||||
</RecordProvider>
|
||||
</RecordIndexProvider>
|
||||
);
|
||||
},
|
||||
} as TableColumnProps<any>;
|
||||
});
|
||||
if (!exists) {
|
||||
return columns;
|
||||
}
|
||||
return columns.concat({
|
||||
title: render(),
|
||||
dataIndex: 'TABLE_COLUMN_INITIALIZER',
|
||||
key: 'TABLE_COLUMN_INITIALIZER',
|
||||
});
|
||||
};
|
||||
|
||||
export const components = {
|
||||
body: {
|
||||
row: (props) => {
|
||||
@ -93,7 +65,6 @@ const groupColumns = [
|
||||
];
|
||||
|
||||
type CategorizeKey = 'primaryAndForeignKey' | 'relation' | 'systemInfo' | 'basic';
|
||||
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
|
||||
const CategorizeKeyNameMap = new Map<CategorizeKey, string>([
|
||||
['primaryAndForeignKey', 'PK & FK fields'],
|
||||
['relation', 'Association fields'],
|
||||
@ -108,10 +79,13 @@ interface CategorizeDataItem {
|
||||
}
|
||||
|
||||
export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
||||
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
|
||||
const field = useField<ArrayField>();
|
||||
const columns = useTableColumns();
|
||||
const { name } = useRecord();
|
||||
const { t } = useTranslation();
|
||||
const { getInterface } = useCollectionManager();
|
||||
const compile = useCompile();
|
||||
const { getInterface, getParentCollections, getCollection, getCurrentCollectionFields, getInheritedFields } =
|
||||
useCollectionManager();
|
||||
const {
|
||||
showIndex = true,
|
||||
useSelectedRowKeys = useDef,
|
||||
@ -120,12 +94,14 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
||||
...others
|
||||
} = props;
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
|
||||
|
||||
const [categorizeData, setCategorizeData] = useState<Array<CategorizeDataItem>>([]);
|
||||
const [expandedKeys, setExpendedKeys] = useState(selectedRowKeys);
|
||||
const inherits = getParentCollections(name);
|
||||
const currentFields = getCurrentCollectionFields(name);
|
||||
useDataSource({
|
||||
onSuccess(data) {
|
||||
field.value = data?.data || [];
|
||||
// categorize field
|
||||
const tmpData: Array<CategorizeDataItem> = [];
|
||||
const categorizeMap = new Map<CategorizeKey, any>();
|
||||
const addCategorizeVal = (categorizeKey: CategorizeKey, val) => {
|
||||
let fieldArr = categorizeMap.get(categorizeKey);
|
||||
@ -151,21 +127,103 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
||||
addCategorizeVal('basic', item);
|
||||
}
|
||||
});
|
||||
const tmpData: Array<CategorizeDataItem> = [];
|
||||
if (inherits) {
|
||||
inherits.forEach((v) => {
|
||||
sortKeyArr.push(v);
|
||||
const parentCollection = getCollection(v);
|
||||
parentCollection.fields.map((k) => {
|
||||
if (k.interface) {
|
||||
addCategorizeVal(v, new Proxy(k, {}));
|
||||
field.value.push(new Proxy(k, {}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
sortKeyArr.forEach((key) => {
|
||||
if (categorizeMap.get(key)?.length > 0) {
|
||||
const parentCollection = getCollection(key);
|
||||
tmpData.push({
|
||||
key,
|
||||
name: t(CategorizeKeyNameMap.get(key)),
|
||||
name:
|
||||
t(CategorizeKeyNameMap.get(key)) ||
|
||||
t(`Parent collection fields`) + `(${compile(parentCollection.title)})`,
|
||||
data: categorizeMap.get(key),
|
||||
});
|
||||
}
|
||||
});
|
||||
setExpendedKeys(sortKeyArr);
|
||||
setCategorizeData(tmpData);
|
||||
},
|
||||
});
|
||||
const useTableColumns = () => {
|
||||
const schema = useFieldSchema();
|
||||
const { exists, render } = useSchemaInitializer(schema['x-initializer']);
|
||||
const columns = schema
|
||||
.reduceProperties((buf, s) => {
|
||||
if (isColumnComponent(s)) {
|
||||
return buf.concat([s]);
|
||||
}
|
||||
}, [])
|
||||
.map((s: Schema) => {
|
||||
return {
|
||||
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
|
||||
dataIndex: s.name,
|
||||
key: s.name,
|
||||
render: (v, record) => {
|
||||
const index = findIndex(field.value, record);
|
||||
return (
|
||||
<RecordIndexProvider index={index}>
|
||||
<RecordProvider record={record}>
|
||||
<RecursionField schema={s} name={index} onlyRenderProperties />
|
||||
</RecordProvider>
|
||||
</RecordIndexProvider>
|
||||
);
|
||||
},
|
||||
} as TableColumnProps<any>;
|
||||
});
|
||||
if (!exists) {
|
||||
return columns;
|
||||
}
|
||||
return columns.concat({
|
||||
title: render(),
|
||||
dataIndex: 'TABLE_COLUMN_INITIALIZER',
|
||||
key: 'TABLE_COLUMN_INITIALIZER',
|
||||
});
|
||||
};
|
||||
|
||||
const getIsOverriding = (record) => {
|
||||
const flag = currentFields.find((v) => {
|
||||
return v.name === record.name;
|
||||
});
|
||||
return !flag;
|
||||
};
|
||||
|
||||
const expandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
|
||||
const columns = useTableColumns();
|
||||
if (inherits.includes(record.key)) {
|
||||
columns.pop();
|
||||
columns.push({
|
||||
title: <RecursionField name={'column4'} schema={overridingSchema as Schema} onlyRenderSelf />,
|
||||
dataIndex: 'column4',
|
||||
key: 'column4',
|
||||
render: (v, record) => {
|
||||
const index = findIndex(field.value, record);
|
||||
const flag = getIsOverriding(record);
|
||||
//@ts-ignore
|
||||
overridingSchema.properties.actions.properties.overriding['x-visible'] = flag;
|
||||
return (
|
||||
<RecordIndexProvider index={index}>
|
||||
<RecordProvider record={record}>
|
||||
<RecursionField schema={overridingSchema as Schema} name={index} onlyRenderProperties />
|
||||
</RecordProvider>
|
||||
</RecordIndexProvider>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
const restProps = {
|
||||
rowSelection: props.rowSelection
|
||||
rowSelection:
|
||||
props.rowSelection && !inherits.includes(record.key)
|
||||
? {
|
||||
type: 'checkbox',
|
||||
selectedRowKeys,
|
||||
@ -176,12 +234,6 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const defaultRowKey = (record: any) => {
|
||||
return field.value?.indexOf?.(record);
|
||||
};
|
||||
|
||||
const expandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
|
||||
return (
|
||||
<Table
|
||||
{...others}
|
||||
@ -211,7 +263,18 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
||||
pagination={false}
|
||||
expandable={{
|
||||
expandedRowRender,
|
||||
defaultExpandedRowKeys: sortKeyArr,
|
||||
expandedRowKeys: expandedKeys,
|
||||
}}
|
||||
onExpand={(expanded, record) => {
|
||||
let keys = [];
|
||||
if (expanded) {
|
||||
keys = expandedKeys.concat([record.key]);
|
||||
} else {
|
||||
keys = expandedKeys.filter((v) => {
|
||||
return v !== record.key;
|
||||
});
|
||||
}
|
||||
setExpendedKeys(keys);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { useForm } from '@formily/react';
|
||||
import { action } from '@formily/reactive';
|
||||
import { uid } from '@formily/shared';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { CollectionFieldsTable } from '.';
|
||||
import { useRequest } from '../../api-client';
|
||||
import { useCurrentDatabase } from '../../database';
|
||||
import { useRecord } from '../../record-provider';
|
||||
import { SchemaComponent, SchemaComponentContext, useActionContext, useCompile } from '../../schema-component';
|
||||
import { useCancelAction, useUpdateCollectionActionAndRefreshCM } from '../action-hooks';
|
||||
import { useCollectionManager } from '../hooks/useCollectionManager';
|
||||
import { DataSourceContext } from '../sub-table';
|
||||
import { AddSubFieldAction } from './AddSubFieldAction';
|
||||
import { FieldSummary } from './components/FieldSummary';
|
||||
import { EditSubFieldAction } from './EditSubFieldAction';
|
||||
import { collectionSchema } from './schemas/collections';
|
||||
import { CollectionFieldsTable } from ".";
|
||||
|
||||
const useAsyncDataSource = (service: any) => (field: any) => {
|
||||
field.loading = true;
|
||||
@ -173,9 +175,14 @@ const useNewId = (prefix) => {
|
||||
|
||||
export const ConfigurationTable = () => {
|
||||
const { collections = [] } = useCollectionManager();
|
||||
const {
|
||||
data: { database },
|
||||
} = useCurrentDatabase();
|
||||
const collectonsRef: any = useRef();
|
||||
collectonsRef.current = collections;
|
||||
const compile = useCompile();
|
||||
const loadCollections = async (field: any) => {
|
||||
return collections
|
||||
return collectonsRef.current
|
||||
?.filter((item) => !(item.autoCreate && item.isThrough))
|
||||
.map((item: any) => ({
|
||||
label: compile(item.title),
|
||||
@ -187,14 +194,15 @@ export const ConfigurationTable = () => {
|
||||
<div>
|
||||
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
|
||||
<SchemaComponent
|
||||
schema={collectionSchema}
|
||||
schema={collectionSchema(database?.dialect)}
|
||||
components={{
|
||||
AddSubFieldAction,
|
||||
EditSubFieldAction,
|
||||
FieldSummary,
|
||||
CollectionFieldsTable
|
||||
CollectionFieldsTable,
|
||||
}}
|
||||
scope={{
|
||||
enableInherits: database?.dialect === 'postgres',
|
||||
useDestroySubField,
|
||||
useBulkDestroySubField,
|
||||
useSelectedRowKeys,
|
||||
@ -203,6 +211,8 @@ export const ConfigurationTable = () => {
|
||||
loadCollections,
|
||||
useCurrentFields,
|
||||
useNewId,
|
||||
useCancelAction,
|
||||
useUpdateCollectionActionAndRefreshCM,
|
||||
}}
|
||||
/>
|
||||
</SchemaComponentContext.Provider>
|
||||
|
@ -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 <OverridingFieldAction item={record} {...props} />;
|
||||
};
|
||||
|
||||
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<any>({});
|
||||
|
||||
return (
|
||||
<RecordProvider record={record}>
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<a
|
||||
onClick={async () => {
|
||||
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')}
|
||||
</a>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
components={{ ...components, ArrayTable }}
|
||||
scope={{
|
||||
getContainer,
|
||||
useOverridingCollectionField,
|
||||
useCancelAction,
|
||||
showReverseFieldConfig: !data?.reverseField,
|
||||
createOnly: true,
|
||||
...scope,
|
||||
}}
|
||||
/>
|
||||
</ActionContext.Provider>
|
||||
</RecordProvider>
|
||||
);
|
||||
};
|
@ -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 <ViewFieldAction item={record} {...props} />;
|
||||
};
|
||||
|
||||
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<any>({});
|
||||
|
||||
return (
|
||||
<RecordProvider record={record}>
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<a
|
||||
onClick={async () => {
|
||||
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')}
|
||||
</a>
|
||||
<SchemaComponent
|
||||
schema={schema}
|
||||
components={{ ...components, ArrayTable }}
|
||||
scope={{
|
||||
getContainer,
|
||||
useVieInheritedCollectionField,
|
||||
useCancelAction,
|
||||
showReverseFieldConfig: !data?.reverseField,
|
||||
createOnly: !true,
|
||||
...scope,
|
||||
}}
|
||||
/>
|
||||
</ActionContext.Provider>
|
||||
</RecordProvider>
|
||||
);
|
||||
};
|
@ -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_-]*$/,
|
||||
|
@ -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',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,8 @@ const compile = (source) => {
|
||||
return Schema.compile(source, { t: i18n.t });
|
||||
};
|
||||
|
||||
export const collection: CollectionOptions = {
|
||||
export const getCollectionOptions = (database): CollectionOptions => {
|
||||
return {
|
||||
name: 'collections',
|
||||
filterTargetKey: 'name',
|
||||
targetKey: 'name',
|
||||
@ -44,10 +45,103 @@ export const collection: CollectionOptions = {
|
||||
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 = {
|
||||
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 }}',
|
||||
},
|
||||
},
|
||||
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: {
|
||||
@ -55,7 +149,7 @@ export const collectionSchema: ISchema = {
|
||||
'x-collection': 'collections',
|
||||
'x-decorator': 'ResourceActionProvider',
|
||||
'x-decorator-props': {
|
||||
collection,
|
||||
collection: getCollectionOptions(database),
|
||||
request: {
|
||||
resource: 'collections',
|
||||
action: 'list',
|
||||
@ -125,38 +219,7 @@ export const collectionSchema: ISchema = {
|
||||
'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 }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: createCollectionProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -218,6 +281,9 @@ export const collectionSchema: ISchema = {
|
||||
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}`);
|
||||
@ -247,38 +313,7 @@ export const collectionSchema: ISchema = {
|
||||
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 }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: editCollectionProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -304,3 +339,4 @@ export const collectionSchema: ISchema = {
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
};
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ export interface CollectionOptions {
|
||||
targetKey?: string;
|
||||
sortable?: any;
|
||||
fields?: FieldOptions[];
|
||||
inherits?:string[];
|
||||
}
|
||||
|
||||
export interface ICollectionProviderProps {
|
||||
|
@ -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 <Spin />;
|
||||
}
|
||||
return (
|
||||
<CurrentDatabaseContext.Provider
|
||||
value={result.data}
|
||||
>
|
||||
{props.children}
|
||||
</CurrentDatabaseContext.Provider>
|
||||
);
|
||||
};
|
1
packages/core/client/src/database/index.ts
Normal file
1
packages/core/client/src/database/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './CurrentDatabaseProvider';
|
@ -25,4 +25,5 @@ export * from './schema-templates';
|
||||
export * from './settings-form';
|
||||
export * from './system-settings';
|
||||
export * from './user';
|
||||
export * from './database'
|
||||
|
||||
|
@ -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",
|
||||
|
@ -114,6 +114,8 @@ export default {
|
||||
"Configure fields": "フィールドの設定",
|
||||
"Configure columns": "カラムの設定",
|
||||
"Edit field": "フィールドの編集",
|
||||
"Override":"書き換え",
|
||||
"Override field":"フィールドの上書き",
|
||||
"Configure fields of {{title}}": "{{title}}のフィールド設定",
|
||||
"Basic": "基本タイプ",
|
||||
"Single line text": "一行テキスト",
|
||||
|
@ -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": "多行文本",
|
||||
|
@ -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,6 +207,7 @@ const InternalAdminLayout = (props: any) => {
|
||||
|
||||
export const AdminLayout = (props) => {
|
||||
return (
|
||||
<CurrentDatabaseProvider>
|
||||
<CurrentUserProvider>
|
||||
<RemoteSchemaTemplateManagerProvider>
|
||||
<RemoteCollectionManagerProvider>
|
||||
@ -215,6 +217,7 @@ export const AdminLayout = (props) => {
|
||||
</RemoteCollectionManagerProvider>
|
||||
</RemoteSchemaTemplateManagerProvider>
|
||||
</CurrentUserProvider>
|
||||
</CurrentDatabaseProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 = <Menu>{renderItems(items)}</Menu>;
|
||||
const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
|
||||
|
||||
if (!designable && props.designable !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className={classNames('nb-schema-initializer-button')}
|
||||
|
@ -1,23 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializer } from '../SchemaInitializer';
|
||||
import { gridRowColWrap, useCustomFormItemInitializerFields } from '../utils';
|
||||
import { gridRowColWrap, useCustomFormItemInitializerFields,useInheritsFormItemInitializerFields } from '../utils';
|
||||
import { useCompile } from '../../schema-component';
|
||||
|
||||
// 表单里配置字段
|
||||
export const CustomFormItemInitializers = (props: any) => {
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const { insertPosition, component } = props;
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
wrap={gridRowColWrap}
|
||||
icon={'SettingOutlined'}
|
||||
items={[
|
||||
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 (
|
||||
<SchemaInitializer.Button
|
||||
wrap={gridRowColWrap}
|
||||
icon={'SettingOutlined'}
|
||||
items={fieldItems}
|
||||
insertPosition={insertPosition}
|
||||
component={component}
|
||||
title={component ? null : t('Configure fields')}
|
||||
|
@ -2,28 +2,45 @@ 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 FormItemInitializers = (props: any) => {
|
||||
const { t } = useTranslation();
|
||||
const { insertPosition, component } = props;
|
||||
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
|
||||
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
wrap={gridRowColWrap}
|
||||
icon={'SettingOutlined'}
|
||||
items={union<any>(
|
||||
[
|
||||
const inheritFields = useInheritsFormItemInitializerFields();
|
||||
const compile = useCompile();
|
||||
const fieldItems: any[] = [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Display fields'),
|
||||
children: useFormItemInitializerFields(),
|
||||
},
|
||||
],
|
||||
associationFields.length > 0
|
||||
? [
|
||||
];
|
||||
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',
|
||||
},
|
||||
@ -32,9 +49,9 @@ export const FormItemInitializers = (props: any) => {
|
||||
title: t('Display association fields'),
|
||||
children: associationFields,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
[
|
||||
);
|
||||
|
||||
fieldItems.push(
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
@ -53,8 +70,12 @@ export const FormItemInitializers = (props: any) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
)}
|
||||
);
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
wrap={gridRowColWrap}
|
||||
icon={'SettingOutlined'}
|
||||
items={fieldItems}
|
||||
insertPosition={insertPosition}
|
||||
component={component}
|
||||
title={component ? null : t('Configure fields')}
|
||||
|
@ -2,27 +2,38 @@ 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' });
|
||||
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
wrap={gridRowColWrap}
|
||||
icon={'SettingOutlined'}
|
||||
items={union<any>(
|
||||
[
|
||||
const inheritFields = useInheritsFormItemInitializerFields();
|
||||
const compile = useCompile();
|
||||
const fieldItems: any[] = [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Display fields'),
|
||||
children: useFormItemInitializerFields(),
|
||||
},
|
||||
],
|
||||
associationFields.length > 0
|
||||
? [
|
||||
];
|
||||
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',
|
||||
},
|
||||
@ -31,9 +42,9 @@ export const ReadPrettyFormItemInitializers = (props: any) => {
|
||||
title: t('Display association fields'),
|
||||
children: associationFields,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
[
|
||||
]);
|
||||
|
||||
fieldItems.push(
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
@ -52,8 +63,12 @@ export const ReadPrettyFormItemInitializers = (props: any) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
)}
|
||||
);
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
wrap={gridRowColWrap}
|
||||
icon={'SettingOutlined'}
|
||||
items={fieldItems}
|
||||
insertPosition={insertPosition}
|
||||
component={component}
|
||||
title={component ? null : t('Configure fields')}
|
||||
|
@ -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")}}',
|
||||
|
@ -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[] = [{
|
||||
const inheritFields = useInheritsTableColumnInitializerFields();
|
||||
const compile = useCompile();
|
||||
const fieldItems: any[] = [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Display fields'),
|
||||
children: useTableColumnInitializerFields(),
|
||||
}];
|
||||
if (associatedFields?.length > 0) {
|
||||
fieldItems.push({
|
||||
},
|
||||
];
|
||||
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],
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
if (associatedFields?.length > 0) {
|
||||
fieldItems.push(
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Display association fields'),
|
||||
children: associatedFields,
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
fieldItems.push({
|
||||
fieldItems.push(
|
||||
{
|
||||
type: 'divider',
|
||||
}, {
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: t('Action column'),
|
||||
component: 'TableActionColumnInitializer',
|
||||
})
|
||||
},
|
||||
);
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
insertPosition={'beforeEnd'}
|
||||
@ -49,10 +80,7 @@ export const TableColumnInitializers = (props: any) => {
|
||||
},
|
||||
};
|
||||
}}
|
||||
items={itemsMerge(
|
||||
fieldItems,
|
||||
items,
|
||||
)}
|
||||
items={itemsMerge(fieldItems, items)}
|
||||
>
|
||||
{t('Configure columns')}
|
||||
</SchemaInitializer.Button>
|
||||
|
@ -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 (
|
||||
<DataBlockInitializer
|
||||
@ -20,8 +20,8 @@ export const CalendarBlockInitializer = (props) => {
|
||||
componentType={'Calendar'}
|
||||
icon={<FormOutlined />}
|
||||
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 {
|
||||
|
@ -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={<FormOutlined />}
|
||||
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: {
|
||||
|
@ -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 (
|
||||
<SchemaInitializer.Item
|
||||
icon={<FormOutlined />}
|
||||
icon={icon && <FormOutlined />}
|
||||
{...others}
|
||||
key={'123'}
|
||||
onClick={async ({ item }) => {
|
||||
|
@ -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,
|
||||
|
@ -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,14 +8,11 @@ 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 (
|
||||
<Menu.Item key="version" disabled>
|
||||
Version {data?.data?.version}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
3
packages/core/database/src/__tests__/inhertits/helper.ts
Normal file
3
packages/core/database/src/__tests__/inhertits/helper.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const pgOnly = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
|
||||
|
||||
export default pgOnly;
|
@ -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);
|
||||
});
|
||||
});
|
@ -22,6 +22,7 @@ export type CollectionSortable = string | boolean | { name?: string; scopeKey?:
|
||||
export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> {
|
||||
name: string;
|
||||
tableName?: string;
|
||||
inherits?: string[] | string;
|
||||
filterTargetKey?: string;
|
||||
fields?: FieldOptions[];
|
||||
model?: string | ModelCtor<Model>;
|
||||
@ -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 ((<any>association).through) {
|
||||
modelNames.add((<any>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);
|
||||
}
|
||||
}
|
||||
|
@ -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<string, Collection>();
|
||||
pendingFields = new Map<string, RelationField[]>();
|
||||
modelCollection = new Map<ModelCtor<any>, Collection>();
|
||||
tableNameCollectionMap = new Map<string, Collection>();
|
||||
|
||||
referenceMap = new ReferencesMap();
|
||||
inheritanceMap = new InheritanceMap();
|
||||
|
||||
modelHook: ModelHook;
|
||||
version: DatabaseVersion;
|
||||
@ -293,7 +298,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
): Collection<Attributes, CreateAttributes> {
|
||||
this.emit('beforeDefineCollection', options);
|
||||
|
||||
const collection = new Collection(options, {
|
||||
const collection = options.inherits
|
||||
? new InheritedCollection(options, {
|
||||
database: this,
|
||||
})
|
||||
: new Collection(options, {
|
||||
database: this,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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() && (<InheritedCollection>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');
|
||||
|
@ -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';
|
||||
|
||||
|
73
packages/core/database/src/inherited-collection.ts
Normal file
73
packages/core/database/src/inherited-collection.ts
Normal file
@ -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<string, Field>();
|
||||
for (const parent of this.parents) {
|
||||
if (parent.isInherited()) {
|
||||
for (const [name, field] of (<InheritedCollection>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, (<InheritedCollection>parent).parentAttributes());
|
||||
}
|
||||
|
||||
const parentAttributes = (<any>parent.model).tableAttributes;
|
||||
|
||||
Object.assign(attributes, parentAttributes);
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
isInherited() {
|
||||
return true;
|
||||
}
|
||||
}
|
81
packages/core/database/src/inherited-map.ts
Normal file
81
packages/core/database/src/inherited-map.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import lodash from 'lodash';
|
||||
|
||||
class TableNode {
|
||||
name: string;
|
||||
parents: Set<TableNode>;
|
||||
children: Set<TableNode>;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.parents = new Set();
|
||||
this.children = new Set();
|
||||
}
|
||||
}
|
||||
|
||||
export default class InheritanceMap {
|
||||
nodes: Map<string, TableNode> = new Map<string, TableNode>();
|
||||
|
||||
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<string> {
|
||||
const results = new Set<string>();
|
||||
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<string> {
|
||||
const results = new Set<string>();
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<TModelAttributes extends {} = any, TCreationAttributes extend
|
||||
|
||||
return lodash.orderBy(data, orderItems, orderDirections);
|
||||
}
|
||||
|
||||
static async sync(options) {
|
||||
const model = this as any;
|
||||
|
||||
if (this.collection.isInherited()) {
|
||||
return SyncRunner.syncInheritModel(model, options);
|
||||
}
|
||||
|
||||
return SequelizeModel.sync.call(this, options);
|
||||
}
|
||||
}
|
||||
|
@ -104,6 +104,13 @@ export class OptionsParser {
|
||||
return filterParams;
|
||||
}
|
||||
|
||||
protected inheritFromSubQuery(): any {
|
||||
return [
|
||||
Sequelize.literal(`(select relname from pg_class where pg_class.oid = "${this.collection.name}".tableoid)`),
|
||||
'__tableName',
|
||||
];
|
||||
}
|
||||
|
||||
protected parseFields(filterParams: any) {
|
||||
const appends = this.options?.appends || [];
|
||||
const except = [];
|
||||
@ -113,6 +120,10 @@ export class OptionsParser {
|
||||
exclude: [],
|
||||
}; // out put all fields by default
|
||||
|
||||
if (this.collection.isParent()) {
|
||||
attributes.include.push(this.inheritFromSubQuery());
|
||||
}
|
||||
|
||||
if (this.options?.fields) {
|
||||
// 将fields拆分为 attributes 和 appends
|
||||
for (const field of this.options.fields) {
|
||||
|
@ -232,6 +232,8 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
...this.buildQueryOptions(options),
|
||||
};
|
||||
|
||||
let rows;
|
||||
|
||||
if (opts.include && opts.include.length > 0) {
|
||||
// @ts-ignore
|
||||
const primaryKeyField = model.primaryKeyField || model.primaryKeyAttribute;
|
||||
@ -258,29 +260,40 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
},
|
||||
};
|
||||
|
||||
return await handleAppendsQuery({
|
||||
rows = await handleAppendsQuery({
|
||||
queryPromises: opts.include.map((include) => {
|
||||
return model
|
||||
.findAll({
|
||||
const options = {
|
||||
...omit(opts, ['limit', 'offset']),
|
||||
include: include,
|
||||
where,
|
||||
transaction,
|
||||
})
|
||||
.then((rows) => {
|
||||
};
|
||||
|
||||
return model.findAll(options).then((rows) => {
|
||||
return { rows, include };
|
||||
});
|
||||
}),
|
||||
templateModel: ids[0].row,
|
||||
});
|
||||
}
|
||||
|
||||
return await model.findAll({
|
||||
} else {
|
||||
rows = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* find and count
|
||||
* @param options
|
||||
|
117
packages/core/database/src/sync-runner.ts
Normal file
117
packages/core/database/src/sync-runner.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -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 };
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -8,9 +8,7 @@ describe('collections repository', () => {
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
agent = app.agent();
|
||||
await agent
|
||||
.resource('collections')
|
||||
.create({
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'tags',
|
||||
fields: [
|
||||
@ -21,9 +19,7 @@ describe('collections repository', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('collections')
|
||||
.create({
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'foos',
|
||||
fields: [
|
||||
@ -34,18 +30,14 @@ describe('collections repository', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('collections.fields', 'tags')
|
||||
.create({
|
||||
await agent.resource('collections.fields', 'tags').create({
|
||||
values: {
|
||||
name: 'foos',
|
||||
target: 'foos',
|
||||
type: 'belongsToMany',
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('collections')
|
||||
.create({
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'comments',
|
||||
fields: [
|
||||
@ -56,9 +48,7 @@ describe('collections repository', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('collections')
|
||||
.create({
|
||||
await agent.resource('collections').create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
fields: [
|
||||
@ -95,23 +85,17 @@ 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({
|
||||
await agent.resource('posts.comments', postId).create({
|
||||
values: {
|
||||
title: 'comment 1',
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('posts.comments', postId)
|
||||
.create({
|
||||
await agent.resource('posts.comments', postId).create({
|
||||
values: {
|
||||
title: 'comment 2',
|
||||
},
|
||||
});
|
||||
const response2 = await agent
|
||||
.resource('posts')
|
||||
.list({
|
||||
const response2 = await agent.resource('posts').list({
|
||||
filter: {
|
||||
'comments.title': 'comment 1',
|
||||
},
|
||||
@ -122,16 +106,12 @@ describe('collections repository', () => {
|
||||
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({
|
||||
await agent.resource('posts.comments', postId).create({
|
||||
values: {
|
||||
title: 'comment 1',
|
||||
},
|
||||
});
|
||||
const response2 = await agent
|
||||
.resource('posts')
|
||||
.list({
|
||||
const response2 = await agent.resource('posts').list({
|
||||
filter: {
|
||||
'comments.id': 3,
|
||||
},
|
||||
@ -142,16 +122,12 @@ describe('collections repository', () => {
|
||||
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({
|
||||
await agent.resource('posts.comments', postId).create({
|
||||
values: {
|
||||
title: 'comment 1',
|
||||
},
|
||||
});
|
||||
const response2 = await agent
|
||||
.resource('posts')
|
||||
.list({
|
||||
const response2 = await agent.resource('posts').list({
|
||||
filter: {
|
||||
$and: [
|
||||
{
|
||||
@ -174,9 +150,7 @@ describe('collections repository', () => {
|
||||
});
|
||||
|
||||
it('case 6', async () => {
|
||||
const response = await agent
|
||||
.resource('posts')
|
||||
.create({
|
||||
const response = await agent.resource('posts').create({
|
||||
values: {
|
||||
tags: [
|
||||
{},
|
||||
@ -192,9 +166,7 @@ describe('collections repository', () => {
|
||||
});
|
||||
|
||||
it('case 7', async () => {
|
||||
const response = await agent
|
||||
.resource('posts')
|
||||
.create({
|
||||
const response = await agent.resource('posts').create({
|
||||
values: {
|
||||
tags: [
|
||||
{},
|
||||
@ -205,9 +177,7 @@ describe('collections repository', () => {
|
||||
},
|
||||
});
|
||||
const postId = response.body.data.id;
|
||||
const response1 = await agent
|
||||
.resource('posts.tags', postId)
|
||||
.list({
|
||||
const response1 = await agent.resource('posts.tags', postId).list({
|
||||
filter: {
|
||||
title: 'Tag1',
|
||||
},
|
||||
@ -216,9 +186,7 @@ describe('collections repository', () => {
|
||||
});
|
||||
|
||||
it('case 8', async () => {
|
||||
const response = await agent
|
||||
.resource('posts')
|
||||
.create({
|
||||
const response = await agent.resource('posts').create({
|
||||
values: {
|
||||
tags: [
|
||||
{},
|
||||
@ -232,9 +200,7 @@ describe('collections repository', () => {
|
||||
},
|
||||
});
|
||||
const postId = response.body.data.id;
|
||||
const response1 = await agent
|
||||
.resource('posts.tags', postId)
|
||||
.list({
|
||||
const response1 = await agent.resource('posts.tags', postId).list({
|
||||
filter: {
|
||||
$or: [{ title: 'Tag1' }, { title: 'Tag2' }],
|
||||
},
|
||||
@ -243,9 +209,7 @@ describe('collections repository', () => {
|
||||
});
|
||||
|
||||
it('case 9', async () => {
|
||||
const response = await agent
|
||||
.resource('posts')
|
||||
.create({
|
||||
const response = await agent.resource('posts').create({
|
||||
values: {
|
||||
tags: [
|
||||
{},
|
||||
@ -259,9 +223,7 @@ describe('collections repository', () => {
|
||||
},
|
||||
});
|
||||
const postId = response.body.data.id;
|
||||
const response1 = await agent
|
||||
.resource('posts.tags', postId)
|
||||
.list({
|
||||
const response1 = await agent.resource('posts.tags', postId).list({
|
||||
filter: {
|
||||
$or: [{ title: 'Tag1' }, { title: 'Tag2' }],
|
||||
},
|
||||
@ -270,9 +232,7 @@ describe('collections repository', () => {
|
||||
});
|
||||
|
||||
it('case 10', async () => {
|
||||
const response = await agent
|
||||
.resource('posts')
|
||||
.create({
|
||||
const response = await agent.resource('posts').create({
|
||||
values: {
|
||||
tags: [
|
||||
{},
|
||||
@ -286,9 +246,7 @@ describe('collections repository', () => {
|
||||
},
|
||||
});
|
||||
const postId = response.body.data.id;
|
||||
const response1 = await agent
|
||||
.resource('posts.tags', postId)
|
||||
.list({
|
||||
const response1 = await agent.resource('posts.tags', postId).list({
|
||||
appends: ['foos'],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
@ -301,39 +259,29 @@ 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({
|
||||
await agent.resource('posts.comments', postId).create({
|
||||
values: {
|
||||
title: 'comment 1',
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('posts.comments', postId)
|
||||
.create({
|
||||
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({
|
||||
await agent.resource('posts.comments', postId2).create({
|
||||
values: {
|
||||
title: 'comment 2',
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('posts.comments', postId2)
|
||||
.create({
|
||||
await agent.resource('posts.comments', postId2).create({
|
||||
values: {
|
||||
title: 'comment 2',
|
||||
},
|
||||
});
|
||||
const response3 = await agent
|
||||
.resource('posts')
|
||||
.list({
|
||||
const response3 = await agent.resource('posts').list({
|
||||
filter: {
|
||||
$or: [
|
||||
{
|
||||
@ -351,30 +299,22 @@ describe('collections repository', () => {
|
||||
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({
|
||||
await agent.resource('posts.comments', postId).create({
|
||||
values: {
|
||||
title: 'comment 1',
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('posts.comments', postId)
|
||||
.create({
|
||||
await agent.resource('posts.comments', postId).create({
|
||||
values: {
|
||||
title: 'comment 2',
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('posts.comments', postId)
|
||||
.create({
|
||||
await agent.resource('posts.comments', postId).create({
|
||||
values: {
|
||||
title: 'comment 3',
|
||||
},
|
||||
});
|
||||
const response2 = await agent
|
||||
.resource('posts.comments', postId)
|
||||
.list({
|
||||
const response2 = await agent.resource('posts.comments', postId).list({
|
||||
filter: {
|
||||
$or: [
|
||||
{
|
||||
@ -394,31 +334,23 @@ 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({
|
||||
await agent.resource('posts').create({
|
||||
values: {
|
||||
tags: [tag1.get('id'), tag3.get('id')],
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('posts')
|
||||
.create({
|
||||
await agent.resource('posts').create({
|
||||
values: {
|
||||
tags: [tag2.get('id')],
|
||||
},
|
||||
});
|
||||
await agent
|
||||
.resource('posts')
|
||||
.create({
|
||||
await agent.resource('posts').create({
|
||||
values: {
|
||||
tags: [tag2.get('id'), tag3.get('id')],
|
||||
},
|
||||
});
|
||||
|
||||
const response1 = await agent
|
||||
.resource('posts')
|
||||
.list({
|
||||
const response1 = await agent.resource('posts').list({
|
||||
filter: {
|
||||
$or: [{ 'tags.title': 'tag1' }, { 'tags.title': 'tag3' }],
|
||||
},
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,15 +67,21 @@ export class FieldModel extends MagicAttributeModel {
|
||||
return (<any>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,
|
||||
});
|
||||
|
@ -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() && (<InheritedCollection>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 }) => {
|
||||
this.app.db.on(
|
||||
'collections.afterCreateWithAssociations',
|
||||
async (model: CollectionModel, { context, transaction }) => {
|
||||
if (context) {
|
||||
await model.migrate({
|
||||
isNew: true,
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user