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:
|
ports:
|
||||||
- 4873:4873
|
- 4873:4873
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
-
|
- name: Set up QEMU
|
||||||
name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
-
|
- name: Set up Docker Buildx
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
with:
|
||||||
driver-opts: network=host
|
driver-opts: network=host
|
||||||
-
|
- name: Docker meta
|
||||||
name: Docker meta
|
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
@ -45,20 +41,19 @@ jobs:
|
|||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
-
|
- name: Login to Docker Hub
|
||||||
name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ${{ secrets.ALI_DOCKER_REGISTRY }}
|
registry: ${{ secrets.ALI_DOCKER_REGISTRY }}
|
||||||
username: ${{ secrets.ALI_DOCKER_USERNAME }}
|
username: ${{ secrets.ALI_DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.ALI_DOCKER_PASSWORD }}
|
password: ${{ secrets.ALI_DOCKER_PASSWORD }}
|
||||||
-
|
- name: Build and push
|
||||||
name: Build and push
|
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
VERDACCIO_URL=http://localhost:4873/
|
VERDACCIO_URL=http://localhost:4873/
|
||||||
|
COMMIT_HASH=${GITHUB_SHA}
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}
|
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",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
FROM node:16 as builder
|
FROM node:16 as builder
|
||||||
ARG VERDACCIO_URL=http://host.docker.internal:10104/
|
ARG VERDACCIO_URL=http://host.docker.internal:10104/
|
||||||
|
ARG COMIT_HASH
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y jq
|
RUN apt-get update && apt-get install -y jq
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
@ -43,6 +44,8 @@ COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
|
|||||||
|
|
||||||
WORKDIR /app/nocobase
|
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/
|
COPY ./docker/nocobase/docker-entrypoint.sh /app/
|
||||||
|
|
||||||
CMD ["/app/docker-entrypoint.sh"]
|
CMD ["/app/docker-entrypoint.sh"]
|
||||||
|
@ -9,7 +9,7 @@ server {
|
|||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_proxied expired no-cache no-store private auth;
|
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/ {
|
location /storage/uploads/ {
|
||||||
alias /app/nocobase/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';
|
import { IDatabaseOptions } from '@nocobase/database';
|
||||||
|
|
||||||
export default {
|
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,
|
dialect: process.env.DB_DIALECT as any,
|
||||||
storage: process.env.DB_STORAGE,
|
storage: process.env.DB_STORAGE,
|
||||||
username: process.env.DB_USER,
|
username: process.env.DB_USER,
|
||||||
@ -12,3 +12,8 @@ export default {
|
|||||||
timezone: process.env.DB_TIMEZONE,
|
timezone: process.env.DB_TIMEZONE,
|
||||||
tablePrefix: process.env.DB_TABLE_PREFIX,
|
tablePrefix: process.env.DB_TABLE_PREFIX,
|
||||||
} as IDatabaseOptions;
|
} 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 { createForm } from '@formily/core';
|
||||||
import { useField } from '@formily/react';
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
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 { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
import { useDesignable } from '../schema-component';
|
||||||
|
import { useCollectionManager } from '../collection-manager';
|
||||||
|
|
||||||
export const FormBlockContext = createContext<any>({});
|
export const FormBlockContext = createContext<any>({});
|
||||||
|
|
||||||
@ -46,10 +48,19 @@ const InternalFormBlockProvider = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const FormBlockProvider = (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 (
|
return (
|
||||||
|
!flag && (
|
||||||
<BlockProvider {...props}>
|
<BlockProvider {...props}>
|
||||||
<InternalFormBlockProvider {...props} />
|
<InternalFormBlockProvider {...props} />
|
||||||
</BlockProvider>
|
</BlockProvider>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,7 +7,17 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { PluginManager } from '../plugin-manager';
|
import { PluginManager } from '../plugin-manager';
|
||||||
import { ActionContext, SchemaComponent } from '../schema-component';
|
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 = {
|
const schema: ISchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -37,7 +47,20 @@ const schema2: ISchema = {
|
|||||||
export const CollectionManagerPane = () => {
|
export const CollectionManagerPane = () => {
|
||||||
return (
|
return (
|
||||||
<Card bordered={false}>
|
<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>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -68,7 +91,16 @@ export const CollectionManagerShortcut2 = () => {
|
|||||||
setVisible(true);
|
setVisible(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SchemaComponent schema={schema} components={{ ConfigurationTable, AddFieldAction, EditFieldAction }} />
|
<SchemaComponent
|
||||||
|
schema={schema}
|
||||||
|
components={{
|
||||||
|
ConfigurationTable,
|
||||||
|
AddFieldAction,
|
||||||
|
EditFieldAction,
|
||||||
|
OverridingFieldAction,
|
||||||
|
ViewFieldAction,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ActionContext.Provider>
|
</ActionContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,52 +3,24 @@ import { ArrayField, Field } from '@formily/core';
|
|||||||
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Table, TableColumnProps } from 'antd';
|
import { Table, TableColumnProps } from 'antd';
|
||||||
import { default as classNames } from 'classnames';
|
import { default as classNames } from 'classnames';
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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) => {
|
const isColumnComponent = (schema: Schema) => {
|
||||||
return schema['x-component']?.endsWith('.Column') > -1;
|
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 = {
|
export const components = {
|
||||||
body: {
|
body: {
|
||||||
row: (props) => {
|
row: (props) => {
|
||||||
@ -93,7 +65,6 @@ const groupColumns = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type CategorizeKey = 'primaryAndForeignKey' | 'relation' | 'systemInfo' | 'basic';
|
type CategorizeKey = 'primaryAndForeignKey' | 'relation' | 'systemInfo' | 'basic';
|
||||||
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
|
|
||||||
const CategorizeKeyNameMap = new Map<CategorizeKey, string>([
|
const CategorizeKeyNameMap = new Map<CategorizeKey, string>([
|
||||||
['primaryAndForeignKey', 'PK & FK fields'],
|
['primaryAndForeignKey', 'PK & FK fields'],
|
||||||
['relation', 'Association fields'],
|
['relation', 'Association fields'],
|
||||||
@ -108,10 +79,13 @@ interface CategorizeDataItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
||||||
|
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
|
||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
const columns = useTableColumns();
|
const { name } = useRecord();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getInterface } = useCollectionManager();
|
const compile = useCompile();
|
||||||
|
const { getInterface, getParentCollections, getCollection, getCurrentCollectionFields, getInheritedFields } =
|
||||||
|
useCollectionManager();
|
||||||
const {
|
const {
|
||||||
showIndex = true,
|
showIndex = true,
|
||||||
useSelectedRowKeys = useDef,
|
useSelectedRowKeys = useDef,
|
||||||
@ -120,12 +94,14 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
|||||||
...others
|
...others
|
||||||
} = props;
|
} = props;
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
|
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
|
||||||
|
|
||||||
const [categorizeData, setCategorizeData] = useState<Array<CategorizeDataItem>>([]);
|
const [categorizeData, setCategorizeData] = useState<Array<CategorizeDataItem>>([]);
|
||||||
|
const [expandedKeys, setExpendedKeys] = useState(selectedRowKeys);
|
||||||
|
const inherits = getParentCollections(name);
|
||||||
|
const currentFields = getCurrentCollectionFields(name);
|
||||||
useDataSource({
|
useDataSource({
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
field.value = data?.data || [];
|
field.value = data?.data || [];
|
||||||
// categorize field
|
const tmpData: Array<CategorizeDataItem> = [];
|
||||||
const categorizeMap = new Map<CategorizeKey, any>();
|
const categorizeMap = new Map<CategorizeKey, any>();
|
||||||
const addCategorizeVal = (categorizeKey: CategorizeKey, val) => {
|
const addCategorizeVal = (categorizeKey: CategorizeKey, val) => {
|
||||||
let fieldArr = categorizeMap.get(categorizeKey);
|
let fieldArr = categorizeMap.get(categorizeKey);
|
||||||
@ -151,21 +127,103 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
|||||||
addCategorizeVal('basic', item);
|
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) => {
|
sortKeyArr.forEach((key) => {
|
||||||
if (categorizeMap.get(key)?.length > 0) {
|
if (categorizeMap.get(key)?.length > 0) {
|
||||||
|
const parentCollection = getCollection(key);
|
||||||
tmpData.push({
|
tmpData.push({
|
||||||
key,
|
key,
|
||||||
name: t(CategorizeKeyNameMap.get(key)),
|
name:
|
||||||
|
t(CategorizeKeyNameMap.get(key)) ||
|
||||||
|
t(`Parent collection fields`) + `(${compile(parentCollection.title)})`,
|
||||||
data: categorizeMap.get(key),
|
data: categorizeMap.get(key),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
setExpendedKeys(sortKeyArr);
|
||||||
setCategorizeData(tmpData);
|
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 = {
|
const restProps = {
|
||||||
rowSelection: props.rowSelection
|
rowSelection:
|
||||||
|
props.rowSelection && !inherits.includes(record.key)
|
||||||
? {
|
? {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
selectedRowKeys,
|
selectedRowKeys,
|
||||||
@ -176,12 +234,6 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRowKey = (record: any) => {
|
|
||||||
return field.value?.indexOf?.(record);
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
{...others}
|
{...others}
|
||||||
@ -211,7 +263,18 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
expandable={{
|
expandable={{
|
||||||
expandedRowRender,
|
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>
|
</div>
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { action } from '@formily/reactive';
|
import { action } from '@formily/reactive';
|
||||||
import { uid } from '@formily/shared';
|
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 { useRequest } from '../../api-client';
|
||||||
|
import { useCurrentDatabase } from '../../database';
|
||||||
import { useRecord } from '../../record-provider';
|
import { useRecord } from '../../record-provider';
|
||||||
import { SchemaComponent, SchemaComponentContext, useActionContext, useCompile } from '../../schema-component';
|
import { SchemaComponent, SchemaComponentContext, useActionContext, useCompile } from '../../schema-component';
|
||||||
|
import { useCancelAction, useUpdateCollectionActionAndRefreshCM } from '../action-hooks';
|
||||||
import { useCollectionManager } from '../hooks/useCollectionManager';
|
import { useCollectionManager } from '../hooks/useCollectionManager';
|
||||||
import { DataSourceContext } from '../sub-table';
|
import { DataSourceContext } from '../sub-table';
|
||||||
import { AddSubFieldAction } from './AddSubFieldAction';
|
import { AddSubFieldAction } from './AddSubFieldAction';
|
||||||
import { FieldSummary } from './components/FieldSummary';
|
import { FieldSummary } from './components/FieldSummary';
|
||||||
import { EditSubFieldAction } from './EditSubFieldAction';
|
import { EditSubFieldAction } from './EditSubFieldAction';
|
||||||
import { collectionSchema } from './schemas/collections';
|
import { collectionSchema } from './schemas/collections';
|
||||||
import { CollectionFieldsTable } from ".";
|
|
||||||
|
|
||||||
const useAsyncDataSource = (service: any) => (field: any) => {
|
const useAsyncDataSource = (service: any) => (field: any) => {
|
||||||
field.loading = true;
|
field.loading = true;
|
||||||
@ -173,9 +175,14 @@ const useNewId = (prefix) => {
|
|||||||
|
|
||||||
export const ConfigurationTable = () => {
|
export const ConfigurationTable = () => {
|
||||||
const { collections = [] } = useCollectionManager();
|
const { collections = [] } = useCollectionManager();
|
||||||
|
const {
|
||||||
|
data: { database },
|
||||||
|
} = useCurrentDatabase();
|
||||||
|
const collectonsRef: any = useRef();
|
||||||
|
collectonsRef.current = collections;
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const loadCollections = async (field: any) => {
|
const loadCollections = async (field: any) => {
|
||||||
return collections
|
return collectonsRef.current
|
||||||
?.filter((item) => !(item.autoCreate && item.isThrough))
|
?.filter((item) => !(item.autoCreate && item.isThrough))
|
||||||
.map((item: any) => ({
|
.map((item: any) => ({
|
||||||
label: compile(item.title),
|
label: compile(item.title),
|
||||||
@ -187,14 +194,15 @@ export const ConfigurationTable = () => {
|
|||||||
<div>
|
<div>
|
||||||
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
|
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
schema={collectionSchema}
|
schema={collectionSchema(database?.dialect)}
|
||||||
components={{
|
components={{
|
||||||
AddSubFieldAction,
|
AddSubFieldAction,
|
||||||
EditSubFieldAction,
|
EditSubFieldAction,
|
||||||
FieldSummary,
|
FieldSummary,
|
||||||
CollectionFieldsTable
|
CollectionFieldsTable,
|
||||||
}}
|
}}
|
||||||
scope={{
|
scope={{
|
||||||
|
enableInherits: database?.dialect === 'postgres',
|
||||||
useDestroySubField,
|
useDestroySubField,
|
||||||
useBulkDestroySubField,
|
useBulkDestroySubField,
|
||||||
useSelectedRowKeys,
|
useSelectedRowKeys,
|
||||||
@ -203,6 +211,8 @@ export const ConfigurationTable = () => {
|
|||||||
loadCollections,
|
loadCollections,
|
||||||
useCurrentFields,
|
useCurrentFields,
|
||||||
useNewId,
|
useNewId,
|
||||||
|
useCancelAction,
|
||||||
|
useUpdateCollectionActionAndRefreshCM,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SchemaComponentContext.Provider>
|
</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 './interfaces';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './CollectionFieldsTable';
|
export * from './CollectionFieldsTable';
|
||||||
|
export * from './schemas/collections'
|
||||||
|
export * from './OverridingCollectionField'
|
||||||
|
export * from './ViewInheritedField'
|
||||||
|
|
||||||
registerValidateFormats({
|
registerValidateFormats({
|
||||||
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,
|
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: {
|
properties: {
|
||||||
summary: {
|
summary: {
|
||||||
type: 'void',
|
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 });
|
return Schema.compile(source, { t: i18n.t });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const collection: CollectionOptions = {
|
export const getCollectionOptions = (database): CollectionOptions => {
|
||||||
|
return {
|
||||||
name: 'collections',
|
name: 'collections',
|
||||||
filterTargetKey: 'name',
|
filterTargetKey: 'name',
|
||||||
targetKey: 'name',
|
targetKey: 'name',
|
||||||
@ -44,10 +45,103 @@ export const collection: CollectionOptions = {
|
|||||||
targetKey: 'name',
|
targetKey: 'name',
|
||||||
uiSchema: {},
|
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',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
block1: {
|
block1: {
|
||||||
@ -55,7 +149,7 @@ export const collectionSchema: ISchema = {
|
|||||||
'x-collection': 'collections',
|
'x-collection': 'collections',
|
||||||
'x-decorator': 'ResourceActionProvider',
|
'x-decorator': 'ResourceActionProvider',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
collection,
|
collection: getCollectionOptions(database),
|
||||||
request: {
|
request: {
|
||||||
resource: 'collections',
|
resource: 'collections',
|
||||||
action: 'list',
|
action: 'list',
|
||||||
@ -125,38 +219,7 @@ export const collectionSchema: ISchema = {
|
|||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
useValues: '{{ useCollectionValues }}',
|
useValues: '{{ useCollectionValues }}',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: createCollectionProperties,
|
||||||
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 }}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -218,6 +281,9 @@ export const collectionSchema: ISchema = {
|
|||||||
drawer: {
|
drawer: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'Action.Drawer',
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-component-props': {
|
||||||
|
destroyOnClose: true,
|
||||||
|
},
|
||||||
'x-reactions': (field) => {
|
'x-reactions': (field) => {
|
||||||
const i = field.path.segments[1];
|
const i = field.path.segments[1];
|
||||||
const table = field.form.getValuesIn(`table.${i}`);
|
const table = field.form.getValuesIn(`table.${i}`);
|
||||||
@ -247,38 +313,7 @@ export const collectionSchema: ISchema = {
|
|||||||
useValues: '{{ cm.useValuesFromRecord }}',
|
useValues: '{{ cm.useValuesFromRecord }}',
|
||||||
},
|
},
|
||||||
title: '{{ t("Edit collection") }}',
|
title: '{{ t("Edit collection") }}',
|
||||||
properties: {
|
properties: editCollectionProperties,
|
||||||
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 }}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -304,3 +339,4 @@ export const collectionSchema: ISchema = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
@ -1,18 +1,38 @@
|
|||||||
import { SchemaKey } from '@formily/react';
|
import { SchemaKey } from '@formily/react';
|
||||||
|
import { reduce ,unionBy} from 'lodash';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useAPIClient } from '../../api-client';
|
import { useAPIClient } from '../../api-client';
|
||||||
import { CollectionContext } from '../context';
|
import { CollectionContext } from '../context';
|
||||||
import { CollectionFieldOptions } from '../types';
|
import { CollectionFieldOptions } from '../types';
|
||||||
|
import { useCollectionManager } from './useCollectionManager';
|
||||||
|
|
||||||
export const useCollection = () => {
|
export const useCollection = () => {
|
||||||
const collection = useContext(CollectionContext);
|
const collection = useContext(CollectionContext);
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const resource = api?.resource(collection?.name);
|
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 {
|
return {
|
||||||
...collection,
|
...collection,
|
||||||
resource,
|
resource,
|
||||||
getField(name: SchemaKey): CollectionFieldOptions {
|
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 { clone } from '@formily/shared';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { reduce, unionBy,uniq } from 'lodash';
|
||||||
import { CollectionManagerContext } from '../context';
|
import { CollectionManagerContext } from '../context';
|
||||||
|
import { CollectionFieldOptions } from '../types';
|
||||||
|
|
||||||
export const useCollectionManager = () => {
|
export const useCollectionManager = () => {
|
||||||
const { refreshCM, service, interfaces, collections } = useContext(CollectionManagerContext);
|
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 getCollectionField = (name: string) => {
|
||||||
const [collectionName, fieldName] = name.split('.');
|
const [collectionName, fieldName] = name.split('.');
|
||||||
if (!fieldName) {
|
if (!fieldName) {
|
||||||
@ -13,27 +36,68 @@ export const useCollectionManager = () => {
|
|||||||
if (!collection) {
|
if (!collection) {
|
||||||
return;
|
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 {
|
return {
|
||||||
service,
|
service,
|
||||||
interfaces,
|
interfaces,
|
||||||
collections,
|
collections,
|
||||||
|
getParentCollections,
|
||||||
|
getChildrenCollections,
|
||||||
refreshCM: () => refreshCM?.(),
|
refreshCM: () => refreshCM?.(),
|
||||||
get(name: string) {
|
get(name: string) {
|
||||||
return collections?.find((collection) => collection.name === name);
|
return collections?.find((collection) => collection.name === name);
|
||||||
},
|
},
|
||||||
|
getInheritedFields,
|
||||||
|
getCollectionField,
|
||||||
|
getCollectionFields,
|
||||||
|
getCurrentCollectionFields,
|
||||||
getCollection(name: any) {
|
getCollection(name: any) {
|
||||||
if (typeof name !== 'string') {
|
if (typeof name !== 'string') {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
return collections?.find((collection) => collection.name === 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) {
|
getCollectionJoinField(name: string) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return;
|
return;
|
||||||
@ -58,5 +122,23 @@ export const useCollectionManager = () => {
|
|||||||
getInterface(name: string) {
|
getInterface(name: string) {
|
||||||
return interfaces[name] ? clone(interfaces[name]) : null;
|
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;
|
targetKey?: string;
|
||||||
sortable?: any;
|
sortable?: any;
|
||||||
fields?: FieldOptions[];
|
fields?: FieldOptions[];
|
||||||
|
inherits?:string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICollectionProviderProps {
|
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 './settings-form';
|
||||||
export * from './system-settings';
|
export * from './system-settings';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
export * from './database'
|
||||||
|
|
||||||
|
@ -113,6 +113,7 @@ export default {
|
|||||||
"Create collection": "Create collection",
|
"Create collection": "Create collection",
|
||||||
"Collection display name": "Collection display name",
|
"Collection display name": "Collection display name",
|
||||||
"Collection name": "Collection 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.",
|
"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",
|
"Storage type": "Storage type",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
@ -120,9 +121,12 @@ export default {
|
|||||||
"Configure fields": "Configure fields",
|
"Configure fields": "Configure fields",
|
||||||
"Configure columns": "Configure columns",
|
"Configure columns": "Configure columns",
|
||||||
"Edit field": "Edit field",
|
"Edit field": "Edit field",
|
||||||
|
"Override":"Override",
|
||||||
|
"Override field":"Override field",
|
||||||
"Configure fields of {{title}}": "Configure fields of {{title}}",
|
"Configure fields of {{title}}": "Configure fields of {{title}}",
|
||||||
"PK & FK fields": "PK & FK fields",
|
"PK & FK fields": "PK & FK fields",
|
||||||
"Association fields": "Association fields",
|
"Association fields": "Association fields",
|
||||||
|
"Parent collection fields":"Parent collection fields",
|
||||||
"System fields": "System fields",
|
"System fields": "System fields",
|
||||||
"General fields": "General fields",
|
"General fields": "General fields",
|
||||||
"Basic": "Basic",
|
"Basic": "Basic",
|
||||||
|
@ -114,6 +114,8 @@ export default {
|
|||||||
"Configure fields": "フィールドの設定",
|
"Configure fields": "フィールドの設定",
|
||||||
"Configure columns": "カラムの設定",
|
"Configure columns": "カラムの設定",
|
||||||
"Edit field": "フィールドの編集",
|
"Edit field": "フィールドの編集",
|
||||||
|
"Override":"書き換え",
|
||||||
|
"Override field":"フィールドの上書き",
|
||||||
"Configure fields of {{title}}": "{{title}}のフィールド設定",
|
"Configure fields of {{title}}": "{{title}}のフィールド設定",
|
||||||
"Basic": "基本タイプ",
|
"Basic": "基本タイプ",
|
||||||
"Single line text": "一行テキスト",
|
"Single line text": "一行テキスト",
|
||||||
|
@ -117,6 +117,7 @@ export default {
|
|||||||
"Create collection": "创建数据表",
|
"Create collection": "创建数据表",
|
||||||
"Collection display name": "数据表名称",
|
"Collection display name": "数据表名称",
|
||||||
"Collection name": "数据表标识",
|
"Collection name": "数据表标识",
|
||||||
|
"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": "编辑",
|
||||||
@ -124,11 +125,14 @@ export default {
|
|||||||
"Configure fields": "配置字段",
|
"Configure fields": "配置字段",
|
||||||
"Configure columns": "配置字段",
|
"Configure columns": "配置字段",
|
||||||
"Edit field": "编辑字段",
|
"Edit field": "编辑字段",
|
||||||
|
"Override":"重写",
|
||||||
|
"Override field":"重写字段",
|
||||||
"Configure fields of {{title}}": "「{{title}}」的字段配置",
|
"Configure fields of {{title}}": "「{{title}}」的字段配置",
|
||||||
"PK & FK fields": "主外键字段",
|
"PK & FK fields": "主外键字段",
|
||||||
"Association fields": "关系字段",
|
"Association fields": "关系字段",
|
||||||
"System fields": "系统字段",
|
"System fields": "系统字段",
|
||||||
"General fields": "普通字段",
|
"General fields": "普通字段",
|
||||||
|
"Parent collection fields":"父表字段",
|
||||||
"Basic": "基本类型",
|
"Basic": "基本类型",
|
||||||
"Single line text": "单行文本",
|
"Single line text": "单行文本",
|
||||||
"Long text": "多行文本",
|
"Long text": "多行文本",
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
ACLRolesCheckProvider,
|
ACLRolesCheckProvider,
|
||||||
CurrentUser,
|
CurrentUser,
|
||||||
CurrentUserProvider,
|
CurrentUserProvider,
|
||||||
|
CurrentDatabaseProvider,
|
||||||
findByUid,
|
findByUid,
|
||||||
findMenuItem,
|
findMenuItem,
|
||||||
RemoteCollectionManagerProvider,
|
RemoteCollectionManagerProvider,
|
||||||
@ -17,7 +18,7 @@ import {
|
|||||||
useDocumentTitle,
|
useDocumentTitle,
|
||||||
useRequest,
|
useRequest,
|
||||||
useRoute,
|
useRoute,
|
||||||
useSystemSettings
|
useSystemSettings,
|
||||||
} from '../../../';
|
} from '../../../';
|
||||||
import { useCollectionManager } from '../../../collection-manager';
|
import { useCollectionManager } from '../../../collection-manager';
|
||||||
import { PoweredBy } from '../../../powered-by';
|
import { PoweredBy } from '../../../powered-by';
|
||||||
@ -206,6 +207,7 @@ const InternalAdminLayout = (props: any) => {
|
|||||||
|
|
||||||
export const AdminLayout = (props) => {
|
export const AdminLayout = (props) => {
|
||||||
return (
|
return (
|
||||||
|
<CurrentDatabaseProvider>
|
||||||
<CurrentUserProvider>
|
<CurrentUserProvider>
|
||||||
<RemoteSchemaTemplateManagerProvider>
|
<RemoteSchemaTemplateManagerProvider>
|
||||||
<RemoteCollectionManagerProvider>
|
<RemoteCollectionManagerProvider>
|
||||||
@ -215,6 +217,7 @@ export const AdminLayout = (props) => {
|
|||||||
</RemoteCollectionManagerProvider>
|
</RemoteCollectionManagerProvider>
|
||||||
</RemoteSchemaTemplateManagerProvider>
|
</RemoteSchemaTemplateManagerProvider>
|
||||||
</CurrentUserProvider>
|
</CurrentUserProvider>
|
||||||
|
</CurrentDatabaseProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ const useLabelFields = (collectionName?: any) => {
|
|||||||
const { getCollectionFields } = useCollectionManager();
|
const { getCollectionFields } = useCollectionManager();
|
||||||
const targetFields = getCollectionFields(collectionName);
|
const targetFields = getCollectionFields(collectionName);
|
||||||
return targetFields
|
return targetFields
|
||||||
?.filter?.((field) => field?.interface && !field?.target && field.type !== 'boolean')
|
?.filter?.((field) => field?.interface && !field?.target && field.type !== 'boolean'&&!field.isForeignKey)
|
||||||
?.map?.((field) => {
|
?.map?.((field) => {
|
||||||
return {
|
return {
|
||||||
value: field.name,
|
value: field.name,
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
SchemaInitializerButtonProps,
|
SchemaInitializerButtonProps,
|
||||||
SchemaInitializerItemComponent,
|
SchemaInitializerItemComponent,
|
||||||
SchemaInitializerItemOptions,
|
SchemaInitializerItemOptions,
|
||||||
SchemaInitializerItemProps
|
SchemaInitializerItemProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const defaultWrap = (s: ISchema) => s;
|
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) {
|
if (!designable && props.designable !== true) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className={classNames('nb-schema-initializer-button')}
|
className={classNames('nb-schema-initializer-button')}
|
||||||
|
@ -1,23 +1,41 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializer } from '../SchemaInitializer';
|
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) => {
|
export const CustomFormItemInitializers = (props: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const compile = useCompile();
|
||||||
const { insertPosition, component } = props;
|
const { insertPosition, component } = props;
|
||||||
return (
|
const inheritFields = useInheritsFormItemInitializerFields();
|
||||||
<SchemaInitializer.Button
|
const fieldItems:any[]=[
|
||||||
wrap={gridRowColWrap}
|
|
||||||
icon={'SettingOutlined'}
|
|
||||||
items={[
|
|
||||||
{
|
{
|
||||||
type: 'itemGroup',
|
type: 'itemGroup',
|
||||||
title: t('Configure fields'),
|
title: t('Configure fields'),
|
||||||
children: useCustomFormItemInitializerFields(),
|
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}
|
insertPosition={insertPosition}
|
||||||
component={component}
|
component={component}
|
||||||
title={component ? null : t('Configure fields')}
|
title={component ? null : t('Configure fields')}
|
||||||
|
@ -2,28 +2,45 @@ import { union } from 'lodash';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializer } from '../SchemaInitializer';
|
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) => {
|
export const FormItemInitializers = (props: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { insertPosition, component } = props;
|
const { insertPosition, component } = props;
|
||||||
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
|
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
|
||||||
|
const inheritFields = useInheritsFormItemInitializerFields();
|
||||||
return (
|
const compile = useCompile();
|
||||||
<SchemaInitializer.Button
|
const fieldItems: any[] = [
|
||||||
wrap={gridRowColWrap}
|
|
||||||
icon={'SettingOutlined'}
|
|
||||||
items={union<any>(
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
type: 'itemGroup',
|
type: 'itemGroup',
|
||||||
title: t('Display fields'),
|
title: t('Display fields'),
|
||||||
children: useFormItemInitializerFields(),
|
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',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
@ -32,9 +49,9 @@ export const FormItemInitializers = (props: any) => {
|
|||||||
title: t('Display association fields'),
|
title: t('Display association fields'),
|
||||||
children: associationFields,
|
children: associationFields,
|
||||||
},
|
},
|
||||||
]
|
);
|
||||||
: [],
|
|
||||||
[
|
fieldItems.push(
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
@ -53,8 +70,12 @@ export const FormItemInitializers = (props: any) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
);
|
||||||
)}
|
return (
|
||||||
|
<SchemaInitializer.Button
|
||||||
|
wrap={gridRowColWrap}
|
||||||
|
icon={'SettingOutlined'}
|
||||||
|
items={fieldItems}
|
||||||
insertPosition={insertPosition}
|
insertPosition={insertPosition}
|
||||||
component={component}
|
component={component}
|
||||||
title={component ? null : t('Configure fields')}
|
title={component ? null : t('Configure fields')}
|
||||||
|
@ -2,27 +2,38 @@ import { union } from 'lodash';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializer } from '../SchemaInitializer';
|
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) => {
|
export const ReadPrettyFormItemInitializers = (props: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { insertPosition, component } = props;
|
const { insertPosition, component } = props;
|
||||||
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
|
const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
|
||||||
|
const inheritFields = useInheritsFormItemInitializerFields();
|
||||||
return (
|
const compile = useCompile();
|
||||||
<SchemaInitializer.Button
|
const fieldItems: any[] = [
|
||||||
wrap={gridRowColWrap}
|
|
||||||
icon={'SettingOutlined'}
|
|
||||||
items={union<any>(
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
type: 'itemGroup',
|
type: 'itemGroup',
|
||||||
title: t('Display fields'),
|
title: t('Display fields'),
|
||||||
children: useFormItemInitializerFields(),
|
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',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
@ -31,9 +42,9 @@ export const ReadPrettyFormItemInitializers = (props: any) => {
|
|||||||
title: t('Display association fields'),
|
title: t('Display association fields'),
|
||||||
children: associationFields,
|
children: associationFields,
|
||||||
},
|
},
|
||||||
]
|
]);
|
||||||
: [],
|
|
||||||
[
|
fieldItems.push(
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
@ -52,8 +63,12 @@ export const ReadPrettyFormItemInitializers = (props: any) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
);
|
||||||
)}
|
return (
|
||||||
|
<SchemaInitializer.Button
|
||||||
|
wrap={gridRowColWrap}
|
||||||
|
icon={'SettingOutlined'}
|
||||||
|
items={fieldItems}
|
||||||
insertPosition={insertPosition}
|
insertPosition={insertPosition}
|
||||||
component={component}
|
component={component}
|
||||||
title={component ? null : t('Configure fields')}
|
title={component ? null : t('Configure fields')}
|
||||||
|
@ -104,6 +104,23 @@ const useRelationFields = () => {
|
|||||||
return relationFields;
|
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) => {
|
export const RecordBlockInitializers = (props: any) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { insertPosition, component } = props;
|
const { insertPosition, component } = props;
|
||||||
@ -134,6 +151,11 @@ export const RecordBlockInitializers = (props: any) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: '{{t("Children collection blocks")}}',
|
||||||
|
children: useInheritFields(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'itemGroup',
|
type: 'itemGroup',
|
||||||
title: '{{t("Relationship blocks")}}',
|
title: '{{t("Relationship blocks")}}',
|
||||||
|
@ -1,34 +1,65 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializer } from '../SchemaInitializer';
|
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) => {
|
export const TableColumnInitializers = (props: any) => {
|
||||||
const { items = [] } = props;
|
const { items = [] } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const associatedFields = useAssociatedTableColumnInitializerFields();
|
const associatedFields = useAssociatedTableColumnInitializerFields();
|
||||||
const fieldItems: any[] = [{
|
const inheritFields = useInheritsTableColumnInitializerFields();
|
||||||
|
const compile = useCompile();
|
||||||
|
const fieldItems: any[] = [
|
||||||
|
{
|
||||||
type: 'itemGroup',
|
type: 'itemGroup',
|
||||||
title: t('Display fields'),
|
title: t('Display fields'),
|
||||||
children: useTableColumnInitializerFields(),
|
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: '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',
|
type: 'itemGroup',
|
||||||
title: t('Display association fields'),
|
title: t('Display association fields'),
|
||||||
children: associatedFields,
|
children: associatedFields,
|
||||||
})
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
fieldItems.push({
|
fieldItems.push(
|
||||||
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
title: t('Action column'),
|
title: t('Action column'),
|
||||||
component: 'TableActionColumnInitializer',
|
component: 'TableActionColumnInitializer',
|
||||||
})
|
},
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Button
|
<SchemaInitializer.Button
|
||||||
insertPosition={'beforeEnd'}
|
insertPosition={'beforeEnd'}
|
||||||
@ -49,10 +80,7 @@ export const TableColumnInitializers = (props: any) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
items={itemsMerge(
|
items={itemsMerge(fieldItems, items)}
|
||||||
fieldItems,
|
|
||||||
items,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{t('Configure columns')}
|
{t('Configure columns')}
|
||||||
</SchemaInitializer.Button>
|
</SchemaInitializer.Button>
|
||||||
|
@ -12,7 +12,7 @@ import { DataBlockInitializer } from "./DataBlockInitializer";
|
|||||||
export const CalendarBlockInitializer = (props) => {
|
export const CalendarBlockInitializer = (props) => {
|
||||||
const { insert } = props;
|
const { insert } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getCollection } = useCollectionManager();
|
const { getCollectionFields } = useCollectionManager();
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
return (
|
return (
|
||||||
<DataBlockInitializer
|
<DataBlockInitializer
|
||||||
@ -20,8 +20,8 @@ export const CalendarBlockInitializer = (props) => {
|
|||||||
componentType={'Calendar'}
|
componentType={'Calendar'}
|
||||||
icon={<FormOutlined />}
|
icon={<FormOutlined />}
|
||||||
onCreateBlockSchema={async ({ item }) => {
|
onCreateBlockSchema={async ({ item }) => {
|
||||||
const collection = getCollection(item.name);
|
const collectionFields = getCollectionFields(item.name);
|
||||||
const stringFields = collection?.fields
|
const stringFields = collectionFields
|
||||||
?.filter((field) => field.type === 'string')
|
?.filter((field) => field.type === 'string')
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
return {
|
return {
|
||||||
@ -29,7 +29,7 @@ export const CalendarBlockInitializer = (props) => {
|
|||||||
value: field.name,
|
value: field.name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const dateFields = collection?.fields
|
const dateFields = collectionFields
|
||||||
?.filter((field) => field.type === 'date')
|
?.filter((field) => field.type === 'date')
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
return {
|
return {
|
||||||
|
@ -13,7 +13,7 @@ import { SchemaComponent, SchemaComponentOptions } from "../../schema-component"
|
|||||||
export const KanbanBlockInitializer = (props) => {
|
export const KanbanBlockInitializer = (props) => {
|
||||||
const { insert } = props;
|
const { insert } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getCollection } = useCollectionManager();
|
const { getCollectionFields,getCollection } = useCollectionManager();
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
return (
|
return (
|
||||||
@ -22,8 +22,8 @@ export const KanbanBlockInitializer = (props) => {
|
|||||||
componentType={'Kanban'}
|
componentType={'Kanban'}
|
||||||
icon={<FormOutlined />}
|
icon={<FormOutlined />}
|
||||||
onCreateBlockSchema={async ({ item }) => {
|
onCreateBlockSchema={async ({ item }) => {
|
||||||
const collection = getCollection(item.name);
|
const collectionFields = getCollectionFields(item.name);
|
||||||
const fields = collection?.fields
|
const fields = collectionFields
|
||||||
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
|
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
return {
|
return {
|
||||||
@ -64,7 +64,7 @@ export const KanbanBlockInitializer = (props) => {
|
|||||||
initialValues: {},
|
initialValues: {},
|
||||||
});
|
});
|
||||||
const sortName = `${values.groupField.value}_sort`;
|
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) {
|
if (!exists) {
|
||||||
await api.resource('collections.fields', item.name).create({
|
await api.resource('collections.fields', item.name).create({
|
||||||
values: {
|
values: {
|
||||||
|
@ -7,10 +7,17 @@ import { SchemaInitializer } from '../SchemaInitializer';
|
|||||||
import { createReadPrettyFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
|
import { createReadPrettyFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
|
||||||
|
|
||||||
export const RecordReadPrettyFormBlockInitializer = (props) => {
|
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 { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||||
const collection = useCollection();
|
const collection = targetCollection || useCollection();
|
||||||
const association = useBlockAssociationContext();
|
const association = useBlockAssociationContext();
|
||||||
const { block } = useBlockRequestContext();
|
const { block } = useBlockRequestContext();
|
||||||
const actionInitializers =
|
const actionInitializers =
|
||||||
@ -18,7 +25,7 @@ export const RecordReadPrettyFormBlockInitializer = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Item
|
<SchemaInitializer.Item
|
||||||
icon={<FormOutlined />}
|
icon={icon && <FormOutlined />}
|
||||||
{...others}
|
{...others}
|
||||||
key={'123'}
|
key={'123'}
|
||||||
onClick={async ({ item }) => {
|
onClick={async ({ item }) => {
|
||||||
|
@ -73,15 +73,15 @@ export const findTableColumn = (schema: Schema, key: string, action: string, dee
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useTableColumnInitializerFields = () => {
|
export const useTableColumnInitializerFields = () => {
|
||||||
const { name, fields = [] } = useCollection();
|
const { currentFields = [] } = useCollection();
|
||||||
const { getInterface } = useCollectionManager();
|
const { getInterface } = useCollectionManager();
|
||||||
return fields
|
return currentFields
|
||||||
.filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey)
|
.filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey)
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
const interfaceConfig = getInterface(field.interface);
|
const interfaceConfig = getInterface(field.interface);
|
||||||
const schema = {
|
const schema = {
|
||||||
name: field.name,
|
name: field.name,
|
||||||
'x-collection-field': `${name}.${field.name}`,
|
'x-collection-field': `${field.name}`,
|
||||||
'x-component': 'CollectionField',
|
'x-component': 'CollectionField',
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
@ -105,7 +105,6 @@ export const useTableColumnInitializerFields = () => {
|
|||||||
export const useAssociatedTableColumnInitializerFields = () => {
|
export const useAssociatedTableColumnInitializerFields = () => {
|
||||||
const { name, fields } = useCollection();
|
const { name, fields } = useCollection();
|
||||||
const { getInterface, getCollectionFields } = useCollectionManager();
|
const { getInterface, getCollectionFields } = useCollectionManager();
|
||||||
|
|
||||||
const groups = fields
|
const groups = fields
|
||||||
?.filter((field) => {
|
?.filter((field) => {
|
||||||
return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface);
|
return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface);
|
||||||
@ -141,7 +140,6 @@ export const useAssociatedTableColumnInitializerFields = () => {
|
|||||||
schema,
|
schema,
|
||||||
} as SchemaInitializerItemOptions;
|
} as SchemaInitializerItemOptions;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'subMenu',
|
type: 'subMenu',
|
||||||
title: field.uiSchema?.title,
|
title: field.uiSchema?.title,
|
||||||
@ -152,13 +150,51 @@ export const useAssociatedTableColumnInitializerFields = () => {
|
|||||||
return groups;
|
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) => {
|
export const useFormItemInitializerFields = (options?: any) => {
|
||||||
const { name, fields } = useCollection();
|
const { name, currentFields } = useCollection();
|
||||||
const { getInterface } = useCollectionManager();
|
const { getInterface } = useCollectionManager();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
||||||
|
|
||||||
return fields
|
return currentFields
|
||||||
?.filter((field) => field?.interface && !field?.isForeignKey)
|
?.filter((field) => field?.interface && !field?.isForeignKey)
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
const interfaceConfig = getInterface(field.interface);
|
const interfaceConfig = getInterface(field.interface);
|
||||||
@ -170,7 +206,7 @@ export const useFormItemInitializerFields = (options?: any) => {
|
|||||||
'x-designer': 'FormItem.Designer',
|
'x-designer': 'FormItem.Designer',
|
||||||
'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField',
|
'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-collection-field': `${name}.${field.name}`,
|
'x-collection-field': `${field.name}`,
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
|
'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
|
||||||
};
|
};
|
||||||
@ -240,13 +276,52 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
|
|||||||
return groups;
|
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) => {
|
export const useCustomFormItemInitializerFields = (options?: any) => {
|
||||||
const { name, fields } = useCollection();
|
const { name, currentFields } = useCollection();
|
||||||
const { getInterface } = useCollectionManager();
|
const { getInterface } = useCollectionManager();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
||||||
const remove = useRemoveGridFormItem();
|
const remove = useRemoveGridFormItem();
|
||||||
return fields
|
return currentFields
|
||||||
?.filter((field) => {
|
?.filter((field) => {
|
||||||
return field?.interface && !field?.uiSchema?.['x-read-pretty'];
|
return field?.interface && !field?.uiSchema?.['x-read-pretty'];
|
||||||
})
|
})
|
||||||
@ -259,7 +334,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => {
|
|||||||
'x-designer': 'FormItem.Designer',
|
'x-designer': 'FormItem.Designer',
|
||||||
'x-component': 'AssignedField',
|
'x-component': 'AssignedField',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-collection-field': `${name}.${field.name}`,
|
'x-collection-field': `${field.name}`,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
type: 'item',
|
type: 'item',
|
||||||
@ -293,7 +368,7 @@ export const useCustomBulkEditFormItemInitializerFields = (options?: any) => {
|
|||||||
'x-designer': 'FormItem.Designer',
|
'x-designer': 'FormItem.Designer',
|
||||||
'x-component': 'BulkEditField',
|
'x-component': 'BulkEditField',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-collection-field': `${name}.${field.name}`,
|
'x-collection-field': `${field.name}`,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
type: 'item',
|
type: 'item',
|
||||||
@ -344,6 +419,7 @@ export const useCurrentSchema = (action: string, key: string, find = findSchema,
|
|||||||
}
|
}
|
||||||
const { remove } = useDesignable();
|
const { remove } = useDesignable();
|
||||||
const schema = find(fieldSchema, key, action);
|
const schema = find(fieldSchema, key, action);
|
||||||
|
console.log(fieldSchema, key, action);
|
||||||
return {
|
return {
|
||||||
schema,
|
schema,
|
||||||
exists: !!schema,
|
exists: !!schema,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Dropdown, Menu } from 'antd';
|
import { Dropdown, Menu } from 'antd';
|
||||||
import React, { createContext, useState } from 'react';
|
import React, { createContext, useState, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useAPIClient, useCurrentUserContext } from '..';
|
import { useAPIClient, useCurrentUserContext } from '..';
|
||||||
@ -8,14 +8,11 @@ import { ChangePassword } from './ChangePassword';
|
|||||||
import { EditProfile } from './EditProfile';
|
import { EditProfile } from './EditProfile';
|
||||||
import { LanguageSettings } from './LanguageSettings';
|
import { LanguageSettings } from './LanguageSettings';
|
||||||
import { SwitchRole } from './SwitchRole';
|
import { SwitchRole } from './SwitchRole';
|
||||||
|
import {useCurrentDatabase} from '../database/CurrentDatabaseProvider'
|
||||||
|
|
||||||
|
|
||||||
const ApplicationVersion = () => {
|
const ApplicationVersion = () => {
|
||||||
const { data, loading } = useRequest({
|
const data=useCurrentDatabase();
|
||||||
url: 'app:getInfo',
|
|
||||||
});
|
|
||||||
if (loading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Menu.Item key="version" disabled>
|
<Menu.Item key="version" disabled>
|
||||||
Version {data?.data?.version}
|
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'> {
|
export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> {
|
||||||
name: string;
|
name: string;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
|
inherits?: string[] | string;
|
||||||
filterTargetKey?: string;
|
filterTargetKey?: string;
|
||||||
fields?: FieldOptions[];
|
fields?: FieldOptions[];
|
||||||
model?: string | ModelCtor<Model>;
|
model?: string | ModelCtor<Model>;
|
||||||
@ -74,7 +75,9 @@ export class Collection<
|
|||||||
|
|
||||||
this.bindFieldEventListener();
|
this.bindFieldEventListener();
|
||||||
this.modelInit();
|
this.modelInit();
|
||||||
|
|
||||||
this.db.modelCollection.set(this.model, this);
|
this.db.modelCollection.set(this.model, this);
|
||||||
|
this.db.tableNameCollectionMap.set(this.model.tableName, this);
|
||||||
|
|
||||||
this.setFields(options.fields);
|
this.setFields(options.fields);
|
||||||
this.setRepository(options.repository);
|
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.removeField(name);
|
||||||
this.fields.set(name, field);
|
this.fields.set(name, field);
|
||||||
this.emit('field.afterAdd', 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;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +399,7 @@ export class Collection<
|
|||||||
for (const associationKey in associations) {
|
for (const associationKey in associations) {
|
||||||
const association = associations[associationKey];
|
const association = associations[associationKey];
|
||||||
modelNames.add(association.target.name);
|
modelNames.add(association.target.name);
|
||||||
|
|
||||||
if ((<any>association).through) {
|
if ((<any>association).through) {
|
||||||
modelNames.add((<any>association).through.model.name);
|
modelNames.add((<any>association).through.model.name);
|
||||||
}
|
}
|
||||||
@ -388,4 +417,12 @@ export class Collection<
|
|||||||
await model.sync(syncOptions);
|
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';
|
} from './types';
|
||||||
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
||||||
import ReferencesMap from './features/ReferencesMap';
|
import ReferencesMap from './features/ReferencesMap';
|
||||||
|
import { InheritedCollection } from './inherited-collection';
|
||||||
|
import InheritanceMap from './inherited-map';
|
||||||
|
|
||||||
export interface MergeOptions extends merge.Options {}
|
export interface MergeOptions extends merge.Options {}
|
||||||
|
|
||||||
@ -148,7 +150,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
collections = new Map<string, Collection>();
|
collections = new Map<string, Collection>();
|
||||||
pendingFields = new Map<string, RelationField[]>();
|
pendingFields = new Map<string, RelationField[]>();
|
||||||
modelCollection = new Map<ModelCtor<any>, Collection>();
|
modelCollection = new Map<ModelCtor<any>, Collection>();
|
||||||
|
tableNameCollectionMap = new Map<string, Collection>();
|
||||||
|
|
||||||
referenceMap = new ReferencesMap();
|
referenceMap = new ReferencesMap();
|
||||||
|
inheritanceMap = new InheritanceMap();
|
||||||
|
|
||||||
modelHook: ModelHook;
|
modelHook: ModelHook;
|
||||||
version: DatabaseVersion;
|
version: DatabaseVersion;
|
||||||
@ -293,7 +298,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
): Collection<Attributes, CreateAttributes> {
|
): Collection<Attributes, CreateAttributes> {
|
||||||
this.emit('beforeDefineCollection', options);
|
this.emit('beforeDefineCollection', options);
|
||||||
|
|
||||||
const collection = new Collection(options, {
|
const collection = options.inherits
|
||||||
|
? new InheritedCollection(options, {
|
||||||
|
database: this,
|
||||||
|
})
|
||||||
|
: new Collection(options, {
|
||||||
database: this,
|
database: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -429,6 +438,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
if (isMySQL) {
|
if (isMySQL) {
|
||||||
await this.sequelize.query('SET FOREIGN_KEY_CHECKS = 1', null);
|
await this.sequelize.query('SET FOREIGN_KEY_CHECKS = 1', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { Database } from '../database';
|
import { Database } from '../database';
|
||||||
import { ModelEventTypes } from '../types';
|
import { ModelEventTypes } from '../types';
|
||||||
|
import { InheritedCollection } from '../inherited-collection';
|
||||||
|
|
||||||
export interface FieldContext {
|
export interface FieldContext {
|
||||||
database: Database;
|
database: Database;
|
||||||
@ -19,6 +20,7 @@ export interface FieldContext {
|
|||||||
export interface BaseFieldOptions {
|
export interface BaseFieldOptions {
|
||||||
name?: string;
|
name?: string;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,8 +34,17 @@ export abstract class Field {
|
|||||||
context: FieldContext;
|
context: FieldContext;
|
||||||
database: Database;
|
database: Database;
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
|
|
||||||
[key: string]: any;
|
[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() {
|
get name() {
|
||||||
return this.options.name;
|
return this.options.name;
|
||||||
}
|
}
|
||||||
@ -46,14 +57,6 @@ export abstract class Field {
|
|||||||
return this.options.dataType;
|
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) {
|
async sync(syncOptions: SyncOptions) {
|
||||||
await this.collection.sync({
|
await this.collection.sync({
|
||||||
...syncOptions,
|
...syncOptions,
|
||||||
@ -88,11 +91,18 @@ export abstract class Field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeFromDb(options?: QueryInterfaceOptions) {
|
async removeFromDb(options?: QueryInterfaceOptions) {
|
||||||
if (!this.collection.model.rawAttributes[this.name]) {
|
const attribute = this.collection.model.rawAttributes[this.name];
|
||||||
|
|
||||||
|
if (!attribute) {
|
||||||
this.remove();
|
this.remove();
|
||||||
// console.log('field is not attribute');
|
// console.log('field is not attribute');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.collection.isInherited() && (<InheritedCollection>this.collection).parentFields().has(this.name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((this.collection.model as any)._virtualAttributes.has(this.name)) {
|
if ((this.collection.model as any)._virtualAttributes.has(this.name)) {
|
||||||
this.remove();
|
this.remove();
|
||||||
// console.log('field is virtual attribute');
|
// console.log('field is virtual attribute');
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export { DataTypes, ModelCtor, Op, SyncOptions } from 'sequelize';
|
export { DataTypes, ModelCtor, Op, SyncOptions } from 'sequelize';
|
||||||
export * from './collection';
|
export * from './collection';
|
||||||
|
export * from './inherited-collection';
|
||||||
export * from './database';
|
export * from './database';
|
||||||
export { Database as default } from './database';
|
export { Database as default } from './database';
|
||||||
export * from './fields';
|
export * from './fields';
|
||||||
@ -14,4 +15,3 @@ export * from './relation-repository/multiple-relation-repository';
|
|||||||
export * from './relation-repository/single-relation-repository';
|
export * from './relation-repository/single-relation-repository';
|
||||||
export * from './repository';
|
export * from './repository';
|
||||||
export * from './update-associations';
|
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 { Collection } from './collection';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { Field } from './fields';
|
import { Field } from './fields';
|
||||||
|
import type { InheritedCollection } from './inherited-collection';
|
||||||
|
import { SyncRunner } from './sync-runner';
|
||||||
|
|
||||||
interface IModel {
|
interface IModel {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@ -145,4 +147,14 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
|
|||||||
|
|
||||||
return lodash.orderBy(data, orderItems, orderDirections);
|
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;
|
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) {
|
protected parseFields(filterParams: any) {
|
||||||
const appends = this.options?.appends || [];
|
const appends = this.options?.appends || [];
|
||||||
const except = [];
|
const except = [];
|
||||||
@ -113,6 +120,10 @@ export class OptionsParser {
|
|||||||
exclude: [],
|
exclude: [],
|
||||||
}; // out put all fields by default
|
}; // out put all fields by default
|
||||||
|
|
||||||
|
if (this.collection.isParent()) {
|
||||||
|
attributes.include.push(this.inheritFromSubQuery());
|
||||||
|
}
|
||||||
|
|
||||||
if (this.options?.fields) {
|
if (this.options?.fields) {
|
||||||
// 将fields拆分为 attributes 和 appends
|
// 将fields拆分为 attributes 和 appends
|
||||||
for (const field of this.options.fields) {
|
for (const field of this.options.fields) {
|
||||||
|
@ -232,6 +232,8 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
...this.buildQueryOptions(options),
|
...this.buildQueryOptions(options),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let rows;
|
||||||
|
|
||||||
if (opts.include && opts.include.length > 0) {
|
if (opts.include && opts.include.length > 0) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const primaryKeyField = model.primaryKeyField || model.primaryKeyAttribute;
|
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) => {
|
queryPromises: opts.include.map((include) => {
|
||||||
return model
|
const options = {
|
||||||
.findAll({
|
|
||||||
...omit(opts, ['limit', 'offset']),
|
...omit(opts, ['limit', 'offset']),
|
||||||
include: include,
|
include: include,
|
||||||
where,
|
where,
|
||||||
transaction,
|
transaction,
|
||||||
})
|
};
|
||||||
.then((rows) => {
|
|
||||||
|
return model.findAll(options).then((rows) => {
|
||||||
return { rows, include };
|
return { rows, include };
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
templateModel: ids[0].row,
|
templateModel: ids[0].row,
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
|
rows = await model.findAll({
|
||||||
return await model.findAll({
|
|
||||||
...opts,
|
...opts,
|
||||||
transaction,
|
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
|
* find and count
|
||||||
* @param options
|
* @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 { mockDatabase } from '@nocobase/database';
|
||||||
export * from './mockServer';
|
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', 'getInfo');
|
||||||
this.app.acl.allow('app', 'getPlugins');
|
this.app.acl.allow('app', 'getPlugins');
|
||||||
this.app.acl.allow('plugins', 'getPinned', 'loggedIn');
|
this.app.acl.allow('plugins', 'getPinned', 'loggedIn');
|
||||||
|
const dialect = this.app.db.sequelize.getDialect();
|
||||||
this.app.resource({
|
this.app.resource({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
actions: {
|
actions: {
|
||||||
@ -35,6 +36,9 @@ export class ClientPlugin extends Plugin {
|
|||||||
lang = currentUser?.appLang;
|
lang = currentUser?.appLang;
|
||||||
}
|
}
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
database: {
|
||||||
|
dialect,
|
||||||
|
},
|
||||||
version: await ctx.app.version.get(),
|
version: await ctx.app.version.get(),
|
||||||
lang,
|
lang,
|
||||||
};
|
};
|
||||||
|
@ -8,9 +8,7 @@ describe('collections repository', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await createApp();
|
app = await createApp();
|
||||||
agent = app.agent();
|
agent = app.agent();
|
||||||
await agent
|
await agent.resource('collections').create({
|
||||||
.resource('collections')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
fields: [
|
fields: [
|
||||||
@ -21,9 +19,7 @@ describe('collections repository', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('collections').create({
|
||||||
.resource('collections')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
name: 'foos',
|
name: 'foos',
|
||||||
fields: [
|
fields: [
|
||||||
@ -34,18 +30,14 @@ describe('collections repository', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('collections.fields', 'tags').create({
|
||||||
.resource('collections.fields', 'tags')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
name: 'foos',
|
name: 'foos',
|
||||||
target: 'foos',
|
target: 'foos',
|
||||||
type: 'belongsToMany',
|
type: 'belongsToMany',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('collections').create({
|
||||||
.resource('collections')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
name: 'comments',
|
name: 'comments',
|
||||||
fields: [
|
fields: [
|
||||||
@ -56,9 +48,7 @@ describe('collections repository', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('collections').create({
|
||||||
.resource('collections')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
fields: [
|
fields: [
|
||||||
@ -95,23 +85,17 @@ describe('collections repository', () => {
|
|||||||
it('case 2', async () => {
|
it('case 2', async () => {
|
||||||
const response = await app.agent().resource('posts').create();
|
const response = await app.agent().resource('posts').create();
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 1',
|
title: 'comment 1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 2',
|
title: 'comment 2',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const response2 = await agent
|
const response2 = await agent.resource('posts').list({
|
||||||
.resource('posts')
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
'comments.title': 'comment 1',
|
'comments.title': 'comment 1',
|
||||||
},
|
},
|
||||||
@ -122,16 +106,12 @@ describe('collections repository', () => {
|
|||||||
it('case 3', async () => {
|
it('case 3', async () => {
|
||||||
const response = await app.agent().resource('posts').create();
|
const response = await app.agent().resource('posts').create();
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 1',
|
title: 'comment 1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const response2 = await agent
|
const response2 = await agent.resource('posts').list({
|
||||||
.resource('posts')
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
'comments.id': 3,
|
'comments.id': 3,
|
||||||
},
|
},
|
||||||
@ -142,16 +122,12 @@ describe('collections repository', () => {
|
|||||||
it('case 4', async () => {
|
it('case 4', async () => {
|
||||||
const response = await app.agent().resource('posts').create();
|
const response = await app.agent().resource('posts').create();
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 1',
|
title: 'comment 1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const response2 = await agent
|
const response2 = await agent.resource('posts').list({
|
||||||
.resource('posts')
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
$and: [
|
$and: [
|
||||||
{
|
{
|
||||||
@ -174,9 +150,7 @@ describe('collections repository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('case 6', async () => {
|
it('case 6', async () => {
|
||||||
const response = await agent
|
const response = await agent.resource('posts').create({
|
||||||
.resource('posts')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
tags: [
|
tags: [
|
||||||
{},
|
{},
|
||||||
@ -192,9 +166,7 @@ describe('collections repository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('case 7', async () => {
|
it('case 7', async () => {
|
||||||
const response = await agent
|
const response = await agent.resource('posts').create({
|
||||||
.resource('posts')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
tags: [
|
tags: [
|
||||||
{},
|
{},
|
||||||
@ -205,9 +177,7 @@ describe('collections repository', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
const response1 = await agent
|
const response1 = await agent.resource('posts.tags', postId).list({
|
||||||
.resource('posts.tags', postId)
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
title: 'Tag1',
|
title: 'Tag1',
|
||||||
},
|
},
|
||||||
@ -216,9 +186,7 @@ describe('collections repository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('case 8', async () => {
|
it('case 8', async () => {
|
||||||
const response = await agent
|
const response = await agent.resource('posts').create({
|
||||||
.resource('posts')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
tags: [
|
tags: [
|
||||||
{},
|
{},
|
||||||
@ -232,9 +200,7 @@ describe('collections repository', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
const response1 = await agent
|
const response1 = await agent.resource('posts.tags', postId).list({
|
||||||
.resource('posts.tags', postId)
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
$or: [{ title: 'Tag1' }, { title: 'Tag2' }],
|
$or: [{ title: 'Tag1' }, { title: 'Tag2' }],
|
||||||
},
|
},
|
||||||
@ -243,9 +209,7 @@ describe('collections repository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('case 9', async () => {
|
it('case 9', async () => {
|
||||||
const response = await agent
|
const response = await agent.resource('posts').create({
|
||||||
.resource('posts')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
tags: [
|
tags: [
|
||||||
{},
|
{},
|
||||||
@ -259,9 +223,7 @@ describe('collections repository', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
const response1 = await agent
|
const response1 = await agent.resource('posts.tags', postId).list({
|
||||||
.resource('posts.tags', postId)
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
$or: [{ title: 'Tag1' }, { title: 'Tag2' }],
|
$or: [{ title: 'Tag1' }, { title: 'Tag2' }],
|
||||||
},
|
},
|
||||||
@ -270,9 +232,7 @@ describe('collections repository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('case 10', async () => {
|
it('case 10', async () => {
|
||||||
const response = await agent
|
const response = await agent.resource('posts').create({
|
||||||
.resource('posts')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
tags: [
|
tags: [
|
||||||
{},
|
{},
|
||||||
@ -286,9 +246,7 @@ describe('collections repository', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
const response1 = await agent
|
const response1 = await agent.resource('posts.tags', postId).list({
|
||||||
.resource('posts.tags', postId)
|
|
||||||
.list({
|
|
||||||
appends: ['foos'],
|
appends: ['foos'],
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@ -301,39 +259,29 @@ describe('collections repository', () => {
|
|||||||
it('case 11', async () => {
|
it('case 11', async () => {
|
||||||
const response = await app.agent().resource('posts').create();
|
const response = await app.agent().resource('posts').create();
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 1',
|
title: 'comment 1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 2',
|
title: 'comment 2',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const response2 = await app.agent().resource('posts').create();
|
const response2 = await app.agent().resource('posts').create();
|
||||||
const postId2 = response2.body.data.id;
|
const postId2 = response2.body.data.id;
|
||||||
await agent
|
await agent.resource('posts.comments', postId2).create({
|
||||||
.resource('posts.comments', postId2)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 2',
|
title: 'comment 2',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('posts.comments', postId2).create({
|
||||||
.resource('posts.comments', postId2)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 2',
|
title: 'comment 2',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const response3 = await agent
|
const response3 = await agent.resource('posts').list({
|
||||||
.resource('posts')
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
$or: [
|
$or: [
|
||||||
{
|
{
|
||||||
@ -351,30 +299,22 @@ describe('collections repository', () => {
|
|||||||
it('case 12', async () => {
|
it('case 12', async () => {
|
||||||
const response = await app.agent().resource('posts').create();
|
const response = await app.agent().resource('posts').create();
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 1',
|
title: 'comment 1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 2',
|
title: 'comment 2',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('posts.comments', postId).create({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
title: 'comment 3',
|
title: 'comment 3',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const response2 = await agent
|
const response2 = await agent.resource('posts.comments', postId).list({
|
||||||
.resource('posts.comments', postId)
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
$or: [
|
$or: [
|
||||||
{
|
{
|
||||||
@ -394,31 +334,23 @@ describe('collections repository', () => {
|
|||||||
const tag1 = await tagRepository.create({ values: { title: 'tag1' } });
|
const tag1 = await tagRepository.create({ values: { title: 'tag1' } });
|
||||||
const tag2 = await tagRepository.create({ values: { title: 'tag2' } });
|
const tag2 = await tagRepository.create({ values: { title: 'tag2' } });
|
||||||
const tag3 = await tagRepository.create({ values: { title: 'tag3' } });
|
const tag3 = await tagRepository.create({ values: { title: 'tag3' } });
|
||||||
await agent
|
await agent.resource('posts').create({
|
||||||
.resource('posts')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
tags: [tag1.get('id'), tag3.get('id')],
|
tags: [tag1.get('id'), tag3.get('id')],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('posts').create({
|
||||||
.resource('posts')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
tags: [tag2.get('id')],
|
tags: [tag2.get('id')],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await agent
|
await agent.resource('posts').create({
|
||||||
.resource('posts')
|
|
||||||
.create({
|
|
||||||
values: {
|
values: {
|
||||||
tags: [tag2.get('id'), tag3.get('id')],
|
tags: [tag2.get('id'), tag3.get('id')],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const response1 = await agent
|
const response1 = await agent.resource('posts').list({
|
||||||
.resource('posts')
|
|
||||||
.list({
|
|
||||||
filter: {
|
filter: {
|
||||||
$or: [{ 'tags.title': 'tag1' }, { 'tags.title': 'tag3' }],
|
$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 Database, { Collection, MagicAttributeModel } from '@nocobase/database';
|
||||||
import { SyncOptions, Transactionable } from 'sequelize';
|
import { SyncOptions, Transactionable } from 'sequelize';
|
||||||
import { FieldModel } from './field';
|
import { FieldModel } from './field';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
interface LoadOptions extends Transactionable {
|
interface LoadOptions extends Transactionable {
|
||||||
// TODO
|
// TODO
|
||||||
@ -19,25 +20,27 @@ export class CollectionModel extends MagicAttributeModel {
|
|||||||
|
|
||||||
let collection: Collection;
|
let collection: Collection;
|
||||||
|
|
||||||
|
const collectionOptions = {
|
||||||
|
...this.get(),
|
||||||
|
fields: [],
|
||||||
|
};
|
||||||
|
|
||||||
if (this.db.hasCollection(name)) {
|
if (this.db.hasCollection(name)) {
|
||||||
collection = this.db.getCollection(name);
|
collection = this.db.getCollection(name);
|
||||||
|
|
||||||
if (skipExist) {
|
if (skipExist) {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
collection.updateOptions({
|
|
||||||
...this.get(),
|
collection.updateOptions(collectionOptions);
|
||||||
fields: [],
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
collection = this.db.collection({
|
collection = this.db.collection(collectionOptions);
|
||||||
...this.get(),
|
|
||||||
fields: [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipField) {
|
if (!skipField) {
|
||||||
await this.loadFields({ transaction });
|
await this.loadFields({ transaction });
|
||||||
}
|
}
|
||||||
|
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +86,7 @@ export class CollectionModel extends MagicAttributeModel {
|
|||||||
const collection = await this.load({
|
const collection = await this.load({
|
||||||
transaction: options?.transaction,
|
transaction: options?.transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await collection.sync({
|
await collection.sync({
|
||||||
force: false,
|
force: false,
|
||||||
@ -92,8 +96,73 @@ export class CollectionModel extends MagicAttributeModel {
|
|||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
const name = this.get('name');
|
const name = this.get('name');
|
||||||
this.db.removeCollection(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;
|
return (<any>this.constructor).database;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAssociationField() {
|
||||||
|
return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(this.get('type'));
|
||||||
|
}
|
||||||
|
|
||||||
async load(loadOptions?: LoadOptions) {
|
async load(loadOptions?: LoadOptions) {
|
||||||
const { skipExist = false } = loadOptions || {};
|
const { skipExist = false } = loadOptions || {};
|
||||||
const collectionName = this.get('collectionName');
|
const collectionName = this.get('collectionName');
|
||||||
|
|
||||||
if (!this.db.hasCollection(collectionName)) {
|
if (!this.db.hasCollection(collectionName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collection = this.db.getCollection(collectionName);
|
const collection = this.db.getCollection(collectionName);
|
||||||
const name = this.get('name');
|
const name = this.get('name');
|
||||||
|
|
||||||
if (skipExist && collection.hasField(name)) {
|
if (skipExist && collection.hasField(name)) {
|
||||||
return collection.getField(name);
|
return collection.getField(name);
|
||||||
}
|
}
|
||||||
@ -123,6 +129,7 @@ export class FieldModel extends MagicAttributeModel {
|
|||||||
if (!field) {
|
if (!field) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return field.removeFromDb({
|
return field.removeFromDb({
|
||||||
transaction: options.transaction,
|
transaction: options.transaction,
|
||||||
});
|
});
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
} from './hooks';
|
} from './hooks';
|
||||||
|
|
||||||
import { CollectionModel, FieldModel } from './models';
|
import { CollectionModel, FieldModel } from './models';
|
||||||
|
import { InheritedCollection } from '@nocobase/database';
|
||||||
|
|
||||||
export class CollectionManagerPlugin extends Plugin {
|
export class CollectionManagerPlugin extends Plugin {
|
||||||
async beforeLoad() {
|
async beforeLoad() {
|
||||||
@ -56,6 +57,20 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
// 要在 beforeInitOptions 之前处理
|
// 要在 beforeInitOptions 之前处理
|
||||||
this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db));
|
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', 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) => {
|
this.app.db.on('fields.beforeCreate', async (model, options) => {
|
||||||
const type = model.get('type');
|
const type = model.get('type');
|
||||||
const fn = beforeInitOptions[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('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) {
|
if (context) {
|
||||||
await model.migrate({
|
await model.migrate({
|
||||||
isNew: true,
|
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.app.db.on('fields.afterCreate', async (model: FieldModel, { context, transaction }) => {
|
this.app.db.on('fields.afterCreate', async (model: FieldModel, { context, transaction }) => {
|
||||||
if (context) {
|
if (context) {
|
||||||
@ -123,7 +140,7 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
|
|
||||||
// before field remove
|
// before field remove
|
||||||
this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db));
|
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);
|
await model.remove(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -131,6 +148,37 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
await model.remove(options);
|
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) => {
|
this.app.on('afterLoad', async (app, options) => {
|
||||||
if (options?.method === 'install') {
|
if (options?.method === 'install') {
|
||||||
return;
|
return;
|
||||||
|
Loading…
Reference in New Issue
Block a user