feat: collection inheritance (#1069)
* chore: test * chore: inherited-collection class * feat: collection inherit * feat: collection inherit * feat: inhertis sync runner * test: get parents fields * feat: collection inherit style promote * feat: sync * feat: sync alter table * feat: pgOnly Test * fix: child collection create api * feat: replace parent field * chore: reload parent fields * test: reload collection test * feat: details are displayed according to conditions * fix: typo * feat: inheritance map class * chore: is parent node * feat: display where child row created from * fix: find with appends * feat: add parent collection fields * fix: create table * feat: load fields for all children * refactor: sync fields from parent * test: has one field inhertis * feat: replace child association target * feat: should not replace child field when parent field update * test: should update inherit field when parent field update * feat: only the blocks directly inherited from the current data are displayed * fix: inherit from multiple collections * feat: only the blocks directly inherited from the current data are displayed * fix: test * feat: parent collection expend * fix: test * test: belongsToMany inherits * test: belongsToMany inherits * feat: block display * feat: collection inherite * feat: collection inherite * feat: multiple inherits * fix: sync runner * feat: collection inherite * feat: collecton inherits * feat: cannot be modified after inheritance and saving * feat: collection inherit for graph * feat: collection inherits * fix: drop inhertied field * fix: should throw error when type conflit * feat: output inherited fields * feat: bulk update collection fields * feat: collection fields * feat: collection fields * test: create relation with child table * fix: test * fix: test * fix: test * feat: style impove * test: should not replace field with difference type * feat: add text * fix: throw error when replace field with difference type * feat: overriding * feat: kan bankanban group fields * feat: calendar block fields * feat: kan bankanban group fields * fix: test * feat: relationship fields * feat: should delete child's field when parent field deleted * feat: foreign key filter * fix: build error & multiple inherit destory field * fix: test * chore: disable error * feat: no recursive update associations (#1091) * feat: update associations * fix(collection-manager): should update uiSchema * chore: flip if * feat: mutile inherits * feat: db dialect * feat: inherits show by database * chore: git hash into docker image * fix: js gzip * fix: dockerfile * chore: error message * feat: overriding * feat: overriding * feat: overriding * feat: local * feat: filter fields by interface * fix: database logging env * test: replace hasOne target * feat: add view * feat: local * chore: enable error * fix: update docs Co-authored-by: katherinehhh <katherine_15995@163.com> Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									688387413d
								
							
						
					
					
						commit
						e991b2965a
					
				
							
								
								
									
										19
									
								
								.github/workflows/aliyun-container-registry.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/aliyun-container-registry.yml
									
									
									
									
										vendored
									
									
								
							@ -22,19 +22,15 @@ jobs:
 | 
			
		||||
        ports:
 | 
			
		||||
          - 4873:4873
 | 
			
		||||
    steps:
 | 
			
		||||
      -
 | 
			
		||||
        name: Checkout
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
      -
 | 
			
		||||
        name: Set up QEMU
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v2
 | 
			
		||||
      -
 | 
			
		||||
        name: Set up Docker Buildx
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          driver-opts: network=host
 | 
			
		||||
      -
 | 
			
		||||
        name: Docker meta
 | 
			
		||||
      - name: Docker meta
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
@ -45,20 +41,19 @@ jobs:
 | 
			
		||||
            type=ref,event=pr
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
            type=semver,pattern={{major}}.{{minor}}
 | 
			
		||||
      -
 | 
			
		||||
        name: Login to Docker Hub
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ${{ secrets.ALI_DOCKER_REGISTRY }}
 | 
			
		||||
          username: ${{ secrets.ALI_DOCKER_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.ALI_DOCKER_PASSWORD }}
 | 
			
		||||
      -
 | 
			
		||||
        name: Build and push
 | 
			
		||||
      - name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          file: Dockerfile
 | 
			
		||||
          build-args: |
 | 
			
		||||
            VERDACCIO_URL=http://localhost:4873/
 | 
			
		||||
            COMMIT_HASH=${GITHUB_SHA}
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ secrets.ALI_DOCKER_REGISTRY }}/${{ steps.meta.outputs.tags }}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							@ -1,7 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
  // 使用 IntelliSense 了解相关属性。
 | 
			
		||||
  // 悬停以查看现有属性的描述。
 | 
			
		||||
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
 | 
			
		||||
  "version": "0.2.0",
 | 
			
		||||
  "configurations": [
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
FROM node:16 as builder
 | 
			
		||||
ARG VERDACCIO_URL=http://host.docker.internal:10104/
 | 
			
		||||
ARG COMIT_HASH
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && apt-get install -y jq
 | 
			
		||||
WORKDIR /tmp
 | 
			
		||||
@ -43,6 +44,8 @@ COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz
 | 
			
		||||
 | 
			
		||||
WORKDIR /app/nocobase
 | 
			
		||||
 | 
			
		||||
RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMIT_HASH" >> /app/nocobase/storage/uploads/COMIT_HASH
 | 
			
		||||
 | 
			
		||||
COPY ./docker/nocobase/docker-entrypoint.sh /app/
 | 
			
		||||
 | 
			
		||||
CMD ["/app/docker-entrypoint.sh"]
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ server {
 | 
			
		||||
    gzip_vary on;
 | 
			
		||||
    gzip_min_length 1024;
 | 
			
		||||
    gzip_proxied expired no-cache no-store private auth;
 | 
			
		||||
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
 | 
			
		||||
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/javascript application/xml;
 | 
			
		||||
 | 
			
		||||
    location /storage/uploads/ {
 | 
			
		||||
        alias /app/nocobase/storage/uploads/;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										74
									
								
								docs/zh-CN/welcome/release/inherits.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								docs/zh-CN/welcome/release/inherits.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
			
		||||
# v0.8.1: 数据表继承
 | 
			
		||||
 | 
			
		||||
数据表继承基于 [PostgreSQL 的 INHERITS 语法](https://www.postgresql.org/docs/current/tutorial-inheritance.html) 实现,仅限于 PostgreSQL 数据库安装的 NocoBase 时才会提供。
 | 
			
		||||
 | 
			
		||||
## 示例
 | 
			
		||||
 | 
			
		||||
我们从一个例子开始,假设要做一个教学系统,有三类用户:学生、家长和老师。
 | 
			
		||||
 | 
			
		||||
如果没有继承,要分别为三类用户建表:
 | 
			
		||||
 | 
			
		||||
- 学生:姓名、年龄、性别、身份证
 | 
			
		||||
- 家长:姓名、年龄、性别、职业、学历
 | 
			
		||||
- 老师:姓名、年龄、性别、教龄、已婚
 | 
			
		||||
 | 
			
		||||
有了数据表继承之后,共同的信息就可以提炼出来:
 | 
			
		||||
 | 
			
		||||
- 用户:姓名、年龄、性别
 | 
			
		||||
- 学生:身份证
 | 
			
		||||
- 家长:职业、学历
 | 
			
		||||
- 老师:教龄、已婚
 | 
			
		||||
 | 
			
		||||
ER 图如下:
 | 
			
		||||
 | 
			
		||||
<img src="./inherits/er.svg" style="max-width: 700px;" />
 | 
			
		||||
 | 
			
		||||
注:子表 ID 和父表 ID 共享序列
 | 
			
		||||
 | 
			
		||||
## 配置数据表继承
 | 
			
		||||
 | 
			
		||||
Inherits 字段选择需要继承的数据表
 | 
			
		||||
 | 
			
		||||
<img src="./inherits/inherit.jpg" />
 | 
			
		||||
 | 
			
		||||
通过代码配置如下:
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
db.collection({
 | 
			
		||||
  name: 'users',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
db.collection({
 | 
			
		||||
  name: 'students',
 | 
			
		||||
  inherits: 'users',
 | 
			
		||||
});
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
注意:
 | 
			
		||||
 | 
			
		||||
- 继承的表并不能随意选择,主键必须是唯一序列,比如 uuid 或者所有继承线路上的表的 id 自增序列都用同一个
 | 
			
		||||
- Inherits 参数不能被编辑
 | 
			
		||||
- 如果有继承关系,被继承的父表不能被删除
 | 
			
		||||
 | 
			
		||||
## 数据表字段列表
 | 
			
		||||
 | 
			
		||||
字段列表里同步显示继承的父表字段,父表字段不可以修改,但可以重写(Override)
 | 
			
		||||
 | 
			
		||||
<img src="./inherits/inherit-fields.jpg" />
 | 
			
		||||
 | 
			
		||||
重写父表字段的注意事项:
 | 
			
		||||
- 子表字段标识与父表字段一样时为重写
 | 
			
		||||
- 重写字段的类型必须保持一致
 | 
			
		||||
- 关系字段除了 target collection 以外的其他参数需要保持一致
 | 
			
		||||
 | 
			
		||||
## 父表的子表区块
 | 
			
		||||
 | 
			
		||||
在父表区块里可以配置子表的区块
 | 
			
		||||
 | 
			
		||||
<img src="./inherits/inherited-blocks.jpg" />
 | 
			
		||||
 | 
			
		||||
## 新增继承的父表字段的配置
 | 
			
		||||
 | 
			
		||||
当有继承的父表时,配置字段时,会提供从父表继承的字段的配置
 | 
			
		||||
 | 
			
		||||
<img src="./inherits/configure-fields.jpg" />
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/configure-fields.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/configure-fields.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 163 KiB  | 
							
								
								
									
										4
									
								
								docs/zh-CN/welcome/release/inherits/er.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								docs/zh-CN/welcome/release/inherits/er.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 25 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/form.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/form.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 246 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/inherit-fields.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/inherit-fields.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 313 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/inherit.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/inherit.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 231 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/inherited-blocks.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/zh-CN/welcome/release/inherits/inherited-blocks.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 142 KiB  | 
@ -1,7 +1,7 @@
 | 
			
		||||
import { IDatabaseOptions } from '@nocobase/database';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  logging: process.env.DB_LOGGING === 'on' ? console.log : false,
 | 
			
		||||
  logging: process.env.DB_LOGGING == 'on' ? customLogger : false,
 | 
			
		||||
  dialect: process.env.DB_DIALECT as any,
 | 
			
		||||
  storage: process.env.DB_STORAGE,
 | 
			
		||||
  username: process.env.DB_USER,
 | 
			
		||||
@ -12,3 +12,8 @@ export default {
 | 
			
		||||
  timezone: process.env.DB_TIMEZONE,
 | 
			
		||||
  tablePrefix: process.env.DB_TABLE_PREFIX,
 | 
			
		||||
} as IDatabaseOptions;
 | 
			
		||||
 | 
			
		||||
function customLogger(queryString, queryObject) {
 | 
			
		||||
  console.log(queryString); // outputs a string
 | 
			
		||||
  console.log(queryObject.bind); // outputs an array
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,11 @@
 | 
			
		||||
import { createForm } from '@formily/core';
 | 
			
		||||
import { useField } from '@formily/react';
 | 
			
		||||
import { useField, useFieldSchema } from '@formily/react';
 | 
			
		||||
import { Spin } from 'antd';
 | 
			
		||||
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
 | 
			
		||||
import { RecordProvider } from '../record-provider';
 | 
			
		||||
import { RecordProvider, useRecord } from '../record-provider';
 | 
			
		||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
 | 
			
		||||
import { useDesignable } from '../schema-component';
 | 
			
		||||
import { useCollectionManager } from '../collection-manager';
 | 
			
		||||
 | 
			
		||||
export const FormBlockContext = createContext<any>({});
 | 
			
		||||
 | 
			
		||||
@ -46,10 +48,19 @@ const InternalFormBlockProvider = (props) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const FormBlockProvider = (props) => {
 | 
			
		||||
  const record = useRecord();
 | 
			
		||||
  const { __tableName } = record;
 | 
			
		||||
  const { getParentCollections } = useCollectionManager();
 | 
			
		||||
  const inheritCollections = getParentCollections(__tableName);
 | 
			
		||||
  const { designable } = useDesignable();
 | 
			
		||||
  const flag =
 | 
			
		||||
    !designable && __tableName && !inheritCollections.includes(props.collection) && __tableName !== props.collection;
 | 
			
		||||
  return (
 | 
			
		||||
    !flag && (
 | 
			
		||||
      <BlockProvider {...props}>
 | 
			
		||||
        <InternalFormBlockProvider {...props} />
 | 
			
		||||
      </BlockProvider>
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,17 @@ import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import { PluginManager } from '../plugin-manager';
 | 
			
		||||
import { ActionContext, SchemaComponent } from '../schema-component';
 | 
			
		||||
import { AddCollectionField, AddFieldAction, ConfigurationTable, EditFieldAction,EditCollectionField } from './Configuration';
 | 
			
		||||
import {
 | 
			
		||||
  AddCollectionField,
 | 
			
		||||
  AddFieldAction,
 | 
			
		||||
  ConfigurationTable,
 | 
			
		||||
  EditFieldAction,
 | 
			
		||||
  EditCollectionField,
 | 
			
		||||
  OverridingFieldAction,
 | 
			
		||||
  OverridingCollectionField,
 | 
			
		||||
  ViewCollectionField,
 | 
			
		||||
  ViewFieldAction,
 | 
			
		||||
} from './Configuration';
 | 
			
		||||
 | 
			
		||||
const schema: ISchema = {
 | 
			
		||||
  type: 'object',
 | 
			
		||||
@ -37,7 +47,20 @@ const schema2: ISchema = {
 | 
			
		||||
export const CollectionManagerPane = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Card bordered={false}>
 | 
			
		||||
      <SchemaComponent schema={schema2} components={{ ConfigurationTable, AddFieldAction, AddCollectionField, EditFieldAction,EditCollectionField }} />
 | 
			
		||||
      <SchemaComponent
 | 
			
		||||
        schema={schema2}
 | 
			
		||||
        components={{
 | 
			
		||||
          ConfigurationTable,
 | 
			
		||||
          AddFieldAction,
 | 
			
		||||
          AddCollectionField,
 | 
			
		||||
          EditFieldAction,
 | 
			
		||||
          EditCollectionField,
 | 
			
		||||
          OverridingCollectionField,
 | 
			
		||||
          OverridingFieldAction,
 | 
			
		||||
          ViewCollectionField,
 | 
			
		||||
          ViewFieldAction,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -68,7 +91,16 @@ export const CollectionManagerShortcut2 = () => {
 | 
			
		||||
          setVisible(true);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
      <SchemaComponent schema={schema} components={{ ConfigurationTable, AddFieldAction, EditFieldAction }} />
 | 
			
		||||
      <SchemaComponent
 | 
			
		||||
        schema={schema}
 | 
			
		||||
        components={{
 | 
			
		||||
          ConfigurationTable,
 | 
			
		||||
          AddFieldAction,
 | 
			
		||||
          EditFieldAction,
 | 
			
		||||
          OverridingFieldAction,
 | 
			
		||||
          ViewFieldAction,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </ActionContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -3,52 +3,24 @@ import { ArrayField, Field } from '@formily/core';
 | 
			
		||||
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
 | 
			
		||||
import { Table, TableColumnProps } from 'antd';
 | 
			
		||||
import { default as classNames } from 'classnames';
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import React, { useState, useRef } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { RecordIndexProvider, RecordProvider, useCollectionManager, useRequest, useSchemaInitializer } from '../..';
 | 
			
		||||
import { findIndex } from 'lodash';
 | 
			
		||||
import {
 | 
			
		||||
  RecordIndexProvider,
 | 
			
		||||
  RecordProvider,
 | 
			
		||||
  useCollectionManager,
 | 
			
		||||
  useRequest,
 | 
			
		||||
  useSchemaInitializer,
 | 
			
		||||
  useRecord,
 | 
			
		||||
  useCompile,
 | 
			
		||||
} from '../..';
 | 
			
		||||
import { overridingSchema } from '../Configuration/schemas/collectionFields';
 | 
			
		||||
 | 
			
		||||
const isColumnComponent = (schema: Schema) => {
 | 
			
		||||
  return schema['x-component']?.endsWith('.Column') > -1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useTableColumns = () => {
 | 
			
		||||
  const field = useField<ArrayField>();
 | 
			
		||||
  const schema = useFieldSchema();
 | 
			
		||||
  const { exists, render } = useSchemaInitializer(schema['x-initializer']);
 | 
			
		||||
  const columns = schema
 | 
			
		||||
    .reduceProperties((buf, s) => {
 | 
			
		||||
      if (isColumnComponent(s)) {
 | 
			
		||||
        return buf.concat([s]);
 | 
			
		||||
      }
 | 
			
		||||
    }, [])
 | 
			
		||||
    .map((s: Schema) => {
 | 
			
		||||
      return {
 | 
			
		||||
        title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
 | 
			
		||||
        dataIndex: s.name,
 | 
			
		||||
        key: s.name,
 | 
			
		||||
        render: (v, record) => {
 | 
			
		||||
          const index = field.value?.indexOf(record);
 | 
			
		||||
          // console.log((Date.now() - start) / 1000);
 | 
			
		||||
          return (
 | 
			
		||||
            <RecordIndexProvider index={index}>
 | 
			
		||||
              <RecordProvider record={record}>
 | 
			
		||||
                <RecursionField schema={s} name={index} onlyRenderProperties />
 | 
			
		||||
              </RecordProvider>
 | 
			
		||||
            </RecordIndexProvider>
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      } as TableColumnProps<any>;
 | 
			
		||||
    });
 | 
			
		||||
  if (!exists) {
 | 
			
		||||
    return columns;
 | 
			
		||||
  }
 | 
			
		||||
  return columns.concat({
 | 
			
		||||
    title: render(),
 | 
			
		||||
    dataIndex: 'TABLE_COLUMN_INITIALIZER',
 | 
			
		||||
    key: 'TABLE_COLUMN_INITIALIZER',
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const components = {
 | 
			
		||||
  body: {
 | 
			
		||||
    row: (props) => {
 | 
			
		||||
@ -93,7 +65,6 @@ const groupColumns = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
type CategorizeKey = 'primaryAndForeignKey' | 'relation' | 'systemInfo' | 'basic';
 | 
			
		||||
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
 | 
			
		||||
const CategorizeKeyNameMap = new Map<CategorizeKey, string>([
 | 
			
		||||
  ['primaryAndForeignKey', 'PK & FK fields'],
 | 
			
		||||
  ['relation', 'Association fields'],
 | 
			
		||||
@ -108,10 +79,13 @@ interface CategorizeDataItem {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
 | 
			
		||||
  const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
 | 
			
		||||
  const field = useField<ArrayField>();
 | 
			
		||||
  const columns = useTableColumns();
 | 
			
		||||
  const { name } = useRecord();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { getInterface } = useCollectionManager();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const { getInterface, getParentCollections, getCollection, getCurrentCollectionFields, getInheritedFields } =
 | 
			
		||||
    useCollectionManager();
 | 
			
		||||
  const {
 | 
			
		||||
    showIndex = true,
 | 
			
		||||
    useSelectedRowKeys = useDef,
 | 
			
		||||
@ -120,12 +94,14 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
 | 
			
		||||
    ...others
 | 
			
		||||
  } = props;
 | 
			
		||||
  const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
 | 
			
		||||
 | 
			
		||||
  const [categorizeData, setCategorizeData] = useState<Array<CategorizeDataItem>>([]);
 | 
			
		||||
  const [expandedKeys, setExpendedKeys] = useState(selectedRowKeys);
 | 
			
		||||
  const inherits = getParentCollections(name);
 | 
			
		||||
  const currentFields = getCurrentCollectionFields(name);
 | 
			
		||||
  useDataSource({
 | 
			
		||||
    onSuccess(data) {
 | 
			
		||||
      field.value = data?.data || [];
 | 
			
		||||
      // categorize field
 | 
			
		||||
      const tmpData: Array<CategorizeDataItem> = [];
 | 
			
		||||
      const categorizeMap = new Map<CategorizeKey, any>();
 | 
			
		||||
      const addCategorizeVal = (categorizeKey: CategorizeKey, val) => {
 | 
			
		||||
        let fieldArr = categorizeMap.get(categorizeKey);
 | 
			
		||||
@ -151,21 +127,103 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
 | 
			
		||||
            addCategorizeVal('basic', item);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      const tmpData: Array<CategorizeDataItem> = [];
 | 
			
		||||
      if (inherits) {
 | 
			
		||||
        inherits.forEach((v) => {
 | 
			
		||||
          sortKeyArr.push(v);
 | 
			
		||||
          const parentCollection = getCollection(v);
 | 
			
		||||
          parentCollection.fields.map((k) => {
 | 
			
		||||
            if (k.interface) {
 | 
			
		||||
              addCategorizeVal(v, new Proxy(k, {}));
 | 
			
		||||
              field.value.push(new Proxy(k, {}));
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      sortKeyArr.forEach((key) => {
 | 
			
		||||
        if (categorizeMap.get(key)?.length > 0) {
 | 
			
		||||
          const parentCollection = getCollection(key);
 | 
			
		||||
          tmpData.push({
 | 
			
		||||
            key,
 | 
			
		||||
            name: t(CategorizeKeyNameMap.get(key)),
 | 
			
		||||
            name:
 | 
			
		||||
              t(CategorizeKeyNameMap.get(key)) ||
 | 
			
		||||
              t(`Parent collection fields`) + `(${compile(parentCollection.title)})`,
 | 
			
		||||
            data: categorizeMap.get(key),
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      setExpendedKeys(sortKeyArr);
 | 
			
		||||
      setCategorizeData(tmpData);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const useTableColumns = () => {
 | 
			
		||||
    const schema = useFieldSchema();
 | 
			
		||||
    const { exists, render } = useSchemaInitializer(schema['x-initializer']);
 | 
			
		||||
    const columns = schema
 | 
			
		||||
      .reduceProperties((buf, s) => {
 | 
			
		||||
        if (isColumnComponent(s)) {
 | 
			
		||||
          return buf.concat([s]);
 | 
			
		||||
        }
 | 
			
		||||
      }, [])
 | 
			
		||||
      .map((s: Schema) => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
 | 
			
		||||
          dataIndex: s.name,
 | 
			
		||||
          key: s.name,
 | 
			
		||||
          render: (v, record) => {
 | 
			
		||||
            const index = findIndex(field.value, record);
 | 
			
		||||
            return (
 | 
			
		||||
              <RecordIndexProvider index={index}>
 | 
			
		||||
                <RecordProvider record={record}>
 | 
			
		||||
                  <RecursionField schema={s} name={index} onlyRenderProperties />
 | 
			
		||||
                </RecordProvider>
 | 
			
		||||
              </RecordIndexProvider>
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        } as TableColumnProps<any>;
 | 
			
		||||
      });
 | 
			
		||||
    if (!exists) {
 | 
			
		||||
      return columns;
 | 
			
		||||
    }
 | 
			
		||||
    return columns.concat({
 | 
			
		||||
      title: render(),
 | 
			
		||||
      dataIndex: 'TABLE_COLUMN_INITIALIZER',
 | 
			
		||||
      key: 'TABLE_COLUMN_INITIALIZER',
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getIsOverriding = (record) => {
 | 
			
		||||
    const flag = currentFields.find((v) => {
 | 
			
		||||
      return v.name === record.name;
 | 
			
		||||
    });
 | 
			
		||||
    return !flag;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const expandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
 | 
			
		||||
    const columns = useTableColumns();
 | 
			
		||||
    if (inherits.includes(record.key)) {
 | 
			
		||||
      columns.pop();
 | 
			
		||||
      columns.push({
 | 
			
		||||
        title: <RecursionField name={'column4'} schema={overridingSchema as Schema} onlyRenderSelf />,
 | 
			
		||||
        dataIndex: 'column4',
 | 
			
		||||
        key: 'column4',
 | 
			
		||||
        render: (v, record) => {
 | 
			
		||||
          const index = findIndex(field.value, record);
 | 
			
		||||
          const flag = getIsOverriding(record);
 | 
			
		||||
          //@ts-ignore
 | 
			
		||||
          overridingSchema.properties.actions.properties.overriding['x-visible'] = flag;
 | 
			
		||||
          return (
 | 
			
		||||
            <RecordIndexProvider index={index}>
 | 
			
		||||
              <RecordProvider record={record}>
 | 
			
		||||
                <RecursionField schema={overridingSchema as Schema} name={index} onlyRenderProperties />
 | 
			
		||||
              </RecordProvider>
 | 
			
		||||
            </RecordIndexProvider>
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    const restProps = {
 | 
			
		||||
    rowSelection: props.rowSelection
 | 
			
		||||
      rowSelection:
 | 
			
		||||
        props.rowSelection && !inherits.includes(record.key)
 | 
			
		||||
          ? {
 | 
			
		||||
              type: 'checkbox',
 | 
			
		||||
              selectedRowKeys,
 | 
			
		||||
@ -176,12 +234,6 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
 | 
			
		||||
            }
 | 
			
		||||
          : undefined,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  const defaultRowKey = (record: any) => {
 | 
			
		||||
    return field.value?.indexOf?.(record);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const expandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <Table
 | 
			
		||||
        {...others}
 | 
			
		||||
@ -211,7 +263,18 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
 | 
			
		||||
        pagination={false}
 | 
			
		||||
        expandable={{
 | 
			
		||||
          expandedRowRender,
 | 
			
		||||
          defaultExpandedRowKeys: sortKeyArr,
 | 
			
		||||
          expandedRowKeys: expandedKeys,
 | 
			
		||||
        }}
 | 
			
		||||
        onExpand={(expanded, record) => {
 | 
			
		||||
          let keys = [];
 | 
			
		||||
          if (expanded) {
 | 
			
		||||
            keys = expandedKeys.concat([record.key]);
 | 
			
		||||
          } else {
 | 
			
		||||
            keys = expandedKeys.filter((v) => {
 | 
			
		||||
              return v !== record.key;
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
          setExpendedKeys(keys);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,19 @@
 | 
			
		||||
import { useForm } from '@formily/react';
 | 
			
		||||
import { action } from '@formily/reactive';
 | 
			
		||||
import { uid } from '@formily/shared';
 | 
			
		||||
import React, { useContext, useEffect, useState } from 'react';
 | 
			
		||||
import React, { useContext, useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { CollectionFieldsTable } from '.';
 | 
			
		||||
import { useRequest } from '../../api-client';
 | 
			
		||||
import { useCurrentDatabase } from '../../database';
 | 
			
		||||
import { useRecord } from '../../record-provider';
 | 
			
		||||
import { SchemaComponent, SchemaComponentContext, useActionContext, useCompile } from '../../schema-component';
 | 
			
		||||
import { useCancelAction, useUpdateCollectionActionAndRefreshCM } from '../action-hooks';
 | 
			
		||||
import { useCollectionManager } from '../hooks/useCollectionManager';
 | 
			
		||||
import { DataSourceContext } from '../sub-table';
 | 
			
		||||
import { AddSubFieldAction } from './AddSubFieldAction';
 | 
			
		||||
import { FieldSummary } from './components/FieldSummary';
 | 
			
		||||
import { EditSubFieldAction } from './EditSubFieldAction';
 | 
			
		||||
import { collectionSchema } from './schemas/collections';
 | 
			
		||||
import { CollectionFieldsTable } from ".";
 | 
			
		||||
 | 
			
		||||
const useAsyncDataSource = (service: any) => (field: any) => {
 | 
			
		||||
  field.loading = true;
 | 
			
		||||
@ -173,9 +175,14 @@ const useNewId = (prefix) => {
 | 
			
		||||
 | 
			
		||||
export const ConfigurationTable = () => {
 | 
			
		||||
  const { collections = [] } = useCollectionManager();
 | 
			
		||||
  const {
 | 
			
		||||
    data: { database },
 | 
			
		||||
  } = useCurrentDatabase();
 | 
			
		||||
  const collectonsRef: any = useRef();
 | 
			
		||||
  collectonsRef.current = collections;
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const loadCollections = async (field: any) => {
 | 
			
		||||
    return collections
 | 
			
		||||
    return collectonsRef.current
 | 
			
		||||
      ?.filter((item) => !(item.autoCreate && item.isThrough))
 | 
			
		||||
      .map((item: any) => ({
 | 
			
		||||
        label: compile(item.title),
 | 
			
		||||
@ -187,14 +194,15 @@ export const ConfigurationTable = () => {
 | 
			
		||||
    <div>
 | 
			
		||||
      <SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
 | 
			
		||||
        <SchemaComponent
 | 
			
		||||
          schema={collectionSchema}
 | 
			
		||||
          schema={collectionSchema(database?.dialect)}
 | 
			
		||||
          components={{
 | 
			
		||||
            AddSubFieldAction,
 | 
			
		||||
            EditSubFieldAction,
 | 
			
		||||
            FieldSummary,
 | 
			
		||||
            CollectionFieldsTable
 | 
			
		||||
            CollectionFieldsTable,
 | 
			
		||||
          }}
 | 
			
		||||
          scope={{
 | 
			
		||||
            enableInherits: database?.dialect === 'postgres', 
 | 
			
		||||
            useDestroySubField,
 | 
			
		||||
            useBulkDestroySubField,
 | 
			
		||||
            useSelectedRowKeys,
 | 
			
		||||
@ -203,6 +211,8 @@ export const ConfigurationTable = () => {
 | 
			
		||||
            loadCollections,
 | 
			
		||||
            useCurrentFields,
 | 
			
		||||
            useNewId,
 | 
			
		||||
            useCancelAction,
 | 
			
		||||
            useUpdateCollectionActionAndRefreshCM,
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </SchemaComponentContext.Provider>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,180 @@
 | 
			
		||||
import { ArrayTable } from '@formily/antd';
 | 
			
		||||
import { ISchema, useForm } from '@formily/react';
 | 
			
		||||
import { uid } from '@formily/shared';
 | 
			
		||||
import cloneDeep from 'lodash/cloneDeep';
 | 
			
		||||
import set from 'lodash/set';
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useAPIClient, useRequest } from '../../api-client';
 | 
			
		||||
import { useRecord, RecordProvider } from '../../record-provider';
 | 
			
		||||
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
 | 
			
		||||
import { useCancelAction } from '../action-hooks';
 | 
			
		||||
import { useCollectionManager } from '../hooks';
 | 
			
		||||
import { IField } from '../interfaces/types';
 | 
			
		||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
 | 
			
		||||
import * as components from './components';
 | 
			
		||||
 | 
			
		||||
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
 | 
			
		||||
  if (!schema) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const properties = cloneDeep(schema.properties) as any;
 | 
			
		||||
  properties.name['x-disabled'] = true;
 | 
			
		||||
 | 
			
		||||
  if (schema.hasDefaultValue === true) {
 | 
			
		||||
    properties['defaultValue'] = cloneDeep(schema.default.uiSchema);
 | 
			
		||||
    properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
 | 
			
		||||
    properties['defaultValue']['x-decorator'] = 'FormItem';
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    type: 'object',
 | 
			
		||||
    properties: {
 | 
			
		||||
      [uid()]: {
 | 
			
		||||
        type: 'void',
 | 
			
		||||
        'x-component': 'Action.Drawer',
 | 
			
		||||
        'x-decorator': 'Form',
 | 
			
		||||
        'x-decorator-props': {
 | 
			
		||||
          useValues(options) {
 | 
			
		||||
            return useRequest(
 | 
			
		||||
              () =>
 | 
			
		||||
                Promise.resolve({
 | 
			
		||||
                  data: cloneDeep(schema.default),
 | 
			
		||||
                }),
 | 
			
		||||
              options,
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        title: `${compile(record.__parent?.title)} - ${compile('{{ t("Override field") }}')}`,
 | 
			
		||||
        properties: {
 | 
			
		||||
          summary: {
 | 
			
		||||
            type: 'void',
 | 
			
		||||
            'x-component': 'FieldSummary',
 | 
			
		||||
            'x-component-props': {
 | 
			
		||||
              schemaKey: schema.name,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
          ...properties,
 | 
			
		||||
          footer: {
 | 
			
		||||
            type: 'void',
 | 
			
		||||
            'x-component': 'Action.Drawer.Footer',
 | 
			
		||||
            properties: {
 | 
			
		||||
              action1: {
 | 
			
		||||
                title: '{{ t("Cancel") }}',
 | 
			
		||||
                'x-component': 'Action',
 | 
			
		||||
                'x-component-props': {
 | 
			
		||||
                  useAction: '{{ useCancelAction }}',
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
              action2: {
 | 
			
		||||
                title: '{{ t("Submit") }}',
 | 
			
		||||
                'x-component': 'Action',
 | 
			
		||||
                'x-component-props': {
 | 
			
		||||
                  type: 'primary',
 | 
			
		||||
                  useAction: '{{ useOverridingCollectionField }}',
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useOverridingCollectionField = () => {
 | 
			
		||||
  const form = useForm();
 | 
			
		||||
  const { refreshCM } = useCollectionManager();
 | 
			
		||||
  const ctx = useActionContext();
 | 
			
		||||
  const { refresh } = useResourceActionContext();
 | 
			
		||||
  const { resource } = useResourceContext();
 | 
			
		||||
  return {
 | 
			
		||||
    async run() {
 | 
			
		||||
      await form.submit();
 | 
			
		||||
      const values = cloneDeep(form.values);
 | 
			
		||||
      if (values.autoCreateReverseField) {
 | 
			
		||||
      } else {
 | 
			
		||||
        delete values.reverseField;
 | 
			
		||||
      }
 | 
			
		||||
      delete values.autoCreateReverseField;
 | 
			
		||||
      const { uiSchema } = values;
 | 
			
		||||
      delete values.collectionName;
 | 
			
		||||
      delete values.key;
 | 
			
		||||
      await resource.create({
 | 
			
		||||
        values: {
 | 
			
		||||
          ...values,
 | 
			
		||||
          uiSchema: { title: uiSchema.title, type: uiSchema.type, 'x-component': uiSchema['x-component'] },
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
      ctx.setVisible(false);
 | 
			
		||||
      await form.reset();
 | 
			
		||||
      refresh();
 | 
			
		||||
      await refreshCM();
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const OverridingCollectionField = (props) => {
 | 
			
		||||
  const record = useRecord();
 | 
			
		||||
  return <OverridingFieldAction item={record} {...props} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const OverridingFieldAction = (props) => {
 | 
			
		||||
  const { scope, getContainer, item: record, children } = props;
 | 
			
		||||
  const { getInterface } = useCollectionManager();
 | 
			
		||||
  const [visible, setVisible] = useState(false);
 | 
			
		||||
  const [schema, setSchema] = useState({});
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const [data, setData] = useState<any>({});
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <RecordProvider record={record}>
 | 
			
		||||
      <ActionContext.Provider value={{ visible, setVisible }}>
 | 
			
		||||
        <a
 | 
			
		||||
          onClick={async () => {
 | 
			
		||||
            const { data } = await api.resource('collections.fields', record.collectionName).get({
 | 
			
		||||
              filterByTk: record.name,
 | 
			
		||||
              appends: ['uiSchema', 'reverseField'],
 | 
			
		||||
            });
 | 
			
		||||
            setData(data?.data);
 | 
			
		||||
            const interfaceConf = getInterface(record.interface);
 | 
			
		||||
            const defaultValues: any = cloneDeep(data?.data) || {};
 | 
			
		||||
            if (!defaultValues?.reverseField) {
 | 
			
		||||
              defaultValues.autoCreateReverseField = false;
 | 
			
		||||
              defaultValues.reverseField = interfaceConf.default?.reverseField;
 | 
			
		||||
              set(defaultValues.reverseField, 'name', `f_${uid()}`);
 | 
			
		||||
              set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title);
 | 
			
		||||
            }
 | 
			
		||||
            const schema = getSchema(
 | 
			
		||||
              {
 | 
			
		||||
                ...interfaceConf,
 | 
			
		||||
                default: defaultValues,
 | 
			
		||||
              },
 | 
			
		||||
              record,
 | 
			
		||||
              compile,
 | 
			
		||||
              getContainer,
 | 
			
		||||
            );
 | 
			
		||||
            setSchema(schema);
 | 
			
		||||
            setVisible(true);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {children || t('Override')}
 | 
			
		||||
        </a>
 | 
			
		||||
        <SchemaComponent
 | 
			
		||||
          schema={schema}
 | 
			
		||||
          components={{ ...components, ArrayTable }}
 | 
			
		||||
          scope={{
 | 
			
		||||
            getContainer,
 | 
			
		||||
            useOverridingCollectionField,
 | 
			
		||||
            useCancelAction,
 | 
			
		||||
            showReverseFieldConfig: !data?.reverseField,
 | 
			
		||||
            createOnly: true,
 | 
			
		||||
            ...scope,
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </ActionContext.Provider>
 | 
			
		||||
    </RecordProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,179 @@
 | 
			
		||||
import { ArrayTable } from '@formily/antd';
 | 
			
		||||
import { ISchema, useForm } from '@formily/react';
 | 
			
		||||
import { uid } from '@formily/shared';
 | 
			
		||||
import cloneDeep from 'lodash/cloneDeep';
 | 
			
		||||
import set from 'lodash/set';
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useAPIClient, useRequest } from '../../api-client';
 | 
			
		||||
import { useRecord, RecordProvider } from '../../record-provider';
 | 
			
		||||
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
 | 
			
		||||
import { useCancelAction } from '../action-hooks';
 | 
			
		||||
import { useCollectionManager } from '../hooks';
 | 
			
		||||
import { IField } from '../interfaces/types';
 | 
			
		||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
 | 
			
		||||
import * as components from './components';
 | 
			
		||||
 | 
			
		||||
const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => {
 | 
			
		||||
  if (!schema) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const properties = cloneDeep(schema.properties) as any;
 | 
			
		||||
  properties.name['x-disabled'] = true;
 | 
			
		||||
  if (schema.hasDefaultValue === true) {
 | 
			
		||||
    properties['defaultValue'] = cloneDeep(schema.default.uiSchema);
 | 
			
		||||
    properties['defaultValue']['title'] = compile('{{ t("Default value") }}');
 | 
			
		||||
    properties['defaultValue']['x-decorator'] = 'FormItem';
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    type: 'object',
 | 
			
		||||
    properties: {
 | 
			
		||||
      [uid()]: {
 | 
			
		||||
        type: 'void',
 | 
			
		||||
        'x-component': 'Action.Drawer',
 | 
			
		||||
        'x-decorator': 'Form',
 | 
			
		||||
        'x-decorator-props': {
 | 
			
		||||
          useValues(options) {
 | 
			
		||||
            return useRequest(
 | 
			
		||||
              () =>
 | 
			
		||||
                Promise.resolve({
 | 
			
		||||
                  data: cloneDeep(schema.default),
 | 
			
		||||
                }),
 | 
			
		||||
              options,
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        title: `${compile(record.__parent?.title)} - ${compile('{{ t("View") }}')}`,
 | 
			
		||||
        properties: {
 | 
			
		||||
          summary: {
 | 
			
		||||
            type: 'void',
 | 
			
		||||
            'x-component': 'FieldSummary',
 | 
			
		||||
            'x-component-props': {
 | 
			
		||||
              schemaKey: schema.name,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
          ...properties,
 | 
			
		||||
        //   footer: {
 | 
			
		||||
        //     type: 'void',
 | 
			
		||||
        //     'x-component': 'Action.Drawer.Footer',
 | 
			
		||||
        //     properties: {
 | 
			
		||||
        //       action1: {
 | 
			
		||||
        //         title: '{{ t("Cancel") }}',
 | 
			
		||||
        //         'x-component': 'Action',
 | 
			
		||||
        //         'x-component-props': {
 | 
			
		||||
        //           useAction: '{{ useCancelAction }}',
 | 
			
		||||
        //         },
 | 
			
		||||
        //       },
 | 
			
		||||
        //       action2: {
 | 
			
		||||
        //         title: '{{ t("Submit") }}',
 | 
			
		||||
        //         'x-component': 'Action',
 | 
			
		||||
        //         'x-component-props': {
 | 
			
		||||
        //           type: 'primary',
 | 
			
		||||
        //           useAction: '{{ useVieInheritedCollectionField }}',
 | 
			
		||||
        //         },
 | 
			
		||||
        //       },
 | 
			
		||||
        //     },
 | 
			
		||||
        //   },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useVieInheritedCollectionField = () => {
 | 
			
		||||
  const form = useForm();
 | 
			
		||||
  const { refreshCM } = useCollectionManager();
 | 
			
		||||
  const ctx = useActionContext();
 | 
			
		||||
  const { refresh } = useResourceActionContext();
 | 
			
		||||
  const { resource } = useResourceContext();
 | 
			
		||||
  return {
 | 
			
		||||
    async run() {
 | 
			
		||||
      await form.submit();
 | 
			
		||||
      const values = cloneDeep(form.values);
 | 
			
		||||
      if (values.autoCreateReverseField) {
 | 
			
		||||
      } else {
 | 
			
		||||
        delete values.reverseField;
 | 
			
		||||
      }
 | 
			
		||||
      delete values.autoCreateReverseField;
 | 
			
		||||
      const { uiSchema } = values;
 | 
			
		||||
      delete values.collectionName;
 | 
			
		||||
      delete values.key;
 | 
			
		||||
      await resource.create({
 | 
			
		||||
        values: {
 | 
			
		||||
          ...values,
 | 
			
		||||
          uiSchema: { title: uiSchema.title, type: uiSchema.type, 'x-component': uiSchema['x-component'] },
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
      ctx.setVisible(false);
 | 
			
		||||
      await form.reset();
 | 
			
		||||
      refresh();
 | 
			
		||||
      await refreshCM();
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ViewCollectionField = (props) => {
 | 
			
		||||
  const record = useRecord();
 | 
			
		||||
  return <ViewFieldAction item={record} {...props} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ViewFieldAction = (props) => {
 | 
			
		||||
  const { scope, getContainer, item: record, children } = props;
 | 
			
		||||
  const { getInterface } = useCollectionManager();
 | 
			
		||||
  const [visible, setVisible] = useState(false);
 | 
			
		||||
  const [schema, setSchema] = useState({});
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const [data, setData] = useState<any>({});
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <RecordProvider record={record}>
 | 
			
		||||
      <ActionContext.Provider value={{ visible, setVisible }}>
 | 
			
		||||
        <a
 | 
			
		||||
          onClick={async () => {
 | 
			
		||||
            const { data } = await api.resource('collections.fields', record.collectionName).get({
 | 
			
		||||
              filterByTk: record.name,
 | 
			
		||||
              appends: ['uiSchema', 'reverseField'],
 | 
			
		||||
            });
 | 
			
		||||
            setData(data?.data);
 | 
			
		||||
            const interfaceConf = getInterface(record.interface);
 | 
			
		||||
            const defaultValues: any = cloneDeep(data?.data) || {};
 | 
			
		||||
            if (!defaultValues?.reverseField) {
 | 
			
		||||
              defaultValues.autoCreateReverseField = false;
 | 
			
		||||
              defaultValues.reverseField = interfaceConf.default?.reverseField;
 | 
			
		||||
              set(defaultValues.reverseField, 'name', `f_${uid()}`);
 | 
			
		||||
              set(defaultValues.reverseField, 'uiSchema.title', record.__parent.title);
 | 
			
		||||
            }
 | 
			
		||||
            const schema = getSchema(
 | 
			
		||||
              {
 | 
			
		||||
                ...interfaceConf,
 | 
			
		||||
                default: defaultValues,
 | 
			
		||||
              },
 | 
			
		||||
              record,
 | 
			
		||||
              compile,
 | 
			
		||||
              getContainer,
 | 
			
		||||
            );
 | 
			
		||||
            setSchema(schema);
 | 
			
		||||
            setVisible(true);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {children || t('View')}
 | 
			
		||||
        </a>
 | 
			
		||||
        <SchemaComponent
 | 
			
		||||
          schema={schema}
 | 
			
		||||
          components={{ ...components, ArrayTable }}
 | 
			
		||||
          scope={{
 | 
			
		||||
            getContainer,
 | 
			
		||||
            useVieInheritedCollectionField,
 | 
			
		||||
            useCancelAction,
 | 
			
		||||
            showReverseFieldConfig: !data?.reverseField,
 | 
			
		||||
            createOnly: !true,
 | 
			
		||||
            ...scope,
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </ActionContext.Provider>
 | 
			
		||||
    </RecordProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -7,6 +7,9 @@ export * from './EditFieldAction';
 | 
			
		||||
export * from './interfaces';
 | 
			
		||||
export * from './components';
 | 
			
		||||
export * from './CollectionFieldsTable';
 | 
			
		||||
export * from './schemas/collections'
 | 
			
		||||
export * from './OverridingCollectionField'
 | 
			
		||||
export * from './ViewInheritedField'
 | 
			
		||||
 | 
			
		||||
registerValidateFormats({
 | 
			
		||||
  uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,
 | 
			
		||||
 | 
			
		||||
@ -80,10 +80,6 @@ export const collectionFieldSchema: ISchema = {
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  // 'x-component': 'CollectionProvider',
 | 
			
		||||
  // 'x-component-props': {
 | 
			
		||||
  //   collection,
 | 
			
		||||
  // },
 | 
			
		||||
  properties: {
 | 
			
		||||
    summary: {
 | 
			
		||||
      type: 'void',
 | 
			
		||||
@ -206,3 +202,37 @@ export const collectionFieldSchema: ISchema = {
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const overridingSchema: ISchema = {
 | 
			
		||||
  type: 'void',
 | 
			
		||||
  title: '{{ t("Actions") }}',
 | 
			
		||||
  'x-component': 'Table.Column',
 | 
			
		||||
  properties: {
 | 
			
		||||
    actions: {
 | 
			
		||||
      type: 'void',
 | 
			
		||||
      'x-component': 'Space',
 | 
			
		||||
      'x-component-props': {
 | 
			
		||||
        split: '|',
 | 
			
		||||
      },
 | 
			
		||||
      properties: {
 | 
			
		||||
        overriding: {
 | 
			
		||||
          type: 'void',
 | 
			
		||||
          title: '{{ t("Overriding") }}',
 | 
			
		||||
          'x-component': 'OverridingCollectionField',
 | 
			
		||||
          'x-component-props': {
 | 
			
		||||
            type: 'primary',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        view:{
 | 
			
		||||
          type: 'void',
 | 
			
		||||
          title: '{{ t("View") }}',
 | 
			
		||||
          'x-component': 'ViewCollectionField',
 | 
			
		||||
          'x-component-props': {
 | 
			
		||||
            type: 'primary',
 | 
			
		||||
          },
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,8 @@ const compile = (source) => {
 | 
			
		||||
  return Schema.compile(source, { t: i18n.t });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const collection: CollectionOptions = {
 | 
			
		||||
export const getCollectionOptions = (database): CollectionOptions => {
 | 
			
		||||
  return {
 | 
			
		||||
    name: 'collections',
 | 
			
		||||
    filterTargetKey: 'name',
 | 
			
		||||
    targetKey: 'name',
 | 
			
		||||
@ -44,10 +45,103 @@ export const collection: CollectionOptions = {
 | 
			
		||||
        targetKey: 'name',
 | 
			
		||||
        uiSchema: {},
 | 
			
		||||
      },
 | 
			
		||||
      database === 'postgres' && {
 | 
			
		||||
        type: 'hasMany',
 | 
			
		||||
        name: 'inherits',
 | 
			
		||||
        interface: 'select',
 | 
			
		||||
        uiSchema: {
 | 
			
		||||
          title: '{{ t("Inherits") }}',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
          'x-component': 'Select',
 | 
			
		||||
          'x-component-props': {
 | 
			
		||||
            mode: 'multiple',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const collectionSchema: ISchema = {
 | 
			
		||||
export const createCollectionProperties = {
 | 
			
		||||
  title: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  name: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
    'x-validator': 'uid',
 | 
			
		||||
  },
 | 
			
		||||
  inherits: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
    'x-visible': '{{ enableInherits }}',
 | 
			
		||||
    'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
 | 
			
		||||
  },
 | 
			
		||||
  footer: {
 | 
			
		||||
    type: 'void',
 | 
			
		||||
    'x-component': 'Action.Drawer.Footer',
 | 
			
		||||
    properties: {
 | 
			
		||||
      action1: {
 | 
			
		||||
        title: '{{ t("Cancel") }}',
 | 
			
		||||
        'x-component': 'Action',
 | 
			
		||||
        'x-component-props': {
 | 
			
		||||
          useAction: '{{ cm.useCancelAction }}',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      action2: {
 | 
			
		||||
        title: '{{ t("Submit") }}',
 | 
			
		||||
        'x-component': 'Action',
 | 
			
		||||
        'x-component-props': {
 | 
			
		||||
          type: 'primary',
 | 
			
		||||
          useAction: '{{ cm.useCreateActionAndRefreshCM }}',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const editCollectionProperties = {
 | 
			
		||||
  title: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
  },
 | 
			
		||||
  name: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
    'x-disabled': true,
 | 
			
		||||
  },
 | 
			
		||||
  inherits: {
 | 
			
		||||
    'x-component': 'CollectionField',
 | 
			
		||||
    'x-decorator': 'FormItem',
 | 
			
		||||
    'x-disabled': true,
 | 
			
		||||
    'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
 | 
			
		||||
  },
 | 
			
		||||
  footer: {
 | 
			
		||||
    type: 'void',
 | 
			
		||||
    'x-component': 'Action.Drawer.Footer',
 | 
			
		||||
    properties: {
 | 
			
		||||
      action1: {
 | 
			
		||||
        title: '{{ t("Cancel") }}',
 | 
			
		||||
        'x-component': 'Action',
 | 
			
		||||
        'x-component-props': {
 | 
			
		||||
          useAction: '{{ useCancelAction }}',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      action2: {
 | 
			
		||||
        title: '{{ t("Submit") }}',
 | 
			
		||||
        'x-component': 'Action',
 | 
			
		||||
        'x-component-props': {
 | 
			
		||||
          type: 'primary',
 | 
			
		||||
          useAction: '{{ useUpdateCollectionActionAndRefreshCM }}',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const collectionSchema = (database): ISchema => {
 | 
			
		||||
  return {
 | 
			
		||||
    type: 'object',
 | 
			
		||||
    properties: {
 | 
			
		||||
      block1: {
 | 
			
		||||
@ -55,7 +149,7 @@ export const collectionSchema: ISchema = {
 | 
			
		||||
        'x-collection': 'collections',
 | 
			
		||||
        'x-decorator': 'ResourceActionProvider',
 | 
			
		||||
        'x-decorator-props': {
 | 
			
		||||
        collection,
 | 
			
		||||
          collection: getCollectionOptions(database),
 | 
			
		||||
          request: {
 | 
			
		||||
            resource: 'collections',
 | 
			
		||||
            action: 'list',
 | 
			
		||||
@ -125,38 +219,7 @@ export const collectionSchema: ISchema = {
 | 
			
		||||
                    'x-decorator-props': {
 | 
			
		||||
                      useValues: '{{ useCollectionValues }}',
 | 
			
		||||
                    },
 | 
			
		||||
                  properties: {
 | 
			
		||||
                    title: {
 | 
			
		||||
                      'x-component': 'CollectionField',
 | 
			
		||||
                      'x-decorator': 'FormItem',
 | 
			
		||||
                    },
 | 
			
		||||
                    name: {
 | 
			
		||||
                      'x-component': 'CollectionField',
 | 
			
		||||
                      'x-decorator': 'FormItem',
 | 
			
		||||
                      'x-validator': 'uid',
 | 
			
		||||
                    },
 | 
			
		||||
                    footer: {
 | 
			
		||||
                      type: 'void',
 | 
			
		||||
                      'x-component': 'Action.Drawer.Footer',
 | 
			
		||||
                      properties: {
 | 
			
		||||
                        action1: {
 | 
			
		||||
                          title: '{{ t("Cancel") }}',
 | 
			
		||||
                          'x-component': 'Action',
 | 
			
		||||
                          'x-component-props': {
 | 
			
		||||
                            useAction: '{{ cm.useCancelAction }}',
 | 
			
		||||
                          },
 | 
			
		||||
                        },
 | 
			
		||||
                        action2: {
 | 
			
		||||
                          title: '{{ t("Submit") }}',
 | 
			
		||||
                          'x-component': 'Action',
 | 
			
		||||
                          'x-component-props': {
 | 
			
		||||
                            type: 'primary',
 | 
			
		||||
                            useAction: '{{ cm.useCreateActionAndRefreshCM }}',
 | 
			
		||||
                          },
 | 
			
		||||
                        },
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                    properties: createCollectionProperties,
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
@ -218,6 +281,9 @@ export const collectionSchema: ISchema = {
 | 
			
		||||
                          drawer: {
 | 
			
		||||
                            type: 'void',
 | 
			
		||||
                            'x-component': 'Action.Drawer',
 | 
			
		||||
                            'x-component-props': {
 | 
			
		||||
                              destroyOnClose: true,
 | 
			
		||||
                            },
 | 
			
		||||
                            'x-reactions': (field) => {
 | 
			
		||||
                              const i = field.path.segments[1];
 | 
			
		||||
                              const table = field.form.getValuesIn(`table.${i}`);
 | 
			
		||||
@ -247,38 +313,7 @@ export const collectionSchema: ISchema = {
 | 
			
		||||
                              useValues: '{{ cm.useValuesFromRecord }}',
 | 
			
		||||
                            },
 | 
			
		||||
                            title: '{{ t("Edit collection") }}',
 | 
			
		||||
                          properties: {
 | 
			
		||||
                            title: {
 | 
			
		||||
                              'x-component': 'CollectionField',
 | 
			
		||||
                              'x-decorator': 'FormItem',
 | 
			
		||||
                            },
 | 
			
		||||
                            name: {
 | 
			
		||||
                              'x-component': 'CollectionField',
 | 
			
		||||
                              'x-decorator': 'FormItem',
 | 
			
		||||
                              'x-disabled': true,
 | 
			
		||||
                            },
 | 
			
		||||
                            footer: {
 | 
			
		||||
                              type: 'void',
 | 
			
		||||
                              'x-component': 'Action.Drawer.Footer',
 | 
			
		||||
                              properties: {
 | 
			
		||||
                                action1: {
 | 
			
		||||
                                  title: '{{ t("Cancel") }}',
 | 
			
		||||
                                  'x-component': 'Action',
 | 
			
		||||
                                  'x-component-props': {
 | 
			
		||||
                                    useAction: '{{ cm.useCancelAction }}',
 | 
			
		||||
                                  },
 | 
			
		||||
                                },
 | 
			
		||||
                                action2: {
 | 
			
		||||
                                  title: '{{ t("Submit") }}',
 | 
			
		||||
                                  'x-component': 'Action',
 | 
			
		||||
                                  'x-component-props': {
 | 
			
		||||
                                    type: 'primary',
 | 
			
		||||
                                    useAction: '{{ cm.useUpdateCollectionActionAndRefreshCM }}',
 | 
			
		||||
                                  },
 | 
			
		||||
                                },
 | 
			
		||||
                              },
 | 
			
		||||
                            },
 | 
			
		||||
                          },
 | 
			
		||||
                            properties: editCollectionProperties,
 | 
			
		||||
                          },
 | 
			
		||||
                        },
 | 
			
		||||
                      },
 | 
			
		||||
@ -304,3 +339,4 @@ export const collectionSchema: ISchema = {
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,38 @@
 | 
			
		||||
import { SchemaKey } from '@formily/react';
 | 
			
		||||
import { reduce ,unionBy} from 'lodash';
 | 
			
		||||
import { useContext } from 'react';
 | 
			
		||||
import { useAPIClient } from '../../api-client';
 | 
			
		||||
import { CollectionContext } from '../context';
 | 
			
		||||
import { CollectionFieldOptions } from '../types';
 | 
			
		||||
import { useCollectionManager } from './useCollectionManager';
 | 
			
		||||
 | 
			
		||||
export const useCollection = () => {
 | 
			
		||||
  const collection = useContext(CollectionContext);
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
  const resource = api?.resource(collection?.name);
 | 
			
		||||
  const { getParentCollections, getCurrentCollectionFields } = useCollectionManager();
 | 
			
		||||
  const currentFields = collection.fields;
 | 
			
		||||
  const inheritKeys = getParentCollections(collection.name);
 | 
			
		||||
  const inheritedFields = reduce(
 | 
			
		||||
    inheritKeys,
 | 
			
		||||
    (result, value) => {
 | 
			
		||||
      const arr = result;
 | 
			
		||||
      return arr.concat(getCurrentCollectionFields(value));
 | 
			
		||||
    },
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
  const totalFields = unionBy(currentFields?.concat(inheritedFields),'name').filter((v)=>{
 | 
			
		||||
    return !v.isForeignKey
 | 
			
		||||
  });
 | 
			
		||||
  return {
 | 
			
		||||
    ...collection,
 | 
			
		||||
    resource,
 | 
			
		||||
    getField(name: SchemaKey): CollectionFieldOptions {
 | 
			
		||||
      return collection?.fields?.find((field) => field.name === name);
 | 
			
		||||
      const fields = totalFields;
 | 
			
		||||
      return fields?.find((field) => field.name === name);
 | 
			
		||||
    },
 | 
			
		||||
    fields: totalFields,
 | 
			
		||||
    currentFields,
 | 
			
		||||
    inheritedFields
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,32 @@
 | 
			
		||||
import { clone } from '@formily/shared';
 | 
			
		||||
import { useContext } from 'react';
 | 
			
		||||
import { reduce, unionBy,uniq } from 'lodash';
 | 
			
		||||
import { CollectionManagerContext } from '../context';
 | 
			
		||||
import { CollectionFieldOptions } from '../types';
 | 
			
		||||
 | 
			
		||||
export const useCollectionManager = () => {
 | 
			
		||||
  const { refreshCM, service, interfaces, collections } = useContext(CollectionManagerContext);
 | 
			
		||||
  const getInheritedFields = (name) => {
 | 
			
		||||
    const inheritKeys = getParentCollections(name);
 | 
			
		||||
    const inheritedFields = reduce(
 | 
			
		||||
      inheritKeys,
 | 
			
		||||
      (result, value) => {
 | 
			
		||||
        const arr = result;
 | 
			
		||||
        return arr.concat(collections?.find((collection) => collection.name === value)?.fields);
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
    return inheritedFields;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getCollectionFields = (name: string): CollectionFieldOptions[] => {
 | 
			
		||||
    const currentFields = collections?.find((collection) => collection.name === name)?.fields;
 | 
			
		||||
    const inheritedFields = getInheritedFields(name);
 | 
			
		||||
    const totalFields = unionBy(currentFields?.concat(inheritedFields) || [], 'name').filter((v:any) => {
 | 
			
		||||
      return !v.isForeignKey;
 | 
			
		||||
    });
 | 
			
		||||
    return totalFields;
 | 
			
		||||
  };
 | 
			
		||||
  const getCollectionField = (name: string) => {
 | 
			
		||||
    const [collectionName, fieldName] = name.split('.');
 | 
			
		||||
    if (!fieldName) {
 | 
			
		||||
@ -13,27 +36,68 @@ export const useCollectionManager = () => {
 | 
			
		||||
    if (!collection) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    return collection?.fields?.find((field) => field.name === fieldName);
 | 
			
		||||
    return getCollectionFields(collectionName)?.find((field) => field.name === fieldName);
 | 
			
		||||
  };
 | 
			
		||||
  const getParentCollections = (name) => {
 | 
			
		||||
    const parents = [];
 | 
			
		||||
    const getParents = (name) => {
 | 
			
		||||
      const collection = collections?.find((collection) => collection.name === name);
 | 
			
		||||
      if (collection) {
 | 
			
		||||
        const { inherits } = collection;
 | 
			
		||||
        if (inherits) {
 | 
			
		||||
          for (let index = 0; index < inherits.length; index++) {
 | 
			
		||||
            const collectionKey = inherits[index];
 | 
			
		||||
            parents.push(collectionKey);
 | 
			
		||||
            getParents(collectionKey);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return uniq(parents);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return getParents(name);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getChildrenCollections = (name) => {
 | 
			
		||||
    const childrens = [];
 | 
			
		||||
    const getChildrens = (name) => {
 | 
			
		||||
      const inheritCollections = collections.filter((v) => {
 | 
			
		||||
        return v.inherits?.includes(name);
 | 
			
		||||
      });
 | 
			
		||||
      inheritCollections.forEach((v) => {
 | 
			
		||||
        const collectionKey = v.name;
 | 
			
		||||
        childrens.push(v);
 | 
			
		||||
        return getChildrens(collectionKey);
 | 
			
		||||
      });
 | 
			
		||||
      return childrens;
 | 
			
		||||
    };
 | 
			
		||||
    return getChildrens(name);
 | 
			
		||||
  };
 | 
			
		||||
  const getCurrentCollectionFields = (name: string) => {
 | 
			
		||||
    const collection = collections?.find((collection) => collection.name === name);
 | 
			
		||||
    return collection?.fields || [];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    service,
 | 
			
		||||
    interfaces,
 | 
			
		||||
    collections,
 | 
			
		||||
    getParentCollections,
 | 
			
		||||
    getChildrenCollections,
 | 
			
		||||
    refreshCM: () => refreshCM?.(),
 | 
			
		||||
    get(name: string) {
 | 
			
		||||
      return collections?.find((collection) => collection.name === name);
 | 
			
		||||
    },
 | 
			
		||||
    getInheritedFields,
 | 
			
		||||
    getCollectionField,
 | 
			
		||||
    getCollectionFields,
 | 
			
		||||
    getCurrentCollectionFields,
 | 
			
		||||
    getCollection(name: any) {
 | 
			
		||||
      if (typeof name !== 'string') {
 | 
			
		||||
        return name;
 | 
			
		||||
      }
 | 
			
		||||
      return collections?.find((collection) => collection.name === name);
 | 
			
		||||
    },
 | 
			
		||||
    getCollectionFields(name: string) {
 | 
			
		||||
      const collection = collections?.find((collection) => collection.name === name);
 | 
			
		||||
      return collection?.fields || [];
 | 
			
		||||
    },
 | 
			
		||||
    getCollectionField,
 | 
			
		||||
    getCollectionJoinField(name: string) {
 | 
			
		||||
      if (!name) {
 | 
			
		||||
        return;
 | 
			
		||||
@ -58,5 +122,23 @@ export const useCollectionManager = () => {
 | 
			
		||||
    getInterface(name: string) {
 | 
			
		||||
      return interfaces[name] ? clone(interfaces[name]) : null;
 | 
			
		||||
    },
 | 
			
		||||
    getParentCollectionFields: (parentCollection, currentCollection) => {
 | 
			
		||||
      const currentFields = collections?.find((collection) => collection.name === currentCollection)?.fields;
 | 
			
		||||
      const parentFields = collections?.find((collection) => collection.name === parentCollection)?.fields;
 | 
			
		||||
      const inheritKeys = getParentCollections(currentCollection);
 | 
			
		||||
      const index = inheritKeys.indexOf(parentCollection);
 | 
			
		||||
      let filterFields = currentFields;
 | 
			
		||||
      if (index > 0) {
 | 
			
		||||
        inheritKeys.splice(index);
 | 
			
		||||
        inheritKeys.forEach((v) => {
 | 
			
		||||
          filterFields = filterFields.concat(getCurrentCollectionFields(v));
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return parentFields.filter((v) => {
 | 
			
		||||
        return !filterFields.find((k) => {
 | 
			
		||||
          return k.name === v.name;
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ export interface CollectionOptions {
 | 
			
		||||
  targetKey?: string;
 | 
			
		||||
  sortable?: any;
 | 
			
		||||
  fields?: FieldOptions[];
 | 
			
		||||
  inherits?:string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ICollectionProviderProps {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
import React, { createContext,  useContext } from 'react';
 | 
			
		||||
import { Spin } from 'antd';
 | 
			
		||||
import { useRequest } from '../api-client';
 | 
			
		||||
 | 
			
		||||
export const CurrentDatabaseContext = createContext(null);
 | 
			
		||||
 | 
			
		||||
export const useCurrentDatabase = () => {
 | 
			
		||||
  return useContext(CurrentDatabaseContext);
 | 
			
		||||
};
 | 
			
		||||
export const CurrentDatabaseProvider = (props) => {
 | 
			
		||||
    const result = useRequest({
 | 
			
		||||
        url: 'app:getInfo',
 | 
			
		||||
      });
 | 
			
		||||
      if (result.loading) {
 | 
			
		||||
        return <Spin />;
 | 
			
		||||
      }
 | 
			
		||||
  return (
 | 
			
		||||
    <CurrentDatabaseContext.Provider
 | 
			
		||||
      value={result.data}
 | 
			
		||||
    >
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </CurrentDatabaseContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1
									
								
								packages/core/client/src/database/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/core/client/src/database/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
export * from './CurrentDatabaseProvider';
 | 
			
		||||
@ -25,4 +25,5 @@ export * from './schema-templates';
 | 
			
		||||
export * from './settings-form';
 | 
			
		||||
export * from './system-settings';
 | 
			
		||||
export * from './user';
 | 
			
		||||
export * from './database'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -113,6 +113,7 @@ export default {
 | 
			
		||||
  "Create collection": "Create collection",
 | 
			
		||||
  "Collection display name": "Collection display name",
 | 
			
		||||
  "Collection name": "Collection name",
 | 
			
		||||
  "Inherits":"Inherits",
 | 
			
		||||
  "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
 | 
			
		||||
  "Storage type": "Storage type",
 | 
			
		||||
  "Edit": "Edit",
 | 
			
		||||
@ -120,9 +121,12 @@ export default {
 | 
			
		||||
  "Configure fields": "Configure fields",
 | 
			
		||||
  "Configure columns": "Configure columns",
 | 
			
		||||
  "Edit field": "Edit field",
 | 
			
		||||
  "Override":"Override",
 | 
			
		||||
  "Override field":"Override field",
 | 
			
		||||
  "Configure fields of {{title}}": "Configure fields of {{title}}",
 | 
			
		||||
  "PK & FK fields": "PK & FK fields",
 | 
			
		||||
  "Association fields": "Association fields",
 | 
			
		||||
  "Parent collection fields":"Parent collection fields",
 | 
			
		||||
  "System fields": "System fields",
 | 
			
		||||
  "General fields": "General fields",
 | 
			
		||||
  "Basic": "Basic",
 | 
			
		||||
 | 
			
		||||
@ -114,6 +114,8 @@ export default {
 | 
			
		||||
  "Configure fields": "フィールドの設定",
 | 
			
		||||
  "Configure columns": "カラムの設定",
 | 
			
		||||
  "Edit field": "フィールドの編集",
 | 
			
		||||
  "Override":"書き換え",
 | 
			
		||||
  "Override field":"フィールドの上書き",
 | 
			
		||||
  "Configure fields of {{title}}": "{{title}}のフィールド設定",
 | 
			
		||||
  "Basic": "基本タイプ",
 | 
			
		||||
  "Single line text": "一行テキスト",
 | 
			
		||||
 | 
			
		||||
@ -117,6 +117,7 @@ export default {
 | 
			
		||||
  "Create collection": "创建数据表",
 | 
			
		||||
  "Collection display name": "数据表名称",
 | 
			
		||||
  "Collection name": "数据表标识",
 | 
			
		||||
  "Inherits":"继承",
 | 
			
		||||
  "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
 | 
			
		||||
  "Storage type": "存储类型",
 | 
			
		||||
  "Edit": "编辑",
 | 
			
		||||
@ -124,11 +125,14 @@ export default {
 | 
			
		||||
  "Configure fields": "配置字段",
 | 
			
		||||
  "Configure columns": "配置字段",
 | 
			
		||||
  "Edit field": "编辑字段",
 | 
			
		||||
  "Override":"重写",
 | 
			
		||||
  "Override field":"重写字段",
 | 
			
		||||
  "Configure fields of {{title}}": "「{{title}}」的字段配置",
 | 
			
		||||
  "PK & FK fields": "主外键字段",
 | 
			
		||||
  "Association fields": "关系字段",
 | 
			
		||||
  "System fields": "系统字段",
 | 
			
		||||
  "General fields": "普通字段",
 | 
			
		||||
  "Parent collection fields":"父表字段",
 | 
			
		||||
  "Basic": "基本类型",
 | 
			
		||||
  "Single line text": "单行文本",
 | 
			
		||||
  "Long text": "多行文本",
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import {
 | 
			
		||||
  ACLRolesCheckProvider,
 | 
			
		||||
  CurrentUser,
 | 
			
		||||
  CurrentUserProvider,
 | 
			
		||||
  CurrentDatabaseProvider,
 | 
			
		||||
  findByUid,
 | 
			
		||||
  findMenuItem,
 | 
			
		||||
  RemoteCollectionManagerProvider,
 | 
			
		||||
@ -17,7 +18,7 @@ import {
 | 
			
		||||
  useDocumentTitle,
 | 
			
		||||
  useRequest,
 | 
			
		||||
  useRoute,
 | 
			
		||||
  useSystemSettings
 | 
			
		||||
  useSystemSettings,
 | 
			
		||||
} from '../../../';
 | 
			
		||||
import { useCollectionManager } from '../../../collection-manager';
 | 
			
		||||
import { PoweredBy } from '../../../powered-by';
 | 
			
		||||
@ -206,6 +207,7 @@ const InternalAdminLayout = (props: any) => {
 | 
			
		||||
 | 
			
		||||
export const AdminLayout = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <CurrentDatabaseProvider>
 | 
			
		||||
      <CurrentUserProvider>
 | 
			
		||||
        <RemoteSchemaTemplateManagerProvider>
 | 
			
		||||
          <RemoteCollectionManagerProvider>
 | 
			
		||||
@ -215,6 +217,7 @@ export const AdminLayout = (props) => {
 | 
			
		||||
          </RemoteCollectionManagerProvider>
 | 
			
		||||
        </RemoteSchemaTemplateManagerProvider>
 | 
			
		||||
      </CurrentUserProvider>
 | 
			
		||||
    </CurrentDatabaseProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ const useLabelFields = (collectionName?: any) => {
 | 
			
		||||
  const { getCollectionFields } = useCollectionManager();
 | 
			
		||||
  const targetFields = getCollectionFields(collectionName);
 | 
			
		||||
  return targetFields
 | 
			
		||||
    ?.filter?.((field) => field?.interface && !field?.target && field.type !== 'boolean')
 | 
			
		||||
    ?.filter?.((field) => field?.interface && !field?.target && field.type !== 'boolean'&&!field.isForeignKey)
 | 
			
		||||
    ?.map?.((field) => {
 | 
			
		||||
      return {
 | 
			
		||||
        value: field.name,
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ import {
 | 
			
		||||
  SchemaInitializerButtonProps,
 | 
			
		||||
  SchemaInitializerItemComponent,
 | 
			
		||||
  SchemaInitializerItemOptions,
 | 
			
		||||
  SchemaInitializerItemProps
 | 
			
		||||
  SchemaInitializerItemProps,
 | 
			
		||||
} from './types';
 | 
			
		||||
 | 
			
		||||
const defaultWrap = (s: ISchema) => s;
 | 
			
		||||
@ -99,12 +99,11 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
  const menu = <Menu>{renderItems(items)}</Menu>;
 | 
			
		||||
  const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
 | 
			
		||||
 | 
			
		||||
  if (!designable && props.designable !== true) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dropdown
 | 
			
		||||
      className={classNames('nb-schema-initializer-button')}
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,41 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { SchemaInitializer } from '../SchemaInitializer';
 | 
			
		||||
import { gridRowColWrap, useCustomFormItemInitializerFields } from '../utils';
 | 
			
		||||
import { gridRowColWrap, useCustomFormItemInitializerFields,useInheritsFormItemInitializerFields } from '../utils';
 | 
			
		||||
import { useCompile } from '../../schema-component';
 | 
			
		||||
 | 
			
		||||
// 表单里配置字段
 | 
			
		||||
export const CustomFormItemInitializers = (props: any) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const { insertPosition, component } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Button
 | 
			
		||||
      wrap={gridRowColWrap}
 | 
			
		||||
      icon={'SettingOutlined'}
 | 
			
		||||
      items={[
 | 
			
		||||
  const inheritFields = useInheritsFormItemInitializerFields();
 | 
			
		||||
  const fieldItems:any[]=[
 | 
			
		||||
    {
 | 
			
		||||
      type: 'itemGroup',
 | 
			
		||||
      title: t('Configure fields'),
 | 
			
		||||
      children: useCustomFormItemInitializerFields(),
 | 
			
		||||
    },
 | 
			
		||||
      ]}
 | 
			
		||||
  ]
 | 
			
		||||
  if (inheritFields?.length > 0) {
 | 
			
		||||
    inheritFields.forEach((inherit) => {
 | 
			
		||||
      fieldItems.push(
 | 
			
		||||
        {
 | 
			
		||||
          type: 'divider',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'itemGroup',
 | 
			
		||||
          title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
 | 
			
		||||
          children: Object.values(inherit)[0],
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Button
 | 
			
		||||
      wrap={gridRowColWrap}
 | 
			
		||||
      icon={'SettingOutlined'}
 | 
			
		||||
      items={fieldItems}
 | 
			
		||||
      insertPosition={insertPosition}
 | 
			
		||||
      component={component}
 | 
			
		||||
      title={component ? null : t('Configure fields')}
 | 
			
		||||
 | 
			
		||||
@ -2,28 +2,45 @@ import { union } from 'lodash';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { SchemaInitializer } from '../SchemaInitializer';
 | 
			
		||||
import { gridRowColWrap, useAssociatedFormItemInitializerFields, useFormItemInitializerFields } from '../utils';
 | 
			
		||||
import {
 | 
			
		||||
  gridRowColWrap,
 | 
			
		||||
  useAssociatedFormItemInitializerFields,
 | 
			
		||||
  useFormItemInitializerFields,
 | 
			
		||||
  useInheritsFormItemInitializerFields,
 | 
			
		||||
} from '../utils';
 | 
			
		||||
import { useCompile } from '../../schema-component';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 表单里配置字段
 | 
			
		||||
export const FormItemInitializers = (props: any) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { insertPosition, component } = props;
 | 
			
		||||
  const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Button
 | 
			
		||||
      wrap={gridRowColWrap}
 | 
			
		||||
      icon={'SettingOutlined'}
 | 
			
		||||
      items={union<any>(
 | 
			
		||||
        [
 | 
			
		||||
  const inheritFields = useInheritsFormItemInitializerFields();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const fieldItems: any[] = [
 | 
			
		||||
    {
 | 
			
		||||
      type: 'itemGroup',
 | 
			
		||||
      title: t('Display fields'),
 | 
			
		||||
      children: useFormItemInitializerFields(),
 | 
			
		||||
    },
 | 
			
		||||
        ],
 | 
			
		||||
        associationFields.length > 0
 | 
			
		||||
          ? [
 | 
			
		||||
  ];
 | 
			
		||||
  if (inheritFields?.length > 0) {
 | 
			
		||||
    inheritFields.forEach((inherit) => {
 | 
			
		||||
      Object.values(inherit)[0].length&&fieldItems.push(
 | 
			
		||||
        {
 | 
			
		||||
          type: 'divider',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'itemGroup',
 | 
			
		||||
          title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
 | 
			
		||||
          children: Object.values(inherit)[0],
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  associationFields.length > 0 &&
 | 
			
		||||
    fieldItems.push(
 | 
			
		||||
      {
 | 
			
		||||
        type: 'divider',
 | 
			
		||||
      },
 | 
			
		||||
@ -32,9 +49,9 @@ export const FormItemInitializers = (props: any) => {
 | 
			
		||||
        title: t('Display association fields'),
 | 
			
		||||
        children: associationFields,
 | 
			
		||||
      },
 | 
			
		||||
            ]
 | 
			
		||||
          : [],
 | 
			
		||||
        [
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  fieldItems.push(
 | 
			
		||||
    {
 | 
			
		||||
      type: 'divider',
 | 
			
		||||
    },
 | 
			
		||||
@ -53,8 +70,12 @@ export const FormItemInitializers = (props: any) => {
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
        ],
 | 
			
		||||
      )}
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Button
 | 
			
		||||
      wrap={gridRowColWrap}
 | 
			
		||||
      icon={'SettingOutlined'}
 | 
			
		||||
      items={fieldItems}
 | 
			
		||||
      insertPosition={insertPosition}
 | 
			
		||||
      component={component}
 | 
			
		||||
      title={component ? null : t('Configure fields')}
 | 
			
		||||
 | 
			
		||||
@ -2,27 +2,38 @@ import { union } from 'lodash';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { SchemaInitializer } from '../SchemaInitializer';
 | 
			
		||||
import { gridRowColWrap, useAssociatedFormItemInitializerFields, useFormItemInitializerFields } from '../utils';
 | 
			
		||||
import { gridRowColWrap, useAssociatedFormItemInitializerFields, useFormItemInitializerFields ,useInheritsFormItemInitializerFields} from '../utils';
 | 
			
		||||
import { useCompile } from '../../schema-component';
 | 
			
		||||
 | 
			
		||||
export const ReadPrettyFormItemInitializers = (props: any) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { insertPosition, component } = props;
 | 
			
		||||
  const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Button
 | 
			
		||||
      wrap={gridRowColWrap}
 | 
			
		||||
      icon={'SettingOutlined'}
 | 
			
		||||
      items={union<any>(
 | 
			
		||||
        [
 | 
			
		||||
  const inheritFields = useInheritsFormItemInitializerFields();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const fieldItems: any[] = [
 | 
			
		||||
    {
 | 
			
		||||
      type: 'itemGroup',
 | 
			
		||||
      title: t('Display fields'),
 | 
			
		||||
      children: useFormItemInitializerFields(),
 | 
			
		||||
    },
 | 
			
		||||
        ],
 | 
			
		||||
        associationFields.length > 0
 | 
			
		||||
          ? [
 | 
			
		||||
  ];
 | 
			
		||||
  if (inheritFields?.length > 0) {
 | 
			
		||||
    inheritFields.forEach((inherit) => {
 | 
			
		||||
      fieldItems.push(
 | 
			
		||||
        {
 | 
			
		||||
          type: 'divider',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'itemGroup',
 | 
			
		||||
          title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
 | 
			
		||||
          children: Object.values(inherit)[0],
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  associationFields.length > 0 &&
 | 
			
		||||
    fieldItems.push([
 | 
			
		||||
      {
 | 
			
		||||
        type: 'divider',
 | 
			
		||||
      },
 | 
			
		||||
@ -31,9 +42,9 @@ export const ReadPrettyFormItemInitializers = (props: any) => {
 | 
			
		||||
        title: t('Display association fields'),
 | 
			
		||||
        children: associationFields,
 | 
			
		||||
      },
 | 
			
		||||
            ]
 | 
			
		||||
          : [],
 | 
			
		||||
        [
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
  fieldItems.push(
 | 
			
		||||
    {
 | 
			
		||||
      type: 'divider',
 | 
			
		||||
    },
 | 
			
		||||
@ -52,8 +63,12 @@ export const ReadPrettyFormItemInitializers = (props: any) => {
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
        ],
 | 
			
		||||
      )}
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Button
 | 
			
		||||
      wrap={gridRowColWrap}
 | 
			
		||||
      icon={'SettingOutlined'}
 | 
			
		||||
      items={fieldItems}
 | 
			
		||||
      insertPosition={insertPosition}
 | 
			
		||||
      component={component}
 | 
			
		||||
      title={component ? null : t('Configure fields')}
 | 
			
		||||
 | 
			
		||||
@ -104,6 +104,23 @@ const useRelationFields = () => {
 | 
			
		||||
  return relationFields;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useInheritFields = () => {
 | 
			
		||||
  const collection = useCollection();
 | 
			
		||||
  const { getChildrenCollections } = useCollectionManager();
 | 
			
		||||
  const childrenCollections = getChildrenCollections(collection.name);
 | 
			
		||||
  return childrenCollections.map((c) => {
 | 
			
		||||
    return {
 | 
			
		||||
      key: c.key,
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      title: c?.title || c.name,
 | 
			
		||||
      component: 'RecordReadPrettyFormBlockInitializer',
 | 
			
		||||
      icon: false,
 | 
			
		||||
      targetCollection: c,
 | 
			
		||||
      actionInitializers: 'CalendarFormActionInitializers',
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const RecordBlockInitializers = (props: any) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { insertPosition, component } = props;
 | 
			
		||||
@ -134,6 +151,11 @@ export const RecordBlockInitializers = (props: any) => {
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'itemGroup',
 | 
			
		||||
          title: '{{t("Children collection blocks")}}',
 | 
			
		||||
          children: useInheritFields(),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'itemGroup',
 | 
			
		||||
          title: '{{t("Relationship blocks")}}',
 | 
			
		||||
 | 
			
		||||
@ -1,34 +1,65 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { SchemaInitializer } from '../SchemaInitializer';
 | 
			
		||||
import { itemsMerge, useAssociatedTableColumnInitializerFields, useTableColumnInitializerFields } from '../utils';
 | 
			
		||||
import {
 | 
			
		||||
  itemsMerge,
 | 
			
		||||
  useAssociatedTableColumnInitializerFields,
 | 
			
		||||
  useTableColumnInitializerFields,
 | 
			
		||||
  useInheritsTableColumnInitializerFields,
 | 
			
		||||
} from '../utils';
 | 
			
		||||
import { useCompile } from '../../schema-component';
 | 
			
		||||
 | 
			
		||||
// 表格列配置
 | 
			
		||||
export const TableColumnInitializers = (props: any) => {
 | 
			
		||||
  const { items = [] } = props;
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const associatedFields = useAssociatedTableColumnInitializerFields();
 | 
			
		||||
  const fieldItems: any[] = [{
 | 
			
		||||
  const inheritFields = useInheritsTableColumnInitializerFields();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const fieldItems: any[] = [
 | 
			
		||||
    {
 | 
			
		||||
      type: 'itemGroup',
 | 
			
		||||
      title: t('Display fields'),
 | 
			
		||||
      children: useTableColumnInitializerFields(),
 | 
			
		||||
  }];
 | 
			
		||||
  if (associatedFields?.length > 0) {
 | 
			
		||||
    fieldItems.push({
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
  if (inheritFields?.length > 0) {
 | 
			
		||||
    inheritFields.forEach((inherit) => {
 | 
			
		||||
      Object.values(inherit)[0].length &&
 | 
			
		||||
        fieldItems.push(
 | 
			
		||||
          {
 | 
			
		||||
            type: 'divider',
 | 
			
		||||
    }, {
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: 'itemGroup',
 | 
			
		||||
            title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
 | 
			
		||||
            children: Object.values(inherit)[0],
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  if (associatedFields?.length > 0) {
 | 
			
		||||
    fieldItems.push(
 | 
			
		||||
      {
 | 
			
		||||
        type: 'divider',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        type: 'itemGroup',
 | 
			
		||||
        title: t('Display association fields'),
 | 
			
		||||
        children: associatedFields,
 | 
			
		||||
    })
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  fieldItems.push({
 | 
			
		||||
  fieldItems.push(
 | 
			
		||||
    {
 | 
			
		||||
      type: 'divider',
 | 
			
		||||
  }, {
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      title: t('Action column'),
 | 
			
		||||
      component: 'TableActionColumnInitializer',
 | 
			
		||||
  })
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Button
 | 
			
		||||
      insertPosition={'beforeEnd'}
 | 
			
		||||
@ -49,10 +80,7 @@ export const TableColumnInitializers = (props: any) => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
      }}
 | 
			
		||||
      items={itemsMerge(
 | 
			
		||||
        fieldItems,
 | 
			
		||||
        items,
 | 
			
		||||
      )}
 | 
			
		||||
      items={itemsMerge(fieldItems, items)}
 | 
			
		||||
    >
 | 
			
		||||
      {t('Configure columns')}
 | 
			
		||||
    </SchemaInitializer.Button>
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ import { DataBlockInitializer } from "./DataBlockInitializer";
 | 
			
		||||
export const CalendarBlockInitializer = (props) => {
 | 
			
		||||
  const { insert } = props;
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const { getCollection } = useCollectionManager();
 | 
			
		||||
  const { getCollectionFields } = useCollectionManager();
 | 
			
		||||
  const options = useContext(SchemaOptionsContext);
 | 
			
		||||
  return (
 | 
			
		||||
    <DataBlockInitializer
 | 
			
		||||
@ -20,8 +20,8 @@ export const CalendarBlockInitializer = (props) => {
 | 
			
		||||
      componentType={'Calendar'}
 | 
			
		||||
      icon={<FormOutlined />}
 | 
			
		||||
      onCreateBlockSchema={async ({ item }) => {
 | 
			
		||||
        const collection = getCollection(item.name);
 | 
			
		||||
        const stringFields = collection?.fields
 | 
			
		||||
        const collectionFields = getCollectionFields(item.name);
 | 
			
		||||
        const stringFields = collectionFields
 | 
			
		||||
          ?.filter((field) => field.type === 'string')
 | 
			
		||||
          ?.map((field) => {
 | 
			
		||||
            return {
 | 
			
		||||
@ -29,7 +29,7 @@ export const CalendarBlockInitializer = (props) => {
 | 
			
		||||
              value: field.name,
 | 
			
		||||
            };
 | 
			
		||||
          });
 | 
			
		||||
        const dateFields = collection?.fields
 | 
			
		||||
        const dateFields = collectionFields
 | 
			
		||||
          ?.filter((field) => field.type === 'date')
 | 
			
		||||
          ?.map((field) => {
 | 
			
		||||
            return {
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ import { SchemaComponent, SchemaComponentOptions } from "../../schema-component"
 | 
			
		||||
export const KanbanBlockInitializer = (props) => {
 | 
			
		||||
    const { insert } = props;
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { getCollection } = useCollectionManager();
 | 
			
		||||
    const { getCollectionFields,getCollection } = useCollectionManager();
 | 
			
		||||
    const options = useContext(SchemaOptionsContext);
 | 
			
		||||
    const api = useAPIClient();
 | 
			
		||||
    return (
 | 
			
		||||
@ -22,8 +22,8 @@ export const KanbanBlockInitializer = (props) => {
 | 
			
		||||
        componentType={'Kanban'}
 | 
			
		||||
        icon={<FormOutlined />}
 | 
			
		||||
        onCreateBlockSchema={async ({ item }) => {
 | 
			
		||||
          const collection = getCollection(item.name);
 | 
			
		||||
          const fields = collection?.fields
 | 
			
		||||
          const collectionFields = getCollectionFields(item.name);
 | 
			
		||||
          const fields = collectionFields
 | 
			
		||||
            ?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
 | 
			
		||||
            ?.map((field) => {
 | 
			
		||||
              return {
 | 
			
		||||
@ -64,7 +64,7 @@ export const KanbanBlockInitializer = (props) => {
 | 
			
		||||
            initialValues: {},
 | 
			
		||||
          });
 | 
			
		||||
          const sortName = `${values.groupField.value}_sort`;
 | 
			
		||||
          const exists = collection?.fields?.find((field) => field.name === sortName);
 | 
			
		||||
          const exists = collectionFields?.find((field) => field.name === sortName);
 | 
			
		||||
          if (!exists) {
 | 
			
		||||
            await api.resource('collections.fields', item.name).create({
 | 
			
		||||
              values: {
 | 
			
		||||
 | 
			
		||||
@ -7,10 +7,17 @@ import { SchemaInitializer } from '../SchemaInitializer';
 | 
			
		||||
import { createReadPrettyFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
 | 
			
		||||
 | 
			
		||||
export const RecordReadPrettyFormBlockInitializer = (props) => {
 | 
			
		||||
  const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props;
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    onCreateBlockSchema,
 | 
			
		||||
    componentType,
 | 
			
		||||
    createBlockSchema,
 | 
			
		||||
    insert,
 | 
			
		||||
    icon = true,
 | 
			
		||||
    targetCollection,
 | 
			
		||||
    ...others
 | 
			
		||||
  } = props;
 | 
			
		||||
  const { getTemplateSchemaByMode } = useSchemaTemplateManager();
 | 
			
		||||
  const collection = useCollection();
 | 
			
		||||
  const collection = targetCollection || useCollection();
 | 
			
		||||
  const association = useBlockAssociationContext();
 | 
			
		||||
  const { block } = useBlockRequestContext();
 | 
			
		||||
  const actionInitializers =
 | 
			
		||||
@ -18,7 +25,7 @@ export const RecordReadPrettyFormBlockInitializer = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SchemaInitializer.Item
 | 
			
		||||
      icon={<FormOutlined />}
 | 
			
		||||
      icon={icon && <FormOutlined />}
 | 
			
		||||
      {...others}
 | 
			
		||||
      key={'123'}
 | 
			
		||||
      onClick={async ({ item }) => {
 | 
			
		||||
 | 
			
		||||
@ -73,15 +73,15 @@ export const findTableColumn = (schema: Schema, key: string, action: string, dee
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useTableColumnInitializerFields = () => {
 | 
			
		||||
  const { name, fields = [] } = useCollection();
 | 
			
		||||
  const { currentFields = [] } = useCollection();
 | 
			
		||||
  const { getInterface } = useCollectionManager();
 | 
			
		||||
  return fields
 | 
			
		||||
  return currentFields
 | 
			
		||||
    .filter((field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey)
 | 
			
		||||
    .map((field) => {
 | 
			
		||||
      const interfaceConfig = getInterface(field.interface);
 | 
			
		||||
      const schema = {
 | 
			
		||||
        name: field.name,
 | 
			
		||||
        'x-collection-field': `${name}.${field.name}`,
 | 
			
		||||
        'x-collection-field': `${field.name}`,
 | 
			
		||||
        'x-component': 'CollectionField',
 | 
			
		||||
        'x-read-pretty': true,
 | 
			
		||||
        'x-component-props': {},
 | 
			
		||||
@ -105,7 +105,6 @@ export const useTableColumnInitializerFields = () => {
 | 
			
		||||
export const useAssociatedTableColumnInitializerFields = () => {
 | 
			
		||||
  const { name, fields } = useCollection();
 | 
			
		||||
  const { getInterface, getCollectionFields } = useCollectionManager();
 | 
			
		||||
 | 
			
		||||
  const groups = fields
 | 
			
		||||
    ?.filter((field) => {
 | 
			
		||||
      return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface);
 | 
			
		||||
@ -141,7 +140,6 @@ export const useAssociatedTableColumnInitializerFields = () => {
 | 
			
		||||
            schema,
 | 
			
		||||
          } as SchemaInitializerItemOptions;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'subMenu',
 | 
			
		||||
        title: field.uiSchema?.title,
 | 
			
		||||
@ -152,13 +150,51 @@ export const useAssociatedTableColumnInitializerFields = () => {
 | 
			
		||||
  return groups;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useInheritsTableColumnInitializerFields = () => {
 | 
			
		||||
  const { name } = useCollection();
 | 
			
		||||
  const { getInterface, getParentCollections, getCollection, getParentCollectionFields } = useCollectionManager();
 | 
			
		||||
  const inherits = getParentCollections(name);
 | 
			
		||||
  return inherits?.map((v) => {
 | 
			
		||||
    const fields = getParentCollectionFields(v, name);
 | 
			
		||||
    const targetCollection = getCollection(v);
 | 
			
		||||
    return {
 | 
			
		||||
      [targetCollection?.title]: fields
 | 
			
		||||
        ?.filter((field) => {
 | 
			
		||||
          return field?.interface;
 | 
			
		||||
        })
 | 
			
		||||
        .map((k) => {
 | 
			
		||||
          const interfaceConfig = getInterface(k.interface);
 | 
			
		||||
          const schema = {
 | 
			
		||||
            name: `${k.name}`,
 | 
			
		||||
            'x-component': 'CollectionField',
 | 
			
		||||
            'x-read-pretty': true,
 | 
			
		||||
            'x-collection-field': `${k.name}`,
 | 
			
		||||
            'x-component-props': {},
 | 
			
		||||
          };
 | 
			
		||||
          return {
 | 
			
		||||
            type: 'item',
 | 
			
		||||
            title: k?.uiSchema?.title || k.name,
 | 
			
		||||
            component: 'TableCollectionFieldInitializer',
 | 
			
		||||
            find: findTableColumn,
 | 
			
		||||
            remove: removeTableColumn,
 | 
			
		||||
            schemaInitialize: (s) => {
 | 
			
		||||
              interfaceConfig?.schemaInitialize?.(s, { field: k, readPretty: true, block: 'Table' });
 | 
			
		||||
            },
 | 
			
		||||
            field: k,
 | 
			
		||||
            schema,
 | 
			
		||||
          } as SchemaInitializerItemOptions;
 | 
			
		||||
        }),
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
  const { name, fields } = useCollection();
 | 
			
		||||
  const { name, currentFields } = useCollection();
 | 
			
		||||
  const { getInterface } = useCollectionManager();
 | 
			
		||||
  const form = useForm();
 | 
			
		||||
  const { readPretty = form.readPretty, block = 'Form' } = options || {};
 | 
			
		||||
 | 
			
		||||
  return fields
 | 
			
		||||
  return currentFields
 | 
			
		||||
    ?.filter((field) => field?.interface && !field?.isForeignKey)
 | 
			
		||||
    ?.map((field) => {
 | 
			
		||||
      const interfaceConfig = getInterface(field.interface);
 | 
			
		||||
@ -170,7 +206,7 @@ export const useFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
        'x-designer': 'FormItem.Designer',
 | 
			
		||||
        'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField',
 | 
			
		||||
        'x-decorator': 'FormItem',
 | 
			
		||||
        'x-collection-field': `${name}.${field.name}`,
 | 
			
		||||
        'x-collection-field': `${field.name}`,
 | 
			
		||||
        'x-component-props': {},
 | 
			
		||||
        'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
 | 
			
		||||
      };
 | 
			
		||||
@ -240,13 +276,52 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
  return groups;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useInheritsFormItemInitializerFields = (options?) => {
 | 
			
		||||
  const { name } = useCollection();
 | 
			
		||||
  const { getInterface, getParentCollections, getCollection, getParentCollectionFields } = useCollectionManager();
 | 
			
		||||
  const inherits = getParentCollections(name);
 | 
			
		||||
  return inherits?.map((v) => {
 | 
			
		||||
    const fields = getParentCollectionFields(v, name);
 | 
			
		||||
    const form = useForm();
 | 
			
		||||
    const { readPretty = form.readPretty, block = 'Form' } = options || {};
 | 
			
		||||
    const targetCollection = getCollection(v);
 | 
			
		||||
    return {
 | 
			
		||||
      [targetCollection.title]: fields
 | 
			
		||||
        ?.filter((field) => field?.interface && !field?.isForeignKey)
 | 
			
		||||
        ?.map((field) => {
 | 
			
		||||
          const interfaceConfig = getInterface(field.interface);
 | 
			
		||||
          const schema = {
 | 
			
		||||
            type: 'string',
 | 
			
		||||
            name: field.name,
 | 
			
		||||
            title: field?.uiSchema?.title || field.name,
 | 
			
		||||
            'x-designer': 'FormItem.Designer',
 | 
			
		||||
            'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField',
 | 
			
		||||
            'x-decorator': 'FormItem',
 | 
			
		||||
            'x-collection-field': `${field.name}`,
 | 
			
		||||
            'x-component-props': {},
 | 
			
		||||
            'x-read-pretty': field?.uiSchema?.['x-read-pretty'],
 | 
			
		||||
          };
 | 
			
		||||
          return {
 | 
			
		||||
            type: 'item',
 | 
			
		||||
            title: field?.uiSchema?.title || field.name,
 | 
			
		||||
            component: 'CollectionFieldInitializer',
 | 
			
		||||
            remove: removeGridFormItem,
 | 
			
		||||
            schemaInitialize: (s) => {
 | 
			
		||||
              interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty });
 | 
			
		||||
            },
 | 
			
		||||
            schema,
 | 
			
		||||
          } as SchemaInitializerItemOptions;
 | 
			
		||||
        }),
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
export const useCustomFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
  const { name, fields } = useCollection();
 | 
			
		||||
  const { name, currentFields } = useCollection();
 | 
			
		||||
  const { getInterface } = useCollectionManager();
 | 
			
		||||
  const form = useForm();
 | 
			
		||||
  const { readPretty = form.readPretty, block = 'Form' } = options || {};
 | 
			
		||||
  const remove = useRemoveGridFormItem();
 | 
			
		||||
  return fields
 | 
			
		||||
  return currentFields
 | 
			
		||||
    ?.filter((field) => {
 | 
			
		||||
      return field?.interface && !field?.uiSchema?.['x-read-pretty'];
 | 
			
		||||
    })
 | 
			
		||||
@ -259,7 +334,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
        'x-designer': 'FormItem.Designer',
 | 
			
		||||
        'x-component': 'AssignedField',
 | 
			
		||||
        'x-decorator': 'FormItem',
 | 
			
		||||
        'x-collection-field': `${name}.${field.name}`,
 | 
			
		||||
        'x-collection-field': `${field.name}`,
 | 
			
		||||
      };
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'item',
 | 
			
		||||
@ -293,7 +368,7 @@ export const useCustomBulkEditFormItemInitializerFields = (options?: any) => {
 | 
			
		||||
        'x-designer': 'FormItem.Designer',
 | 
			
		||||
        'x-component': 'BulkEditField',
 | 
			
		||||
        'x-decorator': 'FormItem',
 | 
			
		||||
        'x-collection-field': `${name}.${field.name}`,
 | 
			
		||||
        'x-collection-field': `${field.name}`,
 | 
			
		||||
      };
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'item',
 | 
			
		||||
@ -344,6 +419,7 @@ export const useCurrentSchema = (action: string, key: string, find = findSchema,
 | 
			
		||||
  }
 | 
			
		||||
  const { remove } = useDesignable();
 | 
			
		||||
  const schema = find(fieldSchema, key, action);
 | 
			
		||||
  console.log(fieldSchema, key, action);
 | 
			
		||||
  return {
 | 
			
		||||
    schema,
 | 
			
		||||
    exists: !!schema,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Dropdown, Menu } from 'antd';
 | 
			
		||||
import React, { createContext, useState } from 'react';
 | 
			
		||||
import React, { createContext, useState, useContext } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import { useAPIClient, useCurrentUserContext } from '..';
 | 
			
		||||
@ -8,14 +8,11 @@ import { ChangePassword } from './ChangePassword';
 | 
			
		||||
import { EditProfile } from './EditProfile';
 | 
			
		||||
import { LanguageSettings } from './LanguageSettings';
 | 
			
		||||
import { SwitchRole } from './SwitchRole';
 | 
			
		||||
import {useCurrentDatabase} from '../database/CurrentDatabaseProvider'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const ApplicationVersion = () => {
 | 
			
		||||
  const { data, loading } = useRequest({
 | 
			
		||||
    url: 'app:getInfo',
 | 
			
		||||
  });
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  const data=useCurrentDatabase();
 | 
			
		||||
  return (
 | 
			
		||||
      <Menu.Item key="version" disabled>
 | 
			
		||||
        Version {data?.data?.version}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
import { Collection } from '../../collection';
 | 
			
		||||
import Database from '../../database';
 | 
			
		||||
import { InheritedCollection } from '../../inherited-collection';
 | 
			
		||||
import { mockDatabase } from '../index';
 | 
			
		||||
import pgOnly from './helper';
 | 
			
		||||
 | 
			
		||||
pgOnly()('sync inherits', () => {
 | 
			
		||||
  let db: Database;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    db = mockDatabase();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    await db.close();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should update table fields', async () => {
 | 
			
		||||
    const person = db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [{ type: 'string', name: 'name' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const student = db.collection({
 | 
			
		||||
      name: 'student',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    student.setField('score', { type: 'integer' });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    const studentTableInfo = await db.sequelize.getQueryInterface().describeTable(student.model.tableName);
 | 
			
		||||
    expect(studentTableInfo.score).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,706 @@
 | 
			
		||||
import Database from '../../database';
 | 
			
		||||
import { InheritedCollection } from '../../inherited-collection';
 | 
			
		||||
import { mockDatabase } from '../index';
 | 
			
		||||
import pgOnly from './helper';
 | 
			
		||||
 | 
			
		||||
pgOnly()('collection inherits', () => {
 | 
			
		||||
  let db: Database;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    db = mockDatabase();
 | 
			
		||||
    await db.clean({ drop: true });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    await db.close();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('can update relation with child table', async () => {
 | 
			
		||||
    const A = db.collection({
 | 
			
		||||
      name: 'a',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'a-field',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'a1s',
 | 
			
		||||
          type: 'hasMany',
 | 
			
		||||
          target: 'a1',
 | 
			
		||||
          foreignKey: 'aId',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const A1 = db.collection({
 | 
			
		||||
      name: 'a1',
 | 
			
		||||
      inherits: ['a'],
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'a1-field',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    await A.repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        'a-field': 'a-1',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let a11 = await A1.repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        'a1-field': 'a1-1',
 | 
			
		||||
        'a-field': 'a1-1',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const a12 = await A1.repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        'a1-field': 'a1-2',
 | 
			
		||||
        'a-field': 'a1-2',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await A1.repository.update({
 | 
			
		||||
      filterByTk: a11.get('id'),
 | 
			
		||||
      values: {
 | 
			
		||||
        a1s: [{ id: a12.get('id') }],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    a11 = await A1.repository.findOne({
 | 
			
		||||
      filter: {
 | 
			
		||||
        'a1-field': 'a1-1',
 | 
			
		||||
      },
 | 
			
		||||
      appends: ['a1s'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const a11a1s = a11.get('a1s');
 | 
			
		||||
    expect(a11a1s[0].get('id')).toBe(a12.get('id'));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('can create relation with child table', async () => {
 | 
			
		||||
    const A = db.collection({
 | 
			
		||||
      name: 'a',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'af',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'bs',
 | 
			
		||||
          type: 'hasMany',
 | 
			
		||||
          target: 'b',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const B = db.collection({
 | 
			
		||||
      name: 'b',
 | 
			
		||||
      inherits: ['a'],
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'bf',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'a',
 | 
			
		||||
          type: 'belongsTo',
 | 
			
		||||
          target: 'a',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    const a1 = await B.repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        af: 'a1',
 | 
			
		||||
        bs: [{ bf: 'b1' }, { bf: 'b2' }],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(a1.get('bs').length).toBe(2);
 | 
			
		||||
 | 
			
		||||
    const b1 = await B.repository.findOne({
 | 
			
		||||
      filter: {
 | 
			
		||||
        af: 'a1',
 | 
			
		||||
      },
 | 
			
		||||
      appends: ['bs'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(b1.get('bs').length).toBe(2);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should inherit belongsToMany field', async () => {
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'name',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'tags',
 | 
			
		||||
          type: 'belongsToMany',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'tags',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'name',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'person',
 | 
			
		||||
          type: 'belongsToMany',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'students',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'score',
 | 
			
		||||
          type: 'integer',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('students').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'John',
 | 
			
		||||
        score: 100,
 | 
			
		||||
        tags: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 't1',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 't2',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 't3',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('person').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'Max',
 | 
			
		||||
        tags: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 't2',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 't4',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const john = await db.getCollection('students').repository.findOne({
 | 
			
		||||
      appends: ['tags'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(john.get('name')).toBe('John');
 | 
			
		||||
    expect(john.get('tags')).toHaveLength(3);
 | 
			
		||||
 | 
			
		||||
    const max = await db.getCollection('person').repository.findOne({
 | 
			
		||||
      appends: ['tags'],
 | 
			
		||||
      filter: {
 | 
			
		||||
        name: 'Max',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(max.get('name')).toBe('Max');
 | 
			
		||||
    expect(max.get('tags')).toHaveLength(2);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should inherit hasMany field', async () => {
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'name',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'pets',
 | 
			
		||||
          type: 'hasMany',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'pets',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'name',
 | 
			
		||||
          type: 'string',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'students',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'score',
 | 
			
		||||
          type: 'integer',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('person').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'Max',
 | 
			
		||||
        pets: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'doge1',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'kitty1',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('students').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'John',
 | 
			
		||||
        score: 100,
 | 
			
		||||
        pets: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'doge',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'kitty',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'doge2',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const john = await db.getCollection('students').repository.findOne({
 | 
			
		||||
      appends: ['pets'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(john.get('name')).toBe('John');
 | 
			
		||||
    expect(john.get('pets')).toHaveLength(3);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('can inherit from multiple collections', async () => {
 | 
			
		||||
    const a = db.collection({
 | 
			
		||||
      name: 'a',
 | 
			
		||||
      fields: [{ type: 'string', name: 'a1' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const b = db.collection({
 | 
			
		||||
      name: 'b',
 | 
			
		||||
      fields: [{ type: 'string', name: 'b1' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const c = db.collection({
 | 
			
		||||
      name: 'c',
 | 
			
		||||
      inherits: ['a', 'b'],
 | 
			
		||||
      fields: [
 | 
			
		||||
        { type: 'integer', name: 'id', autoIncrement: true },
 | 
			
		||||
        { type: 'string', name: 'c1' },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    expect(c.getField('a1')).toBeTruthy();
 | 
			
		||||
    expect(c.getField('b1')).toBeTruthy();
 | 
			
		||||
    expect(c.getField('c1')).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
    const c1 = await c.repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        a1: 'a1',
 | 
			
		||||
        b1: 'b1',
 | 
			
		||||
        c1: 'c1',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(c1.get('a1')).toBe('a1');
 | 
			
		||||
    expect(c1.get('b1')).toBe('b1');
 | 
			
		||||
    expect(c1.get('c1')).toBe('c1');
 | 
			
		||||
 | 
			
		||||
    const a2 = await a.repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        a1: 'a2',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(a2.get('id')).toEqual(2);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should update inherit field when parent field update', async () => {
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [{ name: 'name', type: 'string', title: 'parent-name' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'students',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(db.getCollection('students').getField('name').get('title')).toBe('parent-name');
 | 
			
		||||
 | 
			
		||||
    db.getCollection('person').setField('name', { type: 'string', title: 'new-name' });
 | 
			
		||||
 | 
			
		||||
    expect(db.getCollection('person').getField('name').get('title')).toBe('new-name');
 | 
			
		||||
    expect(db.getCollection('students').getField('name').get('title')).toBe('new-name');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should not replace child field when parent field update', async () => {
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [{ name: 'name', type: 'string', title: 'parent-name' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'students',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [{ name: 'name', type: 'string', title: 'student-name' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(db.getCollection('students').getField('name').get('title')).toBe('student-name');
 | 
			
		||||
 | 
			
		||||
    db.getCollection('person').setField('name', { type: 'string', title: 'new-name' });
 | 
			
		||||
 | 
			
		||||
    expect(db.getCollection('person').getField('name').get('title')).toBe('new-name');
 | 
			
		||||
    expect(db.getCollection('students').getField('name').get('title')).toBe('student-name');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should replace child association target', async () => {
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [
 | 
			
		||||
        { name: 'name', type: 'string' },
 | 
			
		||||
        { type: 'hasOne', name: 'profile' },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'profiles',
 | 
			
		||||
      fields: [
 | 
			
		||||
        { name: 'age', type: 'integer' },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'belongsTo',
 | 
			
		||||
          name: 'person',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'teachers',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [{ name: 'salary', type: 'integer' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'students',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [
 | 
			
		||||
        { name: 'score', type: 'integer' },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'hasOne',
 | 
			
		||||
          name: 'profile',
 | 
			
		||||
          target: 'studentProfiles',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'studentProfiles',
 | 
			
		||||
      fields: [{ name: 'grade', type: 'string' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    const student = await db.getCollection('students').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'foo',
 | 
			
		||||
        score: 100,
 | 
			
		||||
        profile: {
 | 
			
		||||
          grade: 'A',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(student.get('profile').get('grade')).toBe('A');
 | 
			
		||||
 | 
			
		||||
    const teacher = await db.getCollection('teachers').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'bar',
 | 
			
		||||
        salary: 1000,
 | 
			
		||||
        profile: {
 | 
			
		||||
          age: 30,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(teacher.get('profile').get('age')).toBe(30);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should replace hasOne association field', async () => {
 | 
			
		||||
    const person = db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [
 | 
			
		||||
        { name: 'name', type: 'string' },
 | 
			
		||||
        { type: 'hasOne', name: 'profile', target: 'profiles', foreignKey: 'person_id' },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const profile = db.collection({
 | 
			
		||||
      name: 'profiles',
 | 
			
		||||
      fields: [
 | 
			
		||||
        { name: 'age', type: 'integer' },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'belongsTo',
 | 
			
		||||
          name: 'person',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const student = db.collection({
 | 
			
		||||
      name: 'students',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [{ name: 'profile', type: 'hasOne', target: 'studentProfiles', foreignKey: 'student_id' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const studentProfile = db.collection({
 | 
			
		||||
      name: 'studentProfiles',
 | 
			
		||||
      fields: [{ name: 'score', type: 'integer' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    const student1 = await student.repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'student-1',
 | 
			
		||||
        profile: {
 | 
			
		||||
          score: '100',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let person1 = await person.repository.findOne();
 | 
			
		||||
    await person.repository
 | 
			
		||||
      .relation('profile')
 | 
			
		||||
      .of(person1.get('id'))
 | 
			
		||||
      .create({
 | 
			
		||||
        values: {
 | 
			
		||||
          age: 30,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    person1 = await person.repository.findOne({
 | 
			
		||||
      appends: ['profile'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(person1.get('profile').get('age')).toBe(30);
 | 
			
		||||
 | 
			
		||||
    expect(student1.get('profile').get('score')).toBe(100);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should inherit hasOne association field', async () => {
 | 
			
		||||
    const person = db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [
 | 
			
		||||
        { name: 'name', type: 'string' },
 | 
			
		||||
        { type: 'hasOne', name: 'profile' },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const profile = db.collection({
 | 
			
		||||
      name: 'profiles',
 | 
			
		||||
      fields: [
 | 
			
		||||
        { name: 'age', type: 'integer' },
 | 
			
		||||
        {
 | 
			
		||||
          type: 'belongsTo',
 | 
			
		||||
          name: 'person',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'students',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [{ name: 'score', type: 'integer' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: 'teachers',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [{ name: 'salary', type: 'integer' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('students').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'foo',
 | 
			
		||||
        score: 100,
 | 
			
		||||
        profile: {
 | 
			
		||||
          age: 18,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('teachers').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'bar',
 | 
			
		||||
        salary: 1000,
 | 
			
		||||
        profile: {
 | 
			
		||||
          age: 30,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const studentFoo = await db.getCollection('students').repository.findOne({
 | 
			
		||||
      appends: ['profile'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const teacherBar = await db.getCollection('teachers').repository.findOne({
 | 
			
		||||
      appends: ['profile'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(studentFoo.get('profile').age).toBe(18);
 | 
			
		||||
    expect(teacherBar.get('profile').age).toBe(30);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should inherit from Collection', async () => {
 | 
			
		||||
    const person = db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [{ name: 'name', type: 'string' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const student = db.collection({
 | 
			
		||||
      name: 'student',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [{ name: 'score', type: 'integer' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    const StudentRepository = student.repository;
 | 
			
		||||
 | 
			
		||||
    await StudentRepository.create({
 | 
			
		||||
      values: { name: 'student1' },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(await person.repository.count()).toBe(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create inherited table', async () => {
 | 
			
		||||
    const person = db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [{ name: 'name', type: 'string' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const student = db.collection({
 | 
			
		||||
      name: 'student',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [{ name: 'score', type: 'integer' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    const studentTableInfo = await db.sequelize.getQueryInterface().describeTable(student.model.tableName);
 | 
			
		||||
 | 
			
		||||
    expect(studentTableInfo.score).toBeDefined();
 | 
			
		||||
    expect(studentTableInfo.name).toBeDefined();
 | 
			
		||||
    expect(studentTableInfo.id).toBeDefined();
 | 
			
		||||
    expect(studentTableInfo.createdAt).toBeDefined();
 | 
			
		||||
    expect(studentTableInfo.updatedAt).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should get parent fields', async () => {
 | 
			
		||||
    const root = db.collection({
 | 
			
		||||
      name: 'root',
 | 
			
		||||
      fields: [{ name: 'rootField', type: 'string' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const parent1 = db.collection({
 | 
			
		||||
      name: 'parent1',
 | 
			
		||||
      inherits: 'root',
 | 
			
		||||
      fields: [{ name: 'parent1Field', type: 'string' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const parent2 = db.collection({
 | 
			
		||||
      name: 'parent2',
 | 
			
		||||
      inherits: 'parent1',
 | 
			
		||||
      fields: [{ name: 'parent2Field', type: 'string' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const parent21 = db.collection({
 | 
			
		||||
      name: 'parent21',
 | 
			
		||||
      fields: [{ name: 'parent21Field', type: 'string' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const child: InheritedCollection = db.collection({
 | 
			
		||||
      name: 'child',
 | 
			
		||||
      inherits: ['parent2', 'parent21'],
 | 
			
		||||
      fields: [{ name: 'childField', type: 'string' }],
 | 
			
		||||
    }) as InheritedCollection;
 | 
			
		||||
 | 
			
		||||
    const parentFields = child.parentFields();
 | 
			
		||||
    expect(parentFields.size).toBe(4);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should sync parent fields', async () => {
 | 
			
		||||
    const person = db.collection({
 | 
			
		||||
      name: 'person',
 | 
			
		||||
      fields: [{ name: 'name', type: 'string' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const student = db.collection({
 | 
			
		||||
      name: 'student',
 | 
			
		||||
      inherits: 'person',
 | 
			
		||||
      fields: [{ name: 'score', type: 'integer' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    expect(student.fields.get('name')).toBeDefined();
 | 
			
		||||
 | 
			
		||||
    // add new field to parent
 | 
			
		||||
    person.setField('age', { type: 'integer' });
 | 
			
		||||
 | 
			
		||||
    await db.sync();
 | 
			
		||||
 | 
			
		||||
    expect(student.fields.get('age')).toBeDefined();
 | 
			
		||||
 | 
			
		||||
    const student1 = await db.getCollection('student').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'student1',
 | 
			
		||||
        age: 10,
 | 
			
		||||
        score: 100,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(student1.get('name')).toBe('student1');
 | 
			
		||||
    expect(student1.get('age')).toBe(10);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/core/database/src/__tests__/inhertits/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/core/database/src/__tests__/inhertits/helper.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
const pgOnly = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
 | 
			
		||||
 | 
			
		||||
export default pgOnly;
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
import InheritanceMap from '../../inherited-map';
 | 
			
		||||
 | 
			
		||||
describe('InheritedMap', () => {
 | 
			
		||||
  it('should setInherits', () => {
 | 
			
		||||
    const map = new InheritanceMap();
 | 
			
		||||
    map.setInheritance('b', 'a');
 | 
			
		||||
 | 
			
		||||
    const nodeA = map.getNode('a');
 | 
			
		||||
    const nodeB = map.getNode('b');
 | 
			
		||||
 | 
			
		||||
    expect(nodeA.children.has(nodeB)).toBe(true);
 | 
			
		||||
    expect(nodeB.parents.has(nodeA)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(map.isParentNode('a')).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should get deep children', () => {
 | 
			
		||||
    const map = new InheritanceMap();
 | 
			
		||||
    map.setInheritance('b', 'a');
 | 
			
		||||
    map.setInheritance('c', 'b');
 | 
			
		||||
    map.setInheritance('c1', 'b');
 | 
			
		||||
    map.setInheritance('d', 'c');
 | 
			
		||||
 | 
			
		||||
    const children = map.getChildren('a');
 | 
			
		||||
    expect(children.size).toBe(4);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -22,6 +22,7 @@ export type CollectionSortable = string | boolean | { name?: string; scopeKey?:
 | 
			
		||||
export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> {
 | 
			
		||||
  name: string;
 | 
			
		||||
  tableName?: string;
 | 
			
		||||
  inherits?: string[] | string;
 | 
			
		||||
  filterTargetKey?: string;
 | 
			
		||||
  fields?: FieldOptions[];
 | 
			
		||||
  model?: string | ModelCtor<Model>;
 | 
			
		||||
@ -74,7 +75,9 @@ export class Collection<
 | 
			
		||||
 | 
			
		||||
    this.bindFieldEventListener();
 | 
			
		||||
    this.modelInit();
 | 
			
		||||
 | 
			
		||||
    this.db.modelCollection.set(this.model, this);
 | 
			
		||||
    this.db.tableNameCollectionMap.set(this.model.tableName, this);
 | 
			
		||||
 | 
			
		||||
    this.setFields(options.fields);
 | 
			
		||||
    this.setRepository(options.repository);
 | 
			
		||||
@ -181,9 +184,34 @@ export class Collection<
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const oldField = this.fields.get(name);
 | 
			
		||||
 | 
			
		||||
    if (oldField && oldField.options.inherit && options.type != oldField.options.type) {
 | 
			
		||||
      throw new Error(
 | 
			
		||||
        `Field type conflict: cannot set "${name}" to ${options.type}, parent "${name}" type is ${oldField.options.type}`,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.removeField(name);
 | 
			
		||||
    this.fields.set(name, field);
 | 
			
		||||
    this.emit('field.afterAdd', field);
 | 
			
		||||
 | 
			
		||||
    if (this.isParent()) {
 | 
			
		||||
      for (const child of this.context.database.inheritanceMap.getChildren(this.name, {
 | 
			
		||||
        deep: false,
 | 
			
		||||
      })) {
 | 
			
		||||
        const childCollection = this.db.getCollection(child);
 | 
			
		||||
        const existField = childCollection.getField(name);
 | 
			
		||||
 | 
			
		||||
        if (!existField || existField.options.inherit) {
 | 
			
		||||
          childCollection.setField(name, {
 | 
			
		||||
            ...options,
 | 
			
		||||
            inherit: true,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return field;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -371,6 +399,7 @@ export class Collection<
 | 
			
		||||
    for (const associationKey in associations) {
 | 
			
		||||
      const association = associations[associationKey];
 | 
			
		||||
      modelNames.add(association.target.name);
 | 
			
		||||
 | 
			
		||||
      if ((<any>association).through) {
 | 
			
		||||
        modelNames.add((<any>association).through.model.name);
 | 
			
		||||
      }
 | 
			
		||||
@ -388,4 +417,12 @@ export class Collection<
 | 
			
		||||
      await model.sync(syncOptions);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public isInherited() {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public isParent() {
 | 
			
		||||
    return this.context.database.inheritanceMap.isParentNode(this.name);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,8 @@ import {
 | 
			
		||||
} from './types';
 | 
			
		||||
import { referentialIntegrityCheck } from './features/referential-integrity-check';
 | 
			
		||||
import ReferencesMap from './features/ReferencesMap';
 | 
			
		||||
import { InheritedCollection } from './inherited-collection';
 | 
			
		||||
import InheritanceMap from './inherited-map';
 | 
			
		||||
 | 
			
		||||
export interface MergeOptions extends merge.Options {}
 | 
			
		||||
 | 
			
		||||
@ -148,7 +150,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
  collections = new Map<string, Collection>();
 | 
			
		||||
  pendingFields = new Map<string, RelationField[]>();
 | 
			
		||||
  modelCollection = new Map<ModelCtor<any>, Collection>();
 | 
			
		||||
  tableNameCollectionMap = new Map<string, Collection>();
 | 
			
		||||
 | 
			
		||||
  referenceMap = new ReferencesMap();
 | 
			
		||||
  inheritanceMap = new InheritanceMap();
 | 
			
		||||
 | 
			
		||||
  modelHook: ModelHook;
 | 
			
		||||
  version: DatabaseVersion;
 | 
			
		||||
@ -293,7 +298,11 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
  ): Collection<Attributes, CreateAttributes> {
 | 
			
		||||
    this.emit('beforeDefineCollection', options);
 | 
			
		||||
 | 
			
		||||
    const collection = new Collection(options, {
 | 
			
		||||
    const collection = options.inherits
 | 
			
		||||
      ? new InheritedCollection(options, {
 | 
			
		||||
          database: this,
 | 
			
		||||
        })
 | 
			
		||||
      : new Collection(options, {
 | 
			
		||||
          database: this,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -429,6 +438,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
    if (isMySQL) {
 | 
			
		||||
      await this.sequelize.query('SET FOREIGN_KEY_CHECKS = 1', null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import {
 | 
			
		||||
import { Collection } from '../collection';
 | 
			
		||||
import { Database } from '../database';
 | 
			
		||||
import { ModelEventTypes } from '../types';
 | 
			
		||||
import { InheritedCollection } from '../inherited-collection';
 | 
			
		||||
 | 
			
		||||
export interface FieldContext {
 | 
			
		||||
  database: Database;
 | 
			
		||||
@ -19,6 +20,7 @@ export interface FieldContext {
 | 
			
		||||
export interface BaseFieldOptions {
 | 
			
		||||
  name?: string;
 | 
			
		||||
  hidden?: boolean;
 | 
			
		||||
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,8 +34,17 @@ export abstract class Field {
 | 
			
		||||
  context: FieldContext;
 | 
			
		||||
  database: Database;
 | 
			
		||||
  collection: Collection;
 | 
			
		||||
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
 | 
			
		||||
  constructor(options?: any, context?: FieldContext) {
 | 
			
		||||
    this.context = context;
 | 
			
		||||
    this.database = context.database;
 | 
			
		||||
    this.collection = context.collection;
 | 
			
		||||
    this.options = options || {};
 | 
			
		||||
    this.init();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get name() {
 | 
			
		||||
    return this.options.name;
 | 
			
		||||
  }
 | 
			
		||||
@ -46,14 +57,6 @@ export abstract class Field {
 | 
			
		||||
    return this.options.dataType;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(options?: any, context?: FieldContext) {
 | 
			
		||||
    this.context = context;
 | 
			
		||||
    this.database = context.database;
 | 
			
		||||
    this.collection = context.collection;
 | 
			
		||||
    this.options = options || {};
 | 
			
		||||
    this.init();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async sync(syncOptions: SyncOptions) {
 | 
			
		||||
    await this.collection.sync({
 | 
			
		||||
      ...syncOptions,
 | 
			
		||||
@ -88,11 +91,18 @@ export abstract class Field {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async removeFromDb(options?: QueryInterfaceOptions) {
 | 
			
		||||
    if (!this.collection.model.rawAttributes[this.name]) {
 | 
			
		||||
    const attribute = this.collection.model.rawAttributes[this.name];
 | 
			
		||||
 | 
			
		||||
    if (!attribute) {
 | 
			
		||||
      this.remove();
 | 
			
		||||
      // console.log('field is not attribute');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.collection.isInherited() && (<InheritedCollection>this.collection).parentFields().has(this.name)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ((this.collection.model as any)._virtualAttributes.has(this.name)) {
 | 
			
		||||
      this.remove();
 | 
			
		||||
      // console.log('field is virtual attribute');
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
export { DataTypes, ModelCtor, Op, SyncOptions } from 'sequelize';
 | 
			
		||||
export * from './collection';
 | 
			
		||||
export * from './inherited-collection';
 | 
			
		||||
export * from './database';
 | 
			
		||||
export { Database as default } from './database';
 | 
			
		||||
export * from './fields';
 | 
			
		||||
@ -14,4 +15,3 @@ export * from './relation-repository/multiple-relation-repository';
 | 
			
		||||
export * from './relation-repository/single-relation-repository';
 | 
			
		||||
export * from './repository';
 | 
			
		||||
export * from './update-associations';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										73
									
								
								packages/core/database/src/inherited-collection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								packages/core/database/src/inherited-collection.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,73 @@
 | 
			
		||||
import { Collection, CollectionContext, CollectionOptions } from './collection';
 | 
			
		||||
import { default as lodash } from 'lodash';
 | 
			
		||||
import { Field } from '.';
 | 
			
		||||
 | 
			
		||||
export class InheritedCollection extends Collection {
 | 
			
		||||
  parents?: Collection[];
 | 
			
		||||
 | 
			
		||||
  constructor(options: CollectionOptions, context: CollectionContext) {
 | 
			
		||||
    if (!options.inherits) {
 | 
			
		||||
      throw new Error('InheritedCollection must have inherits option');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    super(options, context);
 | 
			
		||||
    this.setParents(options.inherits);
 | 
			
		||||
    this.context.database.inheritanceMap.setInheritance(this.name, options.inherits);
 | 
			
		||||
    this.setParentFields();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected setParents(inherits: string | string[]) {
 | 
			
		||||
    this.parents = lodash.castArray(inherits).map((name) => this.context.database.collections.get(name));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected setParentFields() {
 | 
			
		||||
    for (const [name, field] of this.parentFields()) {
 | 
			
		||||
      if (!this.hasField(name)) {
 | 
			
		||||
        this.setField(name, {
 | 
			
		||||
          ...field.options,
 | 
			
		||||
          inherit: true,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getParents() {
 | 
			
		||||
    return this.parents;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  parentFields() {
 | 
			
		||||
    const fields = new Map<string, Field>();
 | 
			
		||||
    for (const parent of this.parents) {
 | 
			
		||||
      if (parent.isInherited()) {
 | 
			
		||||
        for (const [name, field] of (<InheritedCollection>parent).parentFields()) {
 | 
			
		||||
          fields.set(name, field);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const parentFields = parent.fields;
 | 
			
		||||
      for (const [name, field] of parentFields) {
 | 
			
		||||
        fields.set(name, field);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return fields;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  parentAttributes() {
 | 
			
		||||
    const attributes = {};
 | 
			
		||||
    for (const parent of this.parents) {
 | 
			
		||||
      if (parent.isInherited()) {
 | 
			
		||||
        Object.assign(attributes, (<InheritedCollection>parent).parentAttributes());
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const parentAttributes = (<any>parent.model).tableAttributes;
 | 
			
		||||
 | 
			
		||||
      Object.assign(attributes, parentAttributes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return attributes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isInherited() {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								packages/core/database/src/inherited-map.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								packages/core/database/src/inherited-map.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
			
		||||
import lodash from 'lodash';
 | 
			
		||||
 | 
			
		||||
class TableNode {
 | 
			
		||||
  name: string;
 | 
			
		||||
  parents: Set<TableNode>;
 | 
			
		||||
  children: Set<TableNode>;
 | 
			
		||||
  constructor(name: string) {
 | 
			
		||||
    this.name = name;
 | 
			
		||||
    this.parents = new Set();
 | 
			
		||||
    this.children = new Set();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class InheritanceMap {
 | 
			
		||||
  nodes: Map<string, TableNode> = new Map<string, TableNode>();
 | 
			
		||||
 | 
			
		||||
  getOrCreateNode(name: string) {
 | 
			
		||||
    if (!this.nodes.has(name)) {
 | 
			
		||||
      this.nodes.set(name, new TableNode(name));
 | 
			
		||||
    }
 | 
			
		||||
    return this.getNode(name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNode(name: string) {
 | 
			
		||||
    return this.nodes.get(name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setInheritance(name: string, inherits: string | string[]) {
 | 
			
		||||
    const node = this.getOrCreateNode(name);
 | 
			
		||||
    const parents = lodash.castArray(inherits).map((name) => this.getOrCreateNode(name));
 | 
			
		||||
 | 
			
		||||
    node.parents = new Set(parents);
 | 
			
		||||
 | 
			
		||||
    for (const parent of parents) {
 | 
			
		||||
      parent.children.add(node);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isParentNode(name: string) {
 | 
			
		||||
    const node = this.getNode(name);
 | 
			
		||||
    return node && node.children.size > 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getChildren(name: string, options: { deep: boolean } = { deep: true }): Set<string> {
 | 
			
		||||
    const results = new Set<string>();
 | 
			
		||||
    const node = this.getNode(name);
 | 
			
		||||
    if (!node) return results;
 | 
			
		||||
 | 
			
		||||
    for (const child of node.children) {
 | 
			
		||||
      results.add(child.name);
 | 
			
		||||
      if (!options.deep) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const grandchild of this.getChildren(child.name)) {
 | 
			
		||||
        results.add(grandchild);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return results;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getParents(name: string, options: { deep: boolean } = { deep: true }): Set<string> {
 | 
			
		||||
    const results = new Set<string>();
 | 
			
		||||
    const node = this.getNode(name);
 | 
			
		||||
    if (!node) return results;
 | 
			
		||||
 | 
			
		||||
    for (const parent of node.parents) {
 | 
			
		||||
      results.add(parent.name);
 | 
			
		||||
      if (!options.deep) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const grandparent of this.getParents(parent.name)) {
 | 
			
		||||
        results.add(grandparent);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return results;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,8 @@ import { Model as SequelizeModel, ModelCtor } from 'sequelize';
 | 
			
		||||
import { Collection } from './collection';
 | 
			
		||||
import { Database } from './database';
 | 
			
		||||
import { Field } from './fields';
 | 
			
		||||
import type { InheritedCollection } from './inherited-collection';
 | 
			
		||||
import { SyncRunner } from './sync-runner';
 | 
			
		||||
 | 
			
		||||
interface IModel {
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
@ -145,4 +147,14 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
 | 
			
		||||
 | 
			
		||||
    return lodash.orderBy(data, orderItems, orderDirections);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async sync(options) {
 | 
			
		||||
    const model = this as any;
 | 
			
		||||
 | 
			
		||||
    if (this.collection.isInherited()) {
 | 
			
		||||
      return SyncRunner.syncInheritModel(model, options);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return SequelizeModel.sync.call(this, options);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -104,6 +104,13 @@ export class OptionsParser {
 | 
			
		||||
    return filterParams;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected inheritFromSubQuery(): any {
 | 
			
		||||
    return [
 | 
			
		||||
      Sequelize.literal(`(select relname from pg_class where pg_class.oid = "${this.collection.name}".tableoid)`),
 | 
			
		||||
      '__tableName',
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected parseFields(filterParams: any) {
 | 
			
		||||
    const appends = this.options?.appends || [];
 | 
			
		||||
    const except = [];
 | 
			
		||||
@ -113,6 +120,10 @@ export class OptionsParser {
 | 
			
		||||
      exclude: [],
 | 
			
		||||
    }; // out put all fields by default
 | 
			
		||||
 | 
			
		||||
    if (this.collection.isParent()) {
 | 
			
		||||
      attributes.include.push(this.inheritFromSubQuery());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.options?.fields) {
 | 
			
		||||
      // 将fields拆分为 attributes 和 appends
 | 
			
		||||
      for (const field of this.options.fields) {
 | 
			
		||||
 | 
			
		||||
@ -232,6 +232,8 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
 | 
			
		||||
      ...this.buildQueryOptions(options),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let rows;
 | 
			
		||||
 | 
			
		||||
    if (opts.include && opts.include.length > 0) {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      const primaryKeyField = model.primaryKeyField || model.primaryKeyAttribute;
 | 
			
		||||
@ -258,29 +260,40 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      return await handleAppendsQuery({
 | 
			
		||||
      rows = await handleAppendsQuery({
 | 
			
		||||
        queryPromises: opts.include.map((include) => {
 | 
			
		||||
          return model
 | 
			
		||||
            .findAll({
 | 
			
		||||
          const options = {
 | 
			
		||||
            ...omit(opts, ['limit', 'offset']),
 | 
			
		||||
            include: include,
 | 
			
		||||
            where,
 | 
			
		||||
            transaction,
 | 
			
		||||
            })
 | 
			
		||||
            .then((rows) => {
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          return model.findAll(options).then((rows) => {
 | 
			
		||||
            return { rows, include };
 | 
			
		||||
          });
 | 
			
		||||
        }),
 | 
			
		||||
        templateModel: ids[0].row,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return await model.findAll({
 | 
			
		||||
    } else {
 | 
			
		||||
      rows = await model.findAll({
 | 
			
		||||
        ...opts,
 | 
			
		||||
        transaction,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.collection.isParent()) {
 | 
			
		||||
      for (const row of rows) {
 | 
			
		||||
        const rowCollectionName = this.database.tableNameCollectionMap.get(row.get('__tableName')).name;
 | 
			
		||||
        row.set('__collection', rowCollectionName, {
 | 
			
		||||
          raw: true,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return rows;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * find and count
 | 
			
		||||
   * @param options
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										117
									
								
								packages/core/database/src/sync-runner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								packages/core/database/src/sync-runner.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
			
		||||
import { InheritedCollection } from './inherited-collection';
 | 
			
		||||
import lodash from 'lodash';
 | 
			
		||||
import { Sequelize } from 'sequelize';
 | 
			
		||||
 | 
			
		||||
export class SyncRunner {
 | 
			
		||||
  static async syncInheritModel(model: any, options: any) {
 | 
			
		||||
    const { transaction } = options;
 | 
			
		||||
 | 
			
		||||
    const inheritedCollection = model.collection as InheritedCollection;
 | 
			
		||||
    const db = inheritedCollection.context.database;
 | 
			
		||||
    const dialect = db.sequelize.getDialect();
 | 
			
		||||
 | 
			
		||||
    const queryInterface = db.sequelize.getQueryInterface();
 | 
			
		||||
 | 
			
		||||
    if (dialect != 'postgres') {
 | 
			
		||||
      throw new Error('Inherit model is only supported on postgres');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parents = inheritedCollection.parents;
 | 
			
		||||
 | 
			
		||||
    const parentTables = parents.map((parent) => parent.model.tableName);
 | 
			
		||||
 | 
			
		||||
    const tableName = model.getTableName();
 | 
			
		||||
 | 
			
		||||
    const attributes = model.tableAttributes;
 | 
			
		||||
 | 
			
		||||
    const childAttributes = lodash.pickBy(attributes, (value) => {
 | 
			
		||||
      return !value.inherit;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let maxSequenceVal = 0;
 | 
			
		||||
    let maxSequenceName;
 | 
			
		||||
 | 
			
		||||
    if (childAttributes.id && childAttributes.id.autoIncrement) {
 | 
			
		||||
      for (const parent of parentTables) {
 | 
			
		||||
        const sequenceNameResult = await queryInterface.sequelize.query(
 | 
			
		||||
          `select pg_get_serial_sequence('"${parent}"', 'id')`,
 | 
			
		||||
          {
 | 
			
		||||
            transaction,
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
        const sequenceName = sequenceNameResult[0][0]['pg_get_serial_sequence'];
 | 
			
		||||
 | 
			
		||||
        const sequenceCurrentValResult = await queryInterface.sequelize.query(
 | 
			
		||||
          `select last_value from ${sequenceName}`,
 | 
			
		||||
          {
 | 
			
		||||
            transaction,
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
        const sequenceCurrentVal = sequenceCurrentValResult[0][0]['last_value'];
 | 
			
		||||
 | 
			
		||||
        if (sequenceCurrentVal > maxSequenceVal) {
 | 
			
		||||
          maxSequenceName = sequenceName;
 | 
			
		||||
          maxSequenceVal = sequenceCurrentVal;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await this.createTable(tableName, childAttributes, options, model, parentTables);
 | 
			
		||||
 | 
			
		||||
    const parentsDeep = Array.from(db.inheritanceMap.getParents(inheritedCollection.name)).map(
 | 
			
		||||
      (parent) => db.getCollection(parent).model.tableName,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const sequenceTables = [...parentsDeep, tableName];
 | 
			
		||||
 | 
			
		||||
    for (const sequenceTable of sequenceTables) {
 | 
			
		||||
      await queryInterface.sequelize.query(
 | 
			
		||||
        `alter table "${sequenceTable}" alter column id set default nextval('${maxSequenceName}')`,
 | 
			
		||||
        {
 | 
			
		||||
          transaction,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (options.alter) {
 | 
			
		||||
      const columns = await queryInterface.describeTable(tableName, options);
 | 
			
		||||
 | 
			
		||||
      for (const columnName in childAttributes) {
 | 
			
		||||
        if (!columns[columnName]) {
 | 
			
		||||
          await queryInterface.addColumn(tableName, columnName, childAttributes[columnName], options);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async createTable(tableName, attributes, options, model, parentTables) {
 | 
			
		||||
    let sql = '';
 | 
			
		||||
 | 
			
		||||
    options = { ...options };
 | 
			
		||||
 | 
			
		||||
    if (options && options.uniqueKeys) {
 | 
			
		||||
      lodash.forOwn(options.uniqueKeys, (uniqueKey) => {
 | 
			
		||||
        if (uniqueKey.customIndex === undefined) {
 | 
			
		||||
          uniqueKey.customIndex = true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (model) {
 | 
			
		||||
      options.uniqueKeys = options.uniqueKeys || model.uniqueKeys;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const queryGenerator = model.queryGenerator;
 | 
			
		||||
 | 
			
		||||
    attributes = lodash.mapValues(attributes, (attribute) => model.sequelize.normalizeAttribute(attribute));
 | 
			
		||||
 | 
			
		||||
    attributes = queryGenerator.attributesToSQL(attributes, { table: tableName, context: 'createTable' });
 | 
			
		||||
 | 
			
		||||
    sql = `${queryGenerator.createTableQuery(tableName, attributes, options)}`.replace(
 | 
			
		||||
      ';',
 | 
			
		||||
      ` INHERITS (${parentTables.map((t) => `"${t}"`).join(', ')});`,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return await model.sequelize.query(sql, options);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
export { mockDatabase } from '@nocobase/database';
 | 
			
		||||
export * from './mockServer';
 | 
			
		||||
 | 
			
		||||
const pgOnly: () => jest.Describe = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
 | 
			
		||||
export { pgOnly };
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ export class ClientPlugin extends Plugin {
 | 
			
		||||
    this.app.acl.allow('app', 'getInfo');
 | 
			
		||||
    this.app.acl.allow('app', 'getPlugins');
 | 
			
		||||
    this.app.acl.allow('plugins', 'getPinned', 'loggedIn');
 | 
			
		||||
    const dialect = this.app.db.sequelize.getDialect();
 | 
			
		||||
    this.app.resource({
 | 
			
		||||
      name: 'app',
 | 
			
		||||
      actions: {
 | 
			
		||||
@ -35,6 +36,9 @@ export class ClientPlugin extends Plugin {
 | 
			
		||||
            lang = currentUser?.appLang;
 | 
			
		||||
          }
 | 
			
		||||
          ctx.body = {
 | 
			
		||||
            database: {
 | 
			
		||||
              dialect,
 | 
			
		||||
            },
 | 
			
		||||
            version: await ctx.app.version.get(),
 | 
			
		||||
            lang,
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
@ -8,9 +8,7 @@ describe('collections repository', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    app = await createApp();
 | 
			
		||||
    agent = app.agent();
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('collections')
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'tags',
 | 
			
		||||
        fields: [
 | 
			
		||||
@ -21,9 +19,7 @@ describe('collections repository', () => {
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('collections')
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'foos',
 | 
			
		||||
        fields: [
 | 
			
		||||
@ -34,18 +30,14 @@ describe('collections repository', () => {
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('collections.fields', 'tags')
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('collections.fields', 'tags').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'foos',
 | 
			
		||||
        target: 'foos',
 | 
			
		||||
        type: 'belongsToMany',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('collections')
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'comments',
 | 
			
		||||
        fields: [
 | 
			
		||||
@ -56,9 +48,7 @@ describe('collections repository', () => {
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('collections')
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'posts',
 | 
			
		||||
        fields: [
 | 
			
		||||
@ -95,23 +85,17 @@ describe('collections repository', () => {
 | 
			
		||||
  it('case 2', async () => {
 | 
			
		||||
    const response = await app.agent().resource('posts').create();
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 1',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 2',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const response2 = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .list({
 | 
			
		||||
    const response2 = await agent.resource('posts').list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        'comments.title': 'comment 1',
 | 
			
		||||
      },
 | 
			
		||||
@ -122,16 +106,12 @@ describe('collections repository', () => {
 | 
			
		||||
  it('case 3', async () => {
 | 
			
		||||
    const response = await app.agent().resource('posts').create();
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 1',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const response2 = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .list({
 | 
			
		||||
    const response2 = await agent.resource('posts').list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        'comments.id': 3,
 | 
			
		||||
      },
 | 
			
		||||
@ -142,16 +122,12 @@ describe('collections repository', () => {
 | 
			
		||||
  it('case 4', async () => {
 | 
			
		||||
    const response = await app.agent().resource('posts').create();
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 1',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const response2 = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .list({
 | 
			
		||||
    const response2 = await agent.resource('posts').list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        $and: [
 | 
			
		||||
          {
 | 
			
		||||
@ -174,9 +150,7 @@ describe('collections repository', () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('case 6', async () => {
 | 
			
		||||
    const response = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .create({
 | 
			
		||||
    const response = await agent.resource('posts').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        tags: [
 | 
			
		||||
          {},
 | 
			
		||||
@ -192,9 +166,7 @@ describe('collections repository', () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('case 7', async () => {
 | 
			
		||||
    const response = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .create({
 | 
			
		||||
    const response = await agent.resource('posts').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        tags: [
 | 
			
		||||
          {},
 | 
			
		||||
@ -205,9 +177,7 @@ describe('collections repository', () => {
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    const response1 = await agent
 | 
			
		||||
      .resource('posts.tags', postId)
 | 
			
		||||
      .list({
 | 
			
		||||
    const response1 = await agent.resource('posts.tags', postId).list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        title: 'Tag1',
 | 
			
		||||
      },
 | 
			
		||||
@ -216,9 +186,7 @@ describe('collections repository', () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('case 8', async () => {
 | 
			
		||||
    const response = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .create({
 | 
			
		||||
    const response = await agent.resource('posts').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        tags: [
 | 
			
		||||
          {},
 | 
			
		||||
@ -232,9 +200,7 @@ describe('collections repository', () => {
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    const response1 = await agent
 | 
			
		||||
      .resource('posts.tags', postId)
 | 
			
		||||
      .list({
 | 
			
		||||
    const response1 = await agent.resource('posts.tags', postId).list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        $or: [{ title: 'Tag1' }, { title: 'Tag2' }],
 | 
			
		||||
      },
 | 
			
		||||
@ -243,9 +209,7 @@ describe('collections repository', () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('case 9', async () => {
 | 
			
		||||
    const response = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .create({
 | 
			
		||||
    const response = await agent.resource('posts').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        tags: [
 | 
			
		||||
          {},
 | 
			
		||||
@ -259,9 +223,7 @@ describe('collections repository', () => {
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    const response1 = await agent
 | 
			
		||||
      .resource('posts.tags', postId)
 | 
			
		||||
      .list({
 | 
			
		||||
    const response1 = await agent.resource('posts.tags', postId).list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        $or: [{ title: 'Tag1' }, { title: 'Tag2' }],
 | 
			
		||||
      },
 | 
			
		||||
@ -270,9 +232,7 @@ describe('collections repository', () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('case 10', async () => {
 | 
			
		||||
    const response = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .create({
 | 
			
		||||
    const response = await agent.resource('posts').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        tags: [
 | 
			
		||||
          {},
 | 
			
		||||
@ -286,9 +246,7 @@ describe('collections repository', () => {
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    const response1 = await agent
 | 
			
		||||
      .resource('posts.tags', postId)
 | 
			
		||||
      .list({
 | 
			
		||||
    const response1 = await agent.resource('posts.tags', postId).list({
 | 
			
		||||
      appends: ['foos'],
 | 
			
		||||
      page: 1,
 | 
			
		||||
      pageSize: 20,
 | 
			
		||||
@ -301,39 +259,29 @@ describe('collections repository', () => {
 | 
			
		||||
  it('case 11', async () => {
 | 
			
		||||
    const response = await app.agent().resource('posts').create();
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 1',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 2',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const response2 = await app.agent().resource('posts').create();
 | 
			
		||||
    const postId2 = response2.body.data.id;
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId2)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId2).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 2',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId2)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId2).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 2',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const response3 = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .list({
 | 
			
		||||
    const response3 = await agent.resource('posts').list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        $or: [
 | 
			
		||||
          {
 | 
			
		||||
@ -351,30 +299,22 @@ describe('collections repository', () => {
 | 
			
		||||
  it('case 12', async () => {
 | 
			
		||||
    const response = await app.agent().resource('posts').create();
 | 
			
		||||
    const postId = response.body.data.id;
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 1',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 2',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts.comments', postId).create({
 | 
			
		||||
      values: {
 | 
			
		||||
        title: 'comment 3',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    const response2 = await agent
 | 
			
		||||
      .resource('posts.comments', postId)
 | 
			
		||||
      .list({
 | 
			
		||||
    const response2 = await agent.resource('posts.comments', postId).list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        $or: [
 | 
			
		||||
          {
 | 
			
		||||
@ -394,31 +334,23 @@ describe('collections repository', () => {
 | 
			
		||||
    const tag1 = await tagRepository.create({ values: { title: 'tag1' } });
 | 
			
		||||
    const tag2 = await tagRepository.create({ values: { title: 'tag2' } });
 | 
			
		||||
    const tag3 = await tagRepository.create({ values: { title: 'tag3' } });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        tags: [tag1.get('id'), tag3.get('id')],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        tags: [tag2.get('id')],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .create({
 | 
			
		||||
    await agent.resource('posts').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        tags: [tag2.get('id'), tag3.get('id')],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const response1 = await agent
 | 
			
		||||
      .resource('posts')
 | 
			
		||||
      .list({
 | 
			
		||||
    const response1 = await agent.resource('posts').list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        $or: [{ 'tags.title': 'tag1' }, { 'tags.title': 'tag3' }],
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,281 @@
 | 
			
		||||
import { MockServer, pgOnly } from '@nocobase/test';
 | 
			
		||||
import { createApp } from '..';
 | 
			
		||||
 | 
			
		||||
pgOnly()('Inherited Collection', () => {
 | 
			
		||||
  let app: MockServer;
 | 
			
		||||
  let agent;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    app = await createApp();
 | 
			
		||||
    agent = app.agent();
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    await app.destroy();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should not replace field with difference type when add field', async () => {
 | 
			
		||||
    let response = await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'students',
 | 
			
		||||
        inherits: 'person',
 | 
			
		||||
        fields: [],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(response.statusCode).toBe(200);
 | 
			
		||||
 | 
			
		||||
    response = await agent.resource('fields').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        collectionName: 'students',
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'integer',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(response.statusCode).not.toBe(200);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should not replace field with difference type when create collection', async () => {
 | 
			
		||||
    const response = await agent.resource('collections').create({
 | 
			
		||||
      context: {},
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'students',
 | 
			
		||||
        inherits: ['person'],
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    expect(response.statusCode).toBe(500);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('can  create relation with child table', async () => {
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'a',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'af',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'bs',
 | 
			
		||||
            type: 'hasMany',
 | 
			
		||||
            target: 'b',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'b',
 | 
			
		||||
        inherits: ['a'],
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'bf',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'a',
 | 
			
		||||
            type: 'belongsTo',
 | 
			
		||||
            target: 'a',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const res = await agent.resource('b').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        af: 'a1',
 | 
			
		||||
        bs: [{ bf: 'b1' }, { bf: 'b2' }],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(res.statusCode).toEqual(200);
 | 
			
		||||
 | 
			
		||||
    const a1 = await agent.resource('b').list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        af: 'a1',
 | 
			
		||||
      },
 | 
			
		||||
      appends: 'bs',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(a1.body.data[0].bs.length).toEqual(2);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('can drop child replaced field', async () => {
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'students',
 | 
			
		||||
        inherits: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const response = await agent.resource('collections.fields', 'students').destroy({
 | 
			
		||||
      filter: {
 | 
			
		||||
        name: 'name',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(response.status).toBe(200);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should reload collection when parent fields change', async () => {
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'employee',
 | 
			
		||||
        inherits: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'salary',
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'instructor',
 | 
			
		||||
        inherits: 'employee',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'rank',
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createInstructorResponse = await agent.resource('instructor').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'foo',
 | 
			
		||||
        salary: 1000,
 | 
			
		||||
        rank: 100,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(createInstructorResponse.statusCode).toBe(200);
 | 
			
		||||
 | 
			
		||||
    const employeeCollection = app.db.getCollection('employee');
 | 
			
		||||
    expect(employeeCollection.fields.get('new-field')).not.toBeDefined();
 | 
			
		||||
 | 
			
		||||
    // add new field to root collection
 | 
			
		||||
    await agent.resource('fields').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        collectionName: 'person',
 | 
			
		||||
        name: 'age',
 | 
			
		||||
        type: 'integer',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(employeeCollection.fields.get('age')).toBeDefined();
 | 
			
		||||
 | 
			
		||||
    let listResponse = await agent.resource('employee').list();
 | 
			
		||||
    expect(listResponse.statusCode).toBe(200);
 | 
			
		||||
 | 
			
		||||
    expect(listResponse.body.data[0].age).toBeDefined();
 | 
			
		||||
 | 
			
		||||
    listResponse = await agent.resource('instructor').list();
 | 
			
		||||
    expect(listResponse.statusCode).toBe(200);
 | 
			
		||||
 | 
			
		||||
    expect(listResponse.body.data[0].age).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create inherited collection', async () => {
 | 
			
		||||
    const response = await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'students',
 | 
			
		||||
        inherits: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'score',
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(response.statusCode).toBe(200);
 | 
			
		||||
 | 
			
		||||
    const studentCollection = app.db.getCollection('students');
 | 
			
		||||
    expect(studentCollection).toBeDefined();
 | 
			
		||||
 | 
			
		||||
    const studentFieldsResponse = await agent.resource('fields').list({
 | 
			
		||||
      filter: {
 | 
			
		||||
        collectionName: 'students',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(studentFieldsResponse.statusCode).toBe(200);
 | 
			
		||||
 | 
			
		||||
    const studentFields = studentFieldsResponse.body.data;
 | 
			
		||||
    expect(studentFields.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
    const createStudentResponse = await agent.resource('students').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'foo',
 | 
			
		||||
        score: 100,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(createStudentResponse.statusCode).toBe(200);
 | 
			
		||||
 | 
			
		||||
    const fooStudent = createStudentResponse.body.data;
 | 
			
		||||
 | 
			
		||||
    expect(fooStudent.name).toBe('foo');
 | 
			
		||||
 | 
			
		||||
    const studentList = await agent.resource('students').list();
 | 
			
		||||
    expect(studentList.statusCode).toBe(200);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should know which child table row it is', async () => {
 | 
			
		||||
    await agent.resource('collections').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'students',
 | 
			
		||||
        inherits: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'score',
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await agent.resource('students').create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'foo',
 | 
			
		||||
        score: 100,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const personList = await agent.resource('person').list();
 | 
			
		||||
 | 
			
		||||
    const person = personList.body.data[0];
 | 
			
		||||
 | 
			
		||||
    expect(person['__collection']).toBe('students');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,320 @@
 | 
			
		||||
import Database, { Repository } from '@nocobase/database';
 | 
			
		||||
import Application from '@nocobase/server';
 | 
			
		||||
import { createApp } from '..';
 | 
			
		||||
import { pgOnly } from '@nocobase/test';
 | 
			
		||||
 | 
			
		||||
pgOnly()('Inherited Collection', () => {
 | 
			
		||||
  let db: Database;
 | 
			
		||||
  let app: Application;
 | 
			
		||||
 | 
			
		||||
  let collectionRepository: Repository;
 | 
			
		||||
 | 
			
		||||
  let fieldsRepository: Repository;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    app = await createApp();
 | 
			
		||||
 | 
			
		||||
    db = app.db;
 | 
			
		||||
 | 
			
		||||
    collectionRepository = db.getCollection('collections').repository;
 | 
			
		||||
    fieldsRepository = db.getCollection('fields').repository;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    await app.destroy();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("should not delete child's field when parent field delete that inherits from multiple table", async () => {
 | 
			
		||||
    await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'b',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'c',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'a',
 | 
			
		||||
        inherits: ['b', 'c'],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await fieldsRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        collectionName: 'a',
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('fields').repository.destroy({
 | 
			
		||||
      filter: {
 | 
			
		||||
        collectionName: 'b',
 | 
			
		||||
        name: 'name',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      await db.getCollection('fields').repository.findOne({
 | 
			
		||||
        filter: {
 | 
			
		||||
          collectionName: 'a',
 | 
			
		||||
          name: 'name',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    ).not.toBeNull();
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('fields').repository.destroy({
 | 
			
		||||
      filter: {
 | 
			
		||||
        collectionName: 'c',
 | 
			
		||||
        name: 'name',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      await db.getCollection('fields').repository.findOne({
 | 
			
		||||
        filter: {
 | 
			
		||||
          collectionName: 'a',
 | 
			
		||||
          name: 'name',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    ).toBeNull();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("should delete child's field when parent field deleted", async () => {
 | 
			
		||||
    await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'students',
 | 
			
		||||
        inherits: ['person'],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('fields').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        collectionName: 'students',
 | 
			
		||||
        name: 'name',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('fields').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        collectionName: 'students',
 | 
			
		||||
        name: 'age',
 | 
			
		||||
        type: 'integer',
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const childNameField = await db.getCollection('fields').repository.findOne({
 | 
			
		||||
      filter: {
 | 
			
		||||
        collectionName: 'students',
 | 
			
		||||
        name: 'name',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(childNameField.get('overriding')).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('fields').repository.destroy({
 | 
			
		||||
      filter: {
 | 
			
		||||
        collectionName: 'person',
 | 
			
		||||
        name: 'name',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      await db.getCollection('fields').repository.findOne({
 | 
			
		||||
        filter: {
 | 
			
		||||
          collectionName: 'students',
 | 
			
		||||
          name: 'name',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    ).toBeNull();
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      await db.getCollection('fields').repository.findOne({
 | 
			
		||||
        filter: {
 | 
			
		||||
          collectionName: 'students',
 | 
			
		||||
          name: 'age',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    ).not.toBeNull();
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('fields').repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        collectionName: 'person',
 | 
			
		||||
        name: 'age',
 | 
			
		||||
        type: 'integer',
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.getCollection('fields').repository.destroy({
 | 
			
		||||
      filter: {
 | 
			
		||||
        collectionName: 'person',
 | 
			
		||||
        name: 'age',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      await db.getCollection('fields').repository.findOne({
 | 
			
		||||
        filter: {
 | 
			
		||||
          collectionName: 'person',
 | 
			
		||||
          name: 'age',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    ).toBeNull();
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
      await db.getCollection('fields').repository.findOne({
 | 
			
		||||
        filter: {
 | 
			
		||||
          collectionName: 'students',
 | 
			
		||||
          name: 'age',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    ).not.toBeNull();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should not inherit with difference type', async () => {
 | 
			
		||||
    const personCollection = await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let err;
 | 
			
		||||
    try {
 | 
			
		||||
      const studentCollection = await collectionRepository.create({
 | 
			
		||||
        values: {
 | 
			
		||||
          name: 'students',
 | 
			
		||||
          inherits: 'person',
 | 
			
		||||
          fields: [
 | 
			
		||||
            {
 | 
			
		||||
              name: 'name',
 | 
			
		||||
              type: 'integer',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
        context: {},
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      err = e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    expect(err).toBeDefined();
 | 
			
		||||
    expect(err.message.includes('type conflict')).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should replace parent collection field', async () => {
 | 
			
		||||
    const personCollection = await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const studentCollection = await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'students',
 | 
			
		||||
        inherits: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
            title: '姓名',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const studentFields = await studentCollection.getFields();
 | 
			
		||||
    expect(studentFields.length).toBe(1);
 | 
			
		||||
    expect(studentFields[0].get('title')).toBe('姓名');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should remove parent collections field', async () => {
 | 
			
		||||
    await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'person',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'name',
 | 
			
		||||
            type: 'string',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await collectionRepository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'students',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'score',
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      context: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const studentCollection = await db.getCollection('students');
 | 
			
		||||
 | 
			
		||||
    console.log(studentCollection.fields);
 | 
			
		||||
    await studentCollection.repository.create({
 | 
			
		||||
      values: {
 | 
			
		||||
        name: 'foo',
 | 
			
		||||
        score: 100,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,57 @@
 | 
			
		||||
import Database, { Collection as DBCollection } from '@nocobase/database';
 | 
			
		||||
import Application from '@nocobase/server';
 | 
			
		||||
import { createApp } from '.';
 | 
			
		||||
 | 
			
		||||
describe('sync collection', () => {
 | 
			
		||||
  let db: Database;
 | 
			
		||||
  let app: Application;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    app = await createApp();
 | 
			
		||||
    db = app.db;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    await app.destroy();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should not remove column when async with drop false', async () => {
 | 
			
		||||
    const getTableInfo = async (tableName: string) => {
 | 
			
		||||
      const queryInterface = db.sequelize.getQueryInterface();
 | 
			
		||||
      const tableInfo = await queryInterface.describeTable(tableName);
 | 
			
		||||
      return tableInfo;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const c1 = db.collection({
 | 
			
		||||
      name: 'c1',
 | 
			
		||||
      fields: [{ type: 'string', name: 'f1' }],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await db.sync({
 | 
			
		||||
      force: false,
 | 
			
		||||
      alter: {
 | 
			
		||||
        drop: false,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let tableInfo1 = await getTableInfo(c1.model.tableName);
 | 
			
		||||
    expect(tableInfo1.f1).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
    c1.setField('f2', {
 | 
			
		||||
      type: 'string',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    c1.removeField('f1');
 | 
			
		||||
 | 
			
		||||
    await db.sync({
 | 
			
		||||
      force: false,
 | 
			
		||||
      alter: {
 | 
			
		||||
        drop: false,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    let tableInfo2 = await getTableInfo(c1.model.tableName);
 | 
			
		||||
    expect(tableInfo2.f2).toBeTruthy();
 | 
			
		||||
    expect(tableInfo2.f1).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import Database, { Collection, MagicAttributeModel } from '@nocobase/database';
 | 
			
		||||
import { SyncOptions, Transactionable } from 'sequelize';
 | 
			
		||||
import { FieldModel } from './field';
 | 
			
		||||
import lodash from 'lodash';
 | 
			
		||||
 | 
			
		||||
interface LoadOptions extends Transactionable {
 | 
			
		||||
  // TODO
 | 
			
		||||
@ -19,25 +20,27 @@ export class CollectionModel extends MagicAttributeModel {
 | 
			
		||||
 | 
			
		||||
    let collection: Collection;
 | 
			
		||||
 | 
			
		||||
    const collectionOptions = {
 | 
			
		||||
      ...this.get(),
 | 
			
		||||
      fields: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (this.db.hasCollection(name)) {
 | 
			
		||||
      collection = this.db.getCollection(name);
 | 
			
		||||
 | 
			
		||||
      if (skipExist) {
 | 
			
		||||
        return collection;
 | 
			
		||||
      }
 | 
			
		||||
      collection.updateOptions({
 | 
			
		||||
        ...this.get(),
 | 
			
		||||
        fields: [],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      collection.updateOptions(collectionOptions);
 | 
			
		||||
    } else {
 | 
			
		||||
      collection = this.db.collection({
 | 
			
		||||
        ...this.get(),
 | 
			
		||||
        fields: [],
 | 
			
		||||
      });
 | 
			
		||||
      collection = this.db.collection(collectionOptions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!skipField) {
 | 
			
		||||
      await this.loadFields({ transaction });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return collection;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -83,6 +86,7 @@ export class CollectionModel extends MagicAttributeModel {
 | 
			
		||||
    const collection = await this.load({
 | 
			
		||||
      transaction: options?.transaction,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await collection.sync({
 | 
			
		||||
        force: false,
 | 
			
		||||
@ -92,8 +96,73 @@ export class CollectionModel extends MagicAttributeModel {
 | 
			
		||||
        ...options,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(error);
 | 
			
		||||
      const name = this.get('name');
 | 
			
		||||
      this.db.removeCollection(name);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isInheritedModel() {
 | 
			
		||||
    return this.get('inherits');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async findParents(options: Transactionable) {
 | 
			
		||||
    const { transaction } = options;
 | 
			
		||||
 | 
			
		||||
    const findModelParents = async (model: CollectionModel, carry = []) => {
 | 
			
		||||
      if (!model.get('inherits')) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const parents = lodash.castArray(model.get('inherits'));
 | 
			
		||||
 | 
			
		||||
      for (const parent of parents) {
 | 
			
		||||
        const parentModel = (await this.db.getCollection('collections').repository.findOne({
 | 
			
		||||
          filterByTk: parent,
 | 
			
		||||
          transaction,
 | 
			
		||||
        })) as CollectionModel;
 | 
			
		||||
 | 
			
		||||
        carry.push(parentModel.get('name'));
 | 
			
		||||
 | 
			
		||||
        await findModelParents(parentModel, carry);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return carry;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return findModelParents(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async parentFields(options: Transactionable) {
 | 
			
		||||
    const { transaction } = options;
 | 
			
		||||
 | 
			
		||||
    return this.db.getCollection('fields').repository.find({
 | 
			
		||||
      filter: {
 | 
			
		||||
        collectionName: { $in: await this.findParents({ transaction }) },
 | 
			
		||||
      },
 | 
			
		||||
      transaction,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // sync fields from parents
 | 
			
		||||
  async syncParentFields(options: Transactionable) {
 | 
			
		||||
    const { transaction } = options;
 | 
			
		||||
 | 
			
		||||
    const ancestorFields = await this.parentFields({ transaction });
 | 
			
		||||
 | 
			
		||||
    const selfFields = await this.getFields({ transaction });
 | 
			
		||||
 | 
			
		||||
    const inheritedFields = ancestorFields.filter((field: FieldModel) => {
 | 
			
		||||
      return (
 | 
			
		||||
        !field.isAssociationField() &&
 | 
			
		||||
        !selfFields.find((selfField: FieldModel) => selfField.get('name') == field.get('name'))
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (const inheritedField of inheritedFields) {
 | 
			
		||||
      await this.createField(lodash.omit(inheritedField.toJSON(), ['key', 'collectionName', 'sort']), {
 | 
			
		||||
        transaction,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -67,15 +67,21 @@ export class FieldModel extends MagicAttributeModel {
 | 
			
		||||
    return (<any>this.constructor).database;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isAssociationField() {
 | 
			
		||||
    return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(this.get('type'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async load(loadOptions?: LoadOptions) {
 | 
			
		||||
    const { skipExist = false } = loadOptions || {};
 | 
			
		||||
    const collectionName = this.get('collectionName');
 | 
			
		||||
 | 
			
		||||
    if (!this.db.hasCollection(collectionName)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const collection = this.db.getCollection(collectionName);
 | 
			
		||||
    const name = this.get('name');
 | 
			
		||||
 | 
			
		||||
    if (skipExist && collection.hasField(name)) {
 | 
			
		||||
      return collection.getField(name);
 | 
			
		||||
    }
 | 
			
		||||
@ -123,6 +129,7 @@ export class FieldModel extends MagicAttributeModel {
 | 
			
		||||
    if (!field) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return field.removeFromDb({
 | 
			
		||||
      transaction: options.transaction,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ import {
 | 
			
		||||
} from './hooks';
 | 
			
		||||
 | 
			
		||||
import { CollectionModel, FieldModel } from './models';
 | 
			
		||||
import { InheritedCollection } from '@nocobase/database';
 | 
			
		||||
 | 
			
		||||
export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
  async beforeLoad() {
 | 
			
		||||
@ -56,6 +57,20 @@ export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
    // 要在 beforeInitOptions 之前处理
 | 
			
		||||
    this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db));
 | 
			
		||||
    this.app.db.on('fields.beforeCreate', beforeCreateForChildrenCollection(this.app.db));
 | 
			
		||||
 | 
			
		||||
    this.app.db.on('fields.beforeCreate', async (model, options) => {
 | 
			
		||||
      const collectionName = model.get('collectionName');
 | 
			
		||||
      const collection = this.app.db.getCollection(collectionName);
 | 
			
		||||
 | 
			
		||||
      if (!collection) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (collection.isInherited() && (<InheritedCollection>collection).parentFields().has(model.get('name'))) {
 | 
			
		||||
        model.set('overriding', true);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.db.on('fields.beforeCreate', async (model, options) => {
 | 
			
		||||
      const type = model.get('type');
 | 
			
		||||
      const fn = beforeInitOptions[type];
 | 
			
		||||
@ -66,14 +81,16 @@ export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
 | 
			
		||||
    this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db));
 | 
			
		||||
 | 
			
		||||
    this.app.db.on('collections.afterCreateWithAssociations', async (model, { context, transaction }) => {
 | 
			
		||||
    this.app.db.on(
 | 
			
		||||
      'collections.afterCreateWithAssociations',
 | 
			
		||||
      async (model: CollectionModel, { context, transaction }) => {
 | 
			
		||||
        if (context) {
 | 
			
		||||
          await model.migrate({
 | 
			
		||||
          isNew: true,
 | 
			
		||||
            transaction,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.app.db.on('fields.afterCreate', async (model: FieldModel, { context, transaction }) => {
 | 
			
		||||
      if (context) {
 | 
			
		||||
@ -123,7 +140,7 @@ export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
 | 
			
		||||
    // before field remove
 | 
			
		||||
    this.app.db.on('fields.beforeDestroy', beforeDestroyForeignKey(this.app.db));
 | 
			
		||||
    this.app.db.on('fields.beforeDestroy', async (model, options) => {
 | 
			
		||||
    this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => {
 | 
			
		||||
      await model.remove(options);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -131,6 +148,37 @@ export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
      await model.remove(options);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.db.on('fields.afterDestroy', async (model: FieldModel, options) => {
 | 
			
		||||
      const { transaction } = options;
 | 
			
		||||
      const collectionName = model.get('collectionName');
 | 
			
		||||
      const childCollections = this.db.inheritanceMap.getChildren(collectionName);
 | 
			
		||||
 | 
			
		||||
      const childShouldRemoveField = Array.from(childCollections).filter((item) => {
 | 
			
		||||
        const parents = Array.from(this.db.inheritanceMap.getParents(item))
 | 
			
		||||
          .map((parent) => {
 | 
			
		||||
            const collection = this.db.getCollection(parent);
 | 
			
		||||
            const field = collection.getField(model.get('name'));
 | 
			
		||||
            return field;
 | 
			
		||||
          })
 | 
			
		||||
          .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
        return parents.length == 0;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await this.db.getCollection('fields').repository.destroy({
 | 
			
		||||
        filter: {
 | 
			
		||||
          name: model.get('name'),
 | 
			
		||||
          collectionName: {
 | 
			
		||||
            $in: childShouldRemoveField,
 | 
			
		||||
          },
 | 
			
		||||
          options: {
 | 
			
		||||
            overriding: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        transaction,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.app.on('afterLoad', async (app, options) => {
 | 
			
		||||
      if (options?.method === 'install') {
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user