feat(database): view collection support for add new, update and delete actions (#2119)
* feat: writeableView options in view collection * refactor: view collection support edit * refactor: view collection support edit * refactor: view collection support edit * refactor: view collection support edit * test: insert into view with join table * chore: typo * chore: package.json * chore: sql parser * chore: query interface * chore: test * feat: update view collection * chore: test * chore: test * chore: github action pg version * fix: params in update and delete * refactor: locale improve --------- Co-authored-by: katherinehhh <katherine_15995@163.com>
This commit is contained in:
parent
b63012d85a
commit
3510531182
24
.github/workflows/nocobase-test-backend.yml
vendored
24
.github/workflows/nocobase-test-backend.yml
vendored
@ -25,8 +25,8 @@ jobs:
|
|||||||
sqlite-test:
|
sqlite-test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_version: [ '18' ]
|
node_version: ['18']
|
||||||
underscored: [ true, false ]
|
underscored: [true, false]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:${{ matrix.node_version }}
|
container: node:${{ matrix.node_version }}
|
||||||
steps:
|
steps:
|
||||||
@ -40,7 +40,7 @@ jobs:
|
|||||||
- name: Test with Sqlite
|
- name: Test with Sqlite
|
||||||
run: yarn nocobase install -f && yarn test
|
run: yarn nocobase install -f && yarn test
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
DB_DIALECT: sqlite
|
DB_DIALECT: sqlite
|
||||||
DB_STORAGE: /tmp/db.sqlite
|
DB_STORAGE: /tmp/db.sqlite
|
||||||
DB_UNDERSCORED: ${{ matrix.underscored }}
|
DB_UNDERSCORED: ${{ matrix.underscored }}
|
||||||
@ -49,17 +49,17 @@ jobs:
|
|||||||
postgres-test:
|
postgres-test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_version: [ '18' ]
|
node_version: ['18']
|
||||||
underscored: [ true, false ]
|
underscored: [true, false]
|
||||||
schema: [ public, nocobase ]
|
schema: [public, nocobase]
|
||||||
collection_schema: [ public, user_schema ]
|
collection_schema: [public, user_schema]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:${{ matrix.node_version }}
|
container: node:${{ matrix.node_version }}
|
||||||
services:
|
services:
|
||||||
# Label used to access the service container
|
# Label used to access the service container
|
||||||
postgres:
|
postgres:
|
||||||
# Docker Hub image
|
# Docker Hub image
|
||||||
image: postgres:10
|
image: postgres:11
|
||||||
# Provide the password for postgres
|
# Provide the password for postgres
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: nocobase
|
POSTGRES_USER: nocobase
|
||||||
@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Test with postgres
|
- name: Test with postgres
|
||||||
run: yarn nocobase install -f && yarn test
|
run: yarn nocobase install -f && yarn test
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
DB_DIALECT: postgres
|
DB_DIALECT: postgres
|
||||||
DB_HOST: postgres
|
DB_HOST: postgres
|
||||||
DB_PORT: 5432
|
DB_PORT: 5432
|
||||||
@ -97,8 +97,8 @@ jobs:
|
|||||||
mysql-test:
|
mysql-test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_version: [ '18' ]
|
node_version: ['18']
|
||||||
underscored: [ true, false ]
|
underscored: [true, false]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:${{ matrix.node_version }}
|
container: node:${{ matrix.node_version }}
|
||||||
services:
|
services:
|
||||||
@ -120,7 +120,7 @@ jobs:
|
|||||||
- name: Test with MySQL
|
- name: Test with MySQL
|
||||||
run: yarn nocobase install -f && yarn test
|
run: yarn nocobase install -f && yarn test
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
DB_DIALECT: mysql
|
DB_DIALECT: mysql
|
||||||
DB_HOST: mysql
|
DB_HOST: mysql
|
||||||
DB_PORT: 3306
|
DB_PORT: 3306
|
||||||
|
@ -88,7 +88,7 @@ export const useGanttBlockContext = () => {
|
|||||||
export const useGanttBlockProps = () => {
|
export const useGanttBlockProps = () => {
|
||||||
const ctx = useGanttBlockContext();
|
const ctx = useGanttBlockContext();
|
||||||
const [tasks, setTasks] = useState<any>([]);
|
const [tasks, setTasks] = useState<any>([]);
|
||||||
const { getPrimaryKey, name, template } = useCollection();
|
const { getPrimaryKey, name, template, writableView } = useCollection();
|
||||||
const { parseAction } = useACLRoleContext();
|
const { parseAction } = useACLRoleContext();
|
||||||
const primaryKey = getPrimaryKey();
|
const primaryKey = getPrimaryKey();
|
||||||
const checkPermassion = (record) => {
|
const checkPermassion = (record) => {
|
||||||
@ -96,7 +96,7 @@ export const useGanttBlockProps = () => {
|
|||||||
const schema = {};
|
const schema = {};
|
||||||
const recordPkValue = record?.[primaryKey];
|
const recordPkValue = record?.[primaryKey];
|
||||||
const params = parseAction(actionPath, { schema, recordPkValue });
|
const params = parseAction(actionPath, { schema, recordPkValue });
|
||||||
return template === 'view' || !params;
|
return (template === 'view' && !writableView) || !params;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onExpanderClick = (task: any) => {
|
const onExpanderClick = (task: any) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
|
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import { parse } from '@nocobase/utils/client';
|
import { parse } from '@nocobase/utils/client';
|
||||||
import { Modal, message } from 'antd';
|
import { message, Modal } from 'antd';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
@ -17,7 +17,7 @@ import { useRecord } from '../../record-provider';
|
|||||||
import { removeNullCondition, useActionContext, useCompile } from '../../schema-component';
|
import { removeNullCondition, useActionContext, useCompile } from '../../schema-component';
|
||||||
import { BulkEditFormItemValueType } from '../../schema-initializer/components';
|
import { BulkEditFormItemValueType } from '../../schema-initializer/components';
|
||||||
import { useCurrentUserContext } from '../../user';
|
import { useCurrentUserContext } from '../../user';
|
||||||
import { useBlockRequestContext, useFilterByTk } from '../BlockProvider';
|
import { useBlockRequestContext, useFilterByTk, useParamsFromRecord } from '../BlockProvider';
|
||||||
import { useDetailsBlockContext } from '../DetailsBlockProvider';
|
import { useDetailsBlockContext } from '../DetailsBlockProvider';
|
||||||
import { mergeFilter } from '../SharedFilterProvider';
|
import { mergeFilter } from '../SharedFilterProvider';
|
||||||
import { TableFieldResource } from '../TableFieldProvider';
|
import { TableFieldResource } from '../TableFieldProvider';
|
||||||
@ -707,7 +707,7 @@ export const useCustomizeRequestActionProps = () => {
|
|||||||
export const useUpdateActionProps = () => {
|
export const useUpdateActionProps = () => {
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const filterByTk = useFilterByTk();
|
const filterByTk = useFilterByTk();
|
||||||
const { field, resource, __parent } = useBlockRequestContext();
|
const { field, resource, __parent, service } = useBlockRequestContext();
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
const actionSchema = useFieldSchema();
|
const actionSchema = useFieldSchema();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -718,6 +718,7 @@ export const useUpdateActionProps = () => {
|
|||||||
const currentRecord = useRecord();
|
const currentRecord = useRecord();
|
||||||
const currentUserContext = useCurrentUserContext();
|
const currentUserContext = useCurrentUserContext();
|
||||||
const currentUser = currentUserContext?.data?.data;
|
const currentUser = currentUserContext?.data?.data;
|
||||||
|
const data = useParamsFromRecord();
|
||||||
return {
|
return {
|
||||||
async onClick() {
|
async onClick() {
|
||||||
const {
|
const {
|
||||||
@ -742,6 +743,7 @@ export const useUpdateActionProps = () => {
|
|||||||
...overwriteValues,
|
...overwriteValues,
|
||||||
...assignedValues,
|
...assignedValues,
|
||||||
},
|
},
|
||||||
|
...data,
|
||||||
updateAssociationValues,
|
updateAssociationValues,
|
||||||
});
|
});
|
||||||
actionField.data.loading = false;
|
actionField.data.loading = false;
|
||||||
@ -778,10 +780,12 @@ export const useDestroyActionProps = () => {
|
|||||||
const filterByTk = useFilterByTk();
|
const filterByTk = useFilterByTk();
|
||||||
const { resource, service, block, __parent } = useBlockRequestContext();
|
const { resource, service, block, __parent } = useBlockRequestContext();
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
|
const data = useParamsFromRecord();
|
||||||
return {
|
return {
|
||||||
async onClick() {
|
async onClick() {
|
||||||
await resource.destroy({
|
await resource.destroy({
|
||||||
filterByTk,
|
filterByTk,
|
||||||
|
...data,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { count = 0, page = 0, pageSize = 0 } = service?.data?.meta || {};
|
const { count = 0, page = 0, pageSize = 0 } = service?.data?.meta || {};
|
||||||
|
@ -9,9 +9,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useRequest } from '../../api-client';
|
import { useRequest } from '../../api-client';
|
||||||
import { RecordProvider, useRecord } from '../../record-provider';
|
import { RecordProvider, useRecord } from '../../record-provider';
|
||||||
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
|
import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
|
||||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
|
||||||
import { useCancelAction } from '../action-hooks';
|
import { useCancelAction } from '../action-hooks';
|
||||||
import { useCollectionManager } from '../hooks';
|
import { useCollectionManager } from '../hooks';
|
||||||
|
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||||
import * as components from './components';
|
import * as components from './components';
|
||||||
import { TemplateSummay } from './components/TemplateSummay';
|
import { TemplateSummay } from './components/TemplateSummay';
|
||||||
import { templateOptions } from './templates';
|
import { templateOptions } from './templates';
|
||||||
@ -97,7 +97,10 @@ const getSchema = (schema, category, compile): ISchema => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const useDefaultCollectionFields = (values) => {
|
const getDefaultCollectionFields = (values) => {
|
||||||
|
if (values?.template === 'view') {
|
||||||
|
return values.fields;
|
||||||
|
}
|
||||||
const defaults = values.fields ? [...values.fields] : [];
|
const defaults = values.fields ? [...values.fields] : [];
|
||||||
const { autoGenId = true, createdAt = true, createdBy = true, updatedAt = true, updatedBy = true } = values;
|
const { autoGenId = true, createdAt = true, createdBy = true, updatedAt = true, updatedBy = true } = values;
|
||||||
if (autoGenId) {
|
if (autoGenId) {
|
||||||
@ -207,9 +210,8 @@ const useCreateCollection = (schema?: any) => {
|
|||||||
if (schema?.events?.beforeSubmit) {
|
if (schema?.events?.beforeSubmit) {
|
||||||
schema.events.beforeSubmit(values);
|
schema.events.beforeSubmit(values);
|
||||||
}
|
}
|
||||||
const fields = values?.template !== 'view' ? useDefaultCollectionFields(values) : values.fields;
|
const fields = getDefaultCollectionFields(values);
|
||||||
if (values.autoCreateReverseField) {
|
if (!values.autoCreateReverseField) {
|
||||||
} else {
|
|
||||||
delete values.reverseField;
|
delete values.reverseField;
|
||||||
}
|
}
|
||||||
delete values.id;
|
delete values.id;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { getConfigurableProperties } from './properties';
|
|
||||||
import { ICollectionTemplate } from './types';
|
|
||||||
import { PreviewFields } from './components/PreviewFields';
|
import { PreviewFields } from './components/PreviewFields';
|
||||||
import { PreviewTable } from './components/PreviewTable';
|
import { PreviewTable } from './components/PreviewTable';
|
||||||
|
import { getConfigurableProperties } from './properties';
|
||||||
|
import { ICollectionTemplate } from './types';
|
||||||
|
|
||||||
export const view: ICollectionTemplate = {
|
export const view: ICollectionTemplate = {
|
||||||
name: 'view',
|
name: 'view',
|
||||||
@ -91,6 +91,13 @@ export const view: ICollectionTemplate = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
writableView: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-content': '{{t("Allow add new, update and delete actions")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
sources: {
|
sources: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
title: '{{ t("Source collections") }}',
|
title: '{{ t("Source collections") }}',
|
||||||
@ -128,6 +135,7 @@ export const view: ICollectionTemplate = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
...getConfigurableProperties('category'),
|
...getConfigurableProperties('category'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -29,6 +29,7 @@ export interface CollectionOptions {
|
|||||||
inherits?: string[];
|
inherits?: string[];
|
||||||
tree?: string;
|
tree?: string;
|
||||||
template?: string;
|
template?: string;
|
||||||
|
writableView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICollectionProviderProps {
|
export interface ICollectionProviderProps {
|
||||||
|
@ -707,4 +707,5 @@ export default {
|
|||||||
"Current form": "Current form",
|
"Current form": "Current form",
|
||||||
"Current object":"Current object",
|
"Current object":"Current object",
|
||||||
"Linkage with form fields":"Linkage with form fields",
|
"Linkage with form fields":"Linkage with form fields",
|
||||||
|
"Allow add new, update and delete actions":"Allow add new, update and delete actions"
|
||||||
};
|
};
|
||||||
|
@ -618,4 +618,5 @@ export default {
|
|||||||
"Current form":"現在のフォーム",
|
"Current form":"現在のフォーム",
|
||||||
"Current object":"現在のオブジェクト",
|
"Current object":"現在のオブジェクト",
|
||||||
"Linkage with form fields":"フォームデータから連動",
|
"Linkage with form fields":"フォームデータから連動",
|
||||||
|
"Allow add new, update and delete actions":"削除変更操作の許可"
|
||||||
}
|
}
|
||||||
|
@ -792,4 +792,5 @@ export default {
|
|||||||
"Copy into the form and continue to fill in": "复制到表单并继续填写",
|
"Copy into the form and continue to fill in": "复制到表单并继续填写",
|
||||||
"Linkage with form fields":"从表单字段联动",
|
"Linkage with form fields":"从表单字段联动",
|
||||||
"Failed to load plugin": "插件加载失败",
|
"Failed to load plugin": "插件加载失败",
|
||||||
|
"Allow add new, update and delete actions":"允许增删改操作"
|
||||||
}
|
}
|
||||||
|
@ -74,9 +74,9 @@ export const CalendarActionInitializers = {
|
|||||||
skipScopeCheck: true,
|
skipScopeCheck: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -23,9 +23,9 @@ export const CalendarFormActionInitializers = {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -36,9 +36,9 @@ export const CalendarFormActionInitializers = {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -49,9 +49,9 @@ export const CalendarFormActionInitializers = {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -144,9 +144,9 @@ export const CalendarFormActionInitializers = {
|
|||||||
useProps: '{{ useCustomizeUpdateActionProps }}',
|
useProps: '{{ useCustomizeUpdateActionProps }}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -173,7 +173,7 @@ export const CalendarFormActionInitializers = {
|
|||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useFieldSchema, Schema } from '@formily/react';
|
|
||||||
import { useCollection } from '../../collection-manager';
|
import { useCollection } from '../../collection-manager';
|
||||||
|
|
||||||
// 表单的操作配置
|
// 表单的操作配置
|
||||||
@ -34,7 +33,7 @@ export const GridCardActionInitializers = {
|
|||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return collection.template !== 'view' && collection.template !== 'file';
|
return (collection.template !== 'view' && collection.template !== 'file') || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -166,7 +165,7 @@ export const GridCardItemActionInitializers = {
|
|||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return (collection as any).template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -264,7 +263,7 @@ export const GridCardItemActionInitializers = {
|
|||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return (collection as any).template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -290,7 +289,7 @@ export const GridCardItemActionInitializers = {
|
|||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return (collection as any).template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -30,9 +30,9 @@ export const KanbanActionInitializers = {
|
|||||||
skipScopeCheck: true,
|
skipScopeCheck: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return (collection as any).template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useFieldSchema, Schema } from '@formily/react';
|
|
||||||
import { useCollection } from '../../collection-manager';
|
import { useCollection } from '../../collection-manager';
|
||||||
|
|
||||||
// 表单的操作配置
|
// 表单的操作配置
|
||||||
@ -32,9 +31,9 @@ export const ListActionInitializers = {
|
|||||||
skipScopeCheck: true,
|
skipScopeCheck: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return collection.template !== 'view' && collection.template !== 'file';
|
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'file';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -164,9 +163,9 @@ export const ListItemActionInitializers = {
|
|||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
'x-align': 'left',
|
'x-align': 'left',
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return (collection as any).template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -262,9 +261,9 @@ export const ListItemActionInitializers = {
|
|||||||
useProps: '{{ useCustomizeUpdateActionProps }}',
|
useProps: '{{ useCustomizeUpdateActionProps }}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return (collection as any).template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -288,9 +287,9 @@ export const ListItemActionInitializers = {
|
|||||||
useProps: '{{ useCustomizeRequestActionProps }}',
|
useProps: '{{ useCustomizeRequestActionProps }}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return (collection as any).template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -2,7 +2,7 @@ import { useCollection } from '../..';
|
|||||||
|
|
||||||
const useVisibleCollection = () => {
|
const useVisibleCollection = () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return collection.template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
};
|
};
|
||||||
// 表单的操作配置
|
// 表单的操作配置
|
||||||
export const ReadPrettyFormActionInitializers = {
|
export const ReadPrettyFormActionInitializers = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Schema, useFieldSchema } from '@formily/react';
|
import { Schema, useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SchemaInitializer, useCollection, useCollectionManager, SchemaInitializerItemOptions } from '../..';
|
import { SchemaInitializer, SchemaInitializerItemOptions, useCollection, useCollectionManager } from '../..';
|
||||||
import { gridRowColWrap } from '../utils';
|
import { gridRowColWrap } from '../utils';
|
||||||
|
|
||||||
const recursiveParent = (schema: Schema) => {
|
const recursiveParent = (schema: Schema) => {
|
||||||
@ -170,7 +170,7 @@ export const RecordBlockInitializers = (props: any) => {
|
|||||||
const hasFormChildCollection = formChildrenCollections?.length > 0;
|
const hasFormChildCollection = formChildrenCollections?.length > 0;
|
||||||
const detailChildrenCollections = getChildrenCollections(collection.name, true);
|
const detailChildrenCollections = getChildrenCollections(collection.name, true);
|
||||||
const hasDetailChildCollection = detailChildrenCollections?.length > 0;
|
const hasDetailChildCollection = detailChildrenCollections?.length > 0;
|
||||||
const modifyFlag = (collection as any).template !== 'view';
|
const modifyFlag = (collection as any).template !== 'view' || collection?.writableView;
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Button
|
<SchemaInitializer.Button
|
||||||
wrap={gridRowColWrap}
|
wrap={gridRowColWrap}
|
||||||
|
@ -53,7 +53,6 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
const { treeTable } = fieldSchema?.parent?.parent['x-decorator-props'] || {};
|
const { treeTable } = fieldSchema?.parent?.parent['x-decorator-props'] || {};
|
||||||
const modifyFlag = (collection as any).template !== 'view';
|
|
||||||
return (
|
return (
|
||||||
<SchemaInitializer.Button
|
<SchemaInitializer.Button
|
||||||
insertPosition={'beforeEnd'}
|
insertPosition={'beforeEnd'}
|
||||||
@ -102,11 +101,11 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
modifyFlag && {
|
{
|
||||||
type: 'item',
|
type: 'item',
|
||||||
title: t('Delete'),
|
title: t('Delete'),
|
||||||
component: 'DestroyActionInitializer',
|
component: 'DestroyActionInitializer',
|
||||||
@ -115,6 +114,9 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
'x-action': 'destroy',
|
'x-action': 'destroy',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
|
visible: () => {
|
||||||
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
collection.tree &&
|
collection.tree &&
|
||||||
treeTable !== false && {
|
treeTable !== false && {
|
||||||
@ -137,7 +139,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -223,7 +225,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -248,7 +250,7 @@ export const TableActionColumnInitializers = (props: any) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -34,7 +34,7 @@ export const TableActionInitializers = {
|
|||||||
},
|
},
|
||||||
visible: function useVisible() {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return collection.template !== 'view' && collection.template !== 'file';
|
return (collection.template !== 'view' && collection.template !== 'file') || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -47,7 +47,7 @@ export const TableActionInitializers = {
|
|||||||
},
|
},
|
||||||
visible: function useVisible() {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -78,7 +78,7 @@ export const TableActionInitializers = {
|
|||||||
type: 'divider',
|
type: 'divider',
|
||||||
visible: function useVisible() {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
@ -159,7 +159,7 @@ export const TableActionInitializers = {
|
|||||||
],
|
],
|
||||||
visible: function useVisible() {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
return collection.template !== 'view' || collection?.writableView;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
|
||||||
import { TableOutlined } from '@ant-design/icons';
|
import { TableOutlined } from '@ant-design/icons';
|
||||||
|
import React from 'react';
|
||||||
import { useCollectionManager } from '../../collection-manager';
|
import { useCollectionManager } from '../../collection-manager';
|
||||||
import { createDetailsBlockSchema } from '../utils';
|
import { createDetailsBlockSchema } from '../utils';
|
||||||
import { DataBlockInitializer } from './DataBlockInitializer';
|
import { DataBlockInitializer } from './DataBlockInitializer';
|
||||||
@ -18,7 +17,8 @@ export const DetailsBlockInitializer = (props) => {
|
|||||||
const schema = createDetailsBlockSchema({
|
const schema = createDetailsBlockSchema({
|
||||||
collection: item.name,
|
collection: item.name,
|
||||||
rowKey: collection.filterTargetKey || 'id',
|
rowKey: collection.filterTargetKey || 'id',
|
||||||
actionInitializers: collection.template !== 'view' && 'DetailsActionInitializers',
|
actionInitializers:
|
||||||
|
(collection.template !== 'view' || collection?.writableView) && 'DetailsActionInitializers',
|
||||||
});
|
});
|
||||||
insert(schema);
|
insert(schema);
|
||||||
}}
|
}}
|
||||||
|
@ -860,7 +860,7 @@ export const useCollectionDataSourceItems = (componentName) => {
|
|||||||
const fields = getCollectionFields(item.name);
|
const fields = getCollectionFields(item.name);
|
||||||
if (item.autoGenId === false && !fields.find((v) => v.primaryKey)) {
|
if (item.autoGenId === false && !fields.find((v) => v.primaryKey)) {
|
||||||
return false;
|
return false;
|
||||||
} else if (['Kanban', 'FormItem'].includes(componentName) && item.template === 'view') {
|
} else if (['Kanban', 'FormItem'].includes(componentName) && item.template === 'view' && !item.writableView) {
|
||||||
return false;
|
return false;
|
||||||
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
|
} else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"excel-date-to-js": "^1.1.5",
|
"excel-date-to-js": "^1.1.5",
|
||||||
"flat": "^5.0.2",
|
"flat": "^5.0.2",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
|
"graphlib": "^2.1.8",
|
||||||
"mathjs": "^10.6.1",
|
"mathjs": "^10.6.1",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"sequelize": "^6.26.0",
|
"sequelize": "^6.26.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Database } from '../database';
|
import { Database } from '../database';
|
||||||
|
import { AdjacencyListRepository } from '../repositories/tree-repository/adjacency-list-repository';
|
||||||
import { mockDatabase } from './';
|
import { mockDatabase } from './';
|
||||||
import { AdjacencyListRepository } from '../tree-repository/adjacency-list-repository';
|
|
||||||
|
|
||||||
describe('tree test', function () {
|
describe('tree test', function () {
|
||||||
let db: Database;
|
let db: Database;
|
||||||
|
@ -1,6 +1,112 @@
|
|||||||
import { Database, mockDatabase } from '@nocobase/database';
|
|
||||||
import { uid } from '@nocobase/utils';
|
import { uid } from '@nocobase/utils';
|
||||||
|
import { Database, mockDatabase } from '../../index';
|
||||||
import { ViewCollection } from '../../view-collection';
|
import { ViewCollection } from '../../view-collection';
|
||||||
|
import pgOnly from '../inhertits/helper';
|
||||||
|
|
||||||
|
pgOnly()('', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase({
|
||||||
|
tablePrefix: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.clean({ drop: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update view collection', async () => {
|
||||||
|
const UserCollection = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
timestamps: false,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
interface: { type: 'string', title: '姓名' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'group',
|
||||||
|
type: 'belongsTo',
|
||||||
|
foreignKey: 'group_id',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const GroupCollection = db.collection({
|
||||||
|
name: 'groups',
|
||||||
|
timestamps: false,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
interface: { type: 'string', title: '分组名' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const viewName = `users_with_group`;
|
||||||
|
const dropSQL = `DROP VIEW IF EXISTS ${viewName}`;
|
||||||
|
await db.sequelize.query(dropSQL);
|
||||||
|
const viewSQL = `CREATE VIEW ${viewName} AS SELECT users.id AS user_id, users.name AS user_name, groups.name AS group_name FROM ${UserCollection.quotedTableName()} AS users INNER JOIN ${GroupCollection.quotedTableName()} AS groups ON users.group_id = groups.id`;
|
||||||
|
await db.sequelize.query(viewSQL);
|
||||||
|
|
||||||
|
const UsersWithGroup = db.collection({
|
||||||
|
name: viewName,
|
||||||
|
view: true,
|
||||||
|
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||||
|
writableView: true,
|
||||||
|
fields: [
|
||||||
|
{ name: 'user_id', type: 'bigInt' },
|
||||||
|
{ name: 'user_name', type: 'string', source: 'users.name' },
|
||||||
|
{ name: 'group_name', type: 'string', source: 'groups.name' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// create INSTEAD OF INSERT trigger
|
||||||
|
await db.sequelize.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION insert_users_with_group() RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
new_group_id BIGINT;
|
||||||
|
BEGIN
|
||||||
|
-- 插入一个新的 groups 行,并获取新插入的行的 ID
|
||||||
|
INSERT INTO ${GroupCollection.quotedTableName()} (name) VALUES (NEW.group_name) RETURNING id INTO new_group_id;
|
||||||
|
|
||||||
|
-- 插入一个新的 users 行,使用新插入的 groups 行的 ID 作为 group_id
|
||||||
|
INSERT INTO ${UserCollection.quotedTableName()} (name, group_id) VALUES (NEW.user_name, new_group_id) RETURNING id INTO NEW.user_id;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.sequelize.query(`
|
||||||
|
CREATE TRIGGER insert_users_with_group_trigger
|
||||||
|
INSTEAD OF INSERT ON ${UsersWithGroup.quotedTableName()}
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION insert_users_with_group();
|
||||||
|
`);
|
||||||
|
|
||||||
|
const returned = await UsersWithGroup.repository.create({
|
||||||
|
values: {
|
||||||
|
user_name: 'u1',
|
||||||
|
group_name: 'g1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(returned.get('user_name')).toBe('u1');
|
||||||
|
expect(returned.get('group_name')).toBe('g1');
|
||||||
|
|
||||||
|
const records = await UsersWithGroup.repository.find();
|
||||||
|
const firstRecord = records[0].toJSON();
|
||||||
|
expect(firstRecord.user_name).toBe('u1');
|
||||||
|
expect(firstRecord.group_name).toBe('g1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('create view', () => {
|
describe('create view', () => {
|
||||||
let db: Database;
|
let db: Database;
|
||||||
@ -9,6 +115,7 @@ describe('create view', () => {
|
|||||||
db = mockDatabase({
|
db = mockDatabase({
|
||||||
tablePrefix: '',
|
tablePrefix: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.clean({ drop: true });
|
await db.clean({ drop: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,7 +199,7 @@ describe('create view', () => {
|
|||||||
|
|
||||||
const dropViewSQL = `DROP VIEW IF EXISTS ${appendSchema}${viewName}`;
|
const dropViewSQL = `DROP VIEW IF EXISTS ${appendSchema}${viewName}`;
|
||||||
await db.sequelize.query(dropViewSQL);
|
await db.sequelize.query(dropViewSQL);
|
||||||
const viewSql = `CREATE VIEW ${appendSchema}${viewName} AS SELECT users.name, profiles.age FROM ${appendSchema}${UserCollection.model.tableName} as users LEFT JOIN ${appendSchema}${ProfileCollection.model.tableName} as profiles ON users.id = profiles.user_id;`;
|
const viewSql = `CREATE VIEW ${appendSchema}${viewName} AS SELECT users.name, profiles.age FROM ${appendSchema}${UserCollection.model.tableName} as users LEFT JOIN ${appendSchema}${ProfileCollection.model.tableName} as profiles ON users.id = profiles.user_id;`;
|
||||||
|
|
||||||
await db.sequelize.query(viewSql);
|
await db.sequelize.query(viewSql);
|
||||||
|
|
||||||
@ -119,7 +226,6 @@ describe('create view', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(fooData);
|
|
||||||
expect(fooData.get('name')).toBe('foo');
|
expect(fooData.get('name')).toBe('foo');
|
||||||
expect(fooData.get('age')).toBe(18);
|
expect(fooData.get('age')).toBe(18);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Database, mockDatabase } from '@nocobase/database';
|
import { Database, mockDatabase } from '@nocobase/database';
|
||||||
import { ViewFieldInference } from '../../view/view-inference';
|
import { ViewFieldInference } from '../../view/view-inference';
|
||||||
import { uid } from '@nocobase/utils';
|
|
||||||
|
|
||||||
describe('view inference', function () {
|
describe('view inference', function () {
|
||||||
let db: Database;
|
let db: Database;
|
||||||
|
@ -12,9 +12,9 @@ import {
|
|||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { BelongsToField, Field, FieldOptions, HasManyField } from './fields';
|
import { BelongsToField, Field, FieldOptions, HasManyField } from './fields';
|
||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
|
import { AdjacencyListRepository } from './repositories/tree-repository/adjacency-list-repository';
|
||||||
import { Repository } from './repository';
|
import { Repository } from './repository';
|
||||||
import { checkIdentifier, md5, snakeCase } from './utils';
|
import { checkIdentifier, md5, snakeCase } from './utils';
|
||||||
import { AdjacencyListRepository } from './tree-repository/adjacency-list-repository';
|
|
||||||
|
|
||||||
export type RepositoryType = typeof Repository;
|
export type RepositoryType = typeof Repository;
|
||||||
|
|
||||||
@ -44,6 +44,7 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
|
|||||||
tableName?: string;
|
tableName?: string;
|
||||||
inherits?: string[] | string;
|
inherits?: string[] | string;
|
||||||
viewName?: string;
|
viewName?: string;
|
||||||
|
writableView?: boolean;
|
||||||
|
|
||||||
filterTargetKey?: string;
|
filterTargetKey?: string;
|
||||||
fields?: FieldOptions[];
|
fields?: FieldOptions[];
|
||||||
@ -272,7 +273,7 @@ export class Collection<
|
|||||||
`source field "${sourceFieldName}" not found for field "${name}" at collection "${this.name}"`,
|
`source field "${sourceFieldName}" not found for field "${name}" at collection "${this.name}"`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
options = { ...sourceField.options, ...options };
|
options = { ...lodash.omit(sourceField.options, 'name'), ...options };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Logger } from '@nocobase/logger';
|
||||||
import { applyMixins, AsyncEmitter, requireModule } from '@nocobase/utils';
|
import { applyMixins, AsyncEmitter, requireModule } from '@nocobase/utils';
|
||||||
import merge from 'deepmerge';
|
import merge from 'deepmerge';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
@ -19,7 +20,9 @@ import {
|
|||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { SequelizeStorage, Umzug } from 'umzug';
|
import { SequelizeStorage, Umzug } from 'umzug';
|
||||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||||
|
import { CollectionGroupManager } from './collection-group-manager';
|
||||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||||
|
import DatabaseUtils from './database-utils';
|
||||||
import ReferencesMap from './features/ReferencesMap';
|
import ReferencesMap from './features/ReferencesMap';
|
||||||
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
||||||
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
||||||
@ -27,10 +30,13 @@ import * as FieldTypes from './fields';
|
|||||||
import { Field, FieldContext, RelationField } from './fields';
|
import { Field, FieldContext, RelationField } from './fields';
|
||||||
import { InheritedCollection } from './inherited-collection';
|
import { InheritedCollection } from './inherited-collection';
|
||||||
import InheritanceMap from './inherited-map';
|
import InheritanceMap from './inherited-map';
|
||||||
|
import { registerBuiltInListeners } from './listeners';
|
||||||
import { MigrationItem, Migrations } from './migration';
|
import { MigrationItem, Migrations } from './migration';
|
||||||
import { Model } from './model';
|
import { Model } from './model';
|
||||||
import { ModelHook } from './model-hook';
|
import { ModelHook } from './model-hook';
|
||||||
import extendOperators from './operators';
|
import extendOperators from './operators';
|
||||||
|
import QueryInterface from './query-interface/query-interface';
|
||||||
|
import buildQueryInterface from './query-interface/query-interface-builder';
|
||||||
import { RelationRepository } from './relation-repository/relation-repository';
|
import { RelationRepository } from './relation-repository/relation-repository';
|
||||||
import { Repository } from './repository';
|
import { Repository } from './repository';
|
||||||
import {
|
import {
|
||||||
@ -61,13 +67,6 @@ import {
|
|||||||
ValidateListener,
|
ValidateListener,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { patchSequelizeQueryInterface, snakeCase } from './utils';
|
import { patchSequelizeQueryInterface, snakeCase } from './utils';
|
||||||
|
|
||||||
import { Logger } from '@nocobase/logger';
|
|
||||||
import { CollectionGroupManager } from './collection-group-manager';
|
|
||||||
import DatabaseUtils from './database-utils';
|
|
||||||
import { registerBuiltInListeners } from './listeners';
|
|
||||||
import QueryInterface from './query-interface/query-interface';
|
|
||||||
import buildQueryInterface from './query-interface/query-interface-builder';
|
|
||||||
import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
|
import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
|
||||||
import { ViewCollection } from './view-collection';
|
import { ViewCollection } from './view-collection';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import QueryInterface from './query-interface';
|
|
||||||
import { Collection } from '../collection';
|
|
||||||
import { Transactionable } from 'sequelize';
|
import { Transactionable } from 'sequelize';
|
||||||
|
import { Collection } from '../collection';
|
||||||
import sqlParser from '../sql-parser';
|
import sqlParser from '../sql-parser';
|
||||||
|
import QueryInterface from './query-interface';
|
||||||
|
|
||||||
export default class MysqlQueryInterface extends QueryInterface {
|
export default class MysqlQueryInterface extends QueryInterface {
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
@ -39,13 +39,7 @@ export default class MysqlQueryInterface extends QueryInterface {
|
|||||||
};
|
};
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const viewDefinition = await this.db.sequelize.query(`SHOW CREATE VIEW ${options.viewName}`, { type: 'SELECT' });
|
const { ast } = this.parseSQL(await this.viewDef(options.viewName));
|
||||||
const createView = viewDefinition[0]['Create View'];
|
|
||||||
const regex = /(?<=AS\s)([\s\S]*)/i;
|
|
||||||
const match = createView.match(regex);
|
|
||||||
const sql = match[0];
|
|
||||||
|
|
||||||
const { ast } = sqlParser.parse(sql);
|
|
||||||
|
|
||||||
const columns = ast.columns;
|
const columns = ast.columns;
|
||||||
|
|
||||||
@ -69,4 +63,17 @@ export default class MysqlQueryInterface extends QueryInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseSQL(sql: string): any {
|
||||||
|
return sqlParser.parse(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
async viewDef(viewName: string): Promise<string> {
|
||||||
|
const viewDefinition = await this.db.sequelize.query(`SHOW CREATE VIEW ${viewName}`, { type: 'SELECT' });
|
||||||
|
const createView = viewDefinition[0]['Create View'];
|
||||||
|
const regex = /(?<=AS\s)([\s\S]*)/i;
|
||||||
|
const match = createView.match(regex);
|
||||||
|
const sql = match[0];
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import QueryInterface from './query-interface';
|
import lodash from 'lodash';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import sqlParser from '../sql-parser/postgres';
|
import sqlParser from '../sql-parser/postgres';
|
||||||
|
import QueryInterface from './query-interface';
|
||||||
|
|
||||||
export default class PostgresQueryInterface extends QueryInterface {
|
export default class PostgresQueryInterface extends QueryInterface {
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
@ -33,6 +34,23 @@ export default class PostgresQueryInterface extends QueryInterface {
|
|||||||
return await this.db.sequelize.query(sql, { type: 'SELECT' });
|
return await this.db.sequelize.query(sql, { type: 'SELECT' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async viewDef(viewName: string) {
|
||||||
|
const [schema, name] = viewName.split('.');
|
||||||
|
|
||||||
|
const viewDefQuery = await this.db.sequelize.query(
|
||||||
|
`
|
||||||
|
select pg_get_viewdef(format('%I.%I', '${schema}', '${name}')::regclass, true) as definition
|
||||||
|
`,
|
||||||
|
{ type: 'SELECT' },
|
||||||
|
);
|
||||||
|
|
||||||
|
return lodash.trim(viewDefQuery[0]['definition']);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSQL(sql: string): any {
|
||||||
|
return sqlParser.parse(sql);
|
||||||
|
}
|
||||||
|
|
||||||
async viewColumnUsage(options): Promise<{
|
async viewColumnUsage(options): Promise<{
|
||||||
[view_column_name: string]: {
|
[view_column_name: string]: {
|
||||||
column_name: string;
|
column_name: string;
|
||||||
@ -54,16 +72,10 @@ export default class PostgresQueryInterface extends QueryInterface {
|
|||||||
table_schema: string;
|
table_schema: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const viewDefQuery = await this.db.sequelize.query(
|
const def = await this.viewDef(`${schema}.${viewName}`);
|
||||||
`
|
|
||||||
select pg_get_viewdef(format('%I.%I', '${schema}', '${viewName}')::regclass, true) as definition
|
|
||||||
`,
|
|
||||||
{ type: 'SELECT' },
|
|
||||||
);
|
|
||||||
|
|
||||||
const def = viewDefQuery[0]['definition'];
|
|
||||||
try {
|
try {
|
||||||
const { ast } = sqlParser.parse(def);
|
const { ast } = this.parseSQL(def);
|
||||||
const columns = ast[0].columns;
|
const columns = ast[0].columns;
|
||||||
|
|
||||||
const usages = columns
|
const usages = columns
|
||||||
@ -96,7 +108,7 @@ export default class PostgresQueryInterface extends QueryInterface {
|
|||||||
|
|
||||||
return Object.fromEntries(usages);
|
return Object.fromEntries(usages);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.db.logger.warn(e);
|
console.log(e);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Database from '../database';
|
|
||||||
import { Collection } from '../collection';
|
|
||||||
import { QueryInterface as SequelizeQueryInterface, Transactionable } from 'sequelize';
|
import { QueryInterface as SequelizeQueryInterface, Transactionable } from 'sequelize';
|
||||||
|
import { Collection } from '../collection';
|
||||||
|
import Database from '../database';
|
||||||
|
|
||||||
export default abstract class QueryInterface {
|
export default abstract class QueryInterface {
|
||||||
sequelizeQueryInterface: SequelizeQueryInterface;
|
sequelizeQueryInterface: SequelizeQueryInterface;
|
||||||
@ -13,6 +13,8 @@ export default abstract class QueryInterface {
|
|||||||
|
|
||||||
abstract listViews();
|
abstract listViews();
|
||||||
|
|
||||||
|
abstract viewDef(viewName: string): Promise<string>;
|
||||||
|
|
||||||
abstract viewColumnUsage(options: { viewName: string; schema?: string }): Promise<{
|
abstract viewColumnUsage(options: { viewName: string; schema?: string }): Promise<{
|
||||||
[view_column_name: string]: {
|
[view_column_name: string]: {
|
||||||
column_name: string;
|
column_name: string;
|
||||||
@ -21,6 +23,8 @@ export default abstract class QueryInterface {
|
|||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
abstract parseSQL(sql: string): any;
|
||||||
|
|
||||||
async dropAll(options) {
|
async dropAll(options) {
|
||||||
if (options.drop !== true) return;
|
if (options.drop !== true) return;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import QueryInterface from './query-interface';
|
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import sqlParser from '../sql-parser';
|
import sqlParser from '../sql-parser';
|
||||||
|
import QueryInterface from './query-interface';
|
||||||
|
|
||||||
export default class SqliteQueryInterface extends QueryInterface {
|
export default class SqliteQueryInterface extends QueryInterface {
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
@ -22,7 +22,7 @@ export default class SqliteQueryInterface extends QueryInterface {
|
|||||||
|
|
||||||
async listViews() {
|
async listViews() {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT name , sql as definition
|
SELECT name, sql as definition
|
||||||
FROM sqlite_master
|
FROM sqlite_master
|
||||||
WHERE type = 'view'
|
WHERE type = 'view'
|
||||||
ORDER BY name;
|
ORDER BY name;
|
||||||
@ -41,19 +41,7 @@ export default class SqliteQueryInterface extends QueryInterface {
|
|||||||
};
|
};
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const viewDefinition = await this.db.sequelize.query(
|
const { ast } = this.parseSQL(await this.viewDef(options.viewName));
|
||||||
`SELECT sql FROM sqlite_master WHERE name = '${options.viewName}' AND type = 'view'`,
|
|
||||||
{
|
|
||||||
type: 'SELECT',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const createView = viewDefinition[0]['sql'];
|
|
||||||
const regex = /(?<=AS\s)([\s\S]*)/i;
|
|
||||||
const match = createView.match(regex);
|
|
||||||
const sql = match[0];
|
|
||||||
|
|
||||||
const { ast } = sqlParser.parse(sql);
|
|
||||||
|
|
||||||
const columns = ast.columns;
|
const columns = ast.columns;
|
||||||
|
|
||||||
@ -76,4 +64,26 @@ export default class SqliteQueryInterface extends QueryInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseSQL(sql: string): any {
|
||||||
|
return sqlParser.parse(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
async viewDef(viewName: string): Promise<string> {
|
||||||
|
const viewDefinition = await this.db.sequelize.query(
|
||||||
|
`SELECT sql
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE name = '${viewName}' AND type = 'view'`,
|
||||||
|
{
|
||||||
|
type: 'SELECT',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const createView = viewDefinition[0]['sql'];
|
||||||
|
const regex = /(?<=AS\s)([\s\S]*)/i;
|
||||||
|
const match = createView.match(regex);
|
||||||
|
const sql = match[0];
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FindOptions, Repository } from '../repository';
|
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
|
import { FindOptions, Repository } from '../../repository';
|
||||||
|
|
||||||
export class AdjacencyListRepository extends Repository {
|
export class AdjacencyListRepository extends Repository {
|
||||||
async update(options): Promise<any> {
|
async update(options): Promise<any> {
|
||||||
@ -143,15 +143,13 @@ export class AdjacencyListRepository extends Repository {
|
|||||||
const q = queryInterface.quoteIdentifier.bind(queryInterface);
|
const q = queryInterface.quoteIdentifier.bind(queryInterface);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
WITH RECURSIVE cte AS (
|
WITH RECURSIVE cte AS (SELECT ${q(primaryKey)}, ${q(foreignKeyField)}, 1 AS level
|
||||||
SELECT ${q(primaryKey)}, ${q(foreignKeyField)}, 1 AS level
|
FROM ${collection.quotedTableName()}
|
||||||
FROM ${collection.quotedTableName()}
|
WHERE ${q(foreignKeyField)} IN (${rootIds.join(',')})
|
||||||
WHERE ${q(foreignKeyField)} IN (${rootIds.join(',')})
|
UNION ALL
|
||||||
UNION ALL
|
SELECT t.${q(primaryKey)}, t.${q(foreignKeyField)}, cte.level + 1 AS level
|
||||||
SELECT t.${q(primaryKey)}, t.${q(foreignKeyField)}, cte.level + 1 AS level
|
FROM ${collection.quotedTableName()} t
|
||||||
FROM ${collection.quotedTableName()} t
|
JOIN cte ON t.${q(foreignKeyField)} = cte.${q(primaryKey)})
|
||||||
JOIN cte ON t.${q(foreignKeyField)} = cte.${q(primaryKey)}
|
|
||||||
)
|
|
||||||
SELECT ${q(primaryKey)}, ${q(foreignKeyField)} as ${q(foreignKey)}, level
|
SELECT ${q(primaryKey)}, ${q(foreignKeyField)} as ${q(foreignKey)}, level
|
||||||
FROM cte
|
FROM cte
|
||||||
`;
|
`;
|
@ -0,0 +1,3 @@
|
|||||||
|
import { Repository } from '../repository';
|
||||||
|
|
||||||
|
export class ViewRepository extends Repository {}
|
File diff suppressed because it is too large
Load Diff
@ -8,13 +8,13 @@ export class ViewCollection extends Collection {
|
|||||||
super(options, context);
|
super(options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isView() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected sequelizeModelOptions(): any {
|
protected sequelizeModelOptions(): any {
|
||||||
const modelOptions = super.sequelizeModelOptions();
|
const modelOptions = super.sequelizeModelOptions();
|
||||||
modelOptions.tableName = this.options.viewName || this.options.name;
|
modelOptions.tableName = this.options.viewName || this.options.name;
|
||||||
return modelOptions;
|
return modelOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
isView() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { isArray } from 'mathjs';
|
||||||
import Database from '../database';
|
import Database from '../database';
|
||||||
import FieldTypeMap from './field-type-map';
|
import FieldTypeMap from './field-type-map';
|
||||||
import { isArray } from 'mathjs';
|
|
||||||
|
|
||||||
type InferredField = {
|
type InferredField = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -31,6 +31,7 @@ export class ViewFieldInference {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const rawFields = [];
|
const rawFields = [];
|
||||||
|
|
||||||
for (const [name, column] of Object.entries(columns)) {
|
for (const [name, column] of Object.entries(columns)) {
|
||||||
const inferResult: any = { name };
|
const inferResult: any = { name };
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MockServer } from '@nocobase/test';
|
|
||||||
import { createApp } from '../index';
|
|
||||||
import { uid } from '@nocobase/utils';
|
|
||||||
import { Database, Repository } from '@nocobase/database';
|
import { Database, Repository } from '@nocobase/database';
|
||||||
|
import { MockServer } from '@nocobase/test';
|
||||||
|
import { uid } from '@nocobase/utils';
|
||||||
|
import { createApp } from '../index';
|
||||||
|
|
||||||
describe('view collection', () => {
|
describe('view collection', () => {
|
||||||
let app: MockServer;
|
let app: MockServer;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Database, { Repository, ViewCollection, ViewFieldInference } from '@nocobase/database';
|
import Database, { Repository, ViewCollection, ViewFieldInference } from '@nocobase/database';
|
||||||
import Application from '@nocobase/server';
|
import Application from '@nocobase/server';
|
||||||
import { createApp } from '../index';
|
|
||||||
import { uid } from '@nocobase/utils';
|
import { uid } from '@nocobase/utils';
|
||||||
|
import { createApp } from '../index';
|
||||||
|
|
||||||
describe('view collection', function () {
|
describe('view collection', function () {
|
||||||
let db: Database;
|
let db: Database;
|
||||||
|
@ -222,12 +222,7 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.app.logger.warn(error);
|
this.app.logger.warn(error);
|
||||||
await this.app.db.sync();
|
await this.app.db.sync();
|
||||||
|
await this.app.db.getRepository<CollectionRepository>('collections').load();
|
||||||
try {
|
|
||||||
await this.app.db.getRepository<CollectionRepository>('collections').load();
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -287,7 +282,7 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
const newOptions = {};
|
const newOptions = {};
|
||||||
|
|
||||||
// write original field options
|
// write original field options
|
||||||
lodash.merge(newOptions, collectionField.options);
|
lodash.merge(newOptions, lodash.omit(collectionField.options, 'name'));
|
||||||
|
|
||||||
// merge with current field options
|
// merge with current field options
|
||||||
lodash.mergeWith(newOptions, field.get(), (objValue, srcValue) => {
|
lodash.mergeWith(newOptions, field.get(), (objValue, srcValue) => {
|
||||||
|
@ -20,7 +20,7 @@ export const ImportInitializerProvider = (props: any) => {
|
|||||||
},
|
},
|
||||||
visible: function useVisible() {
|
visible: function useVisible() {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return collection.template !== 'view' && collection.template !== 'file';
|
return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'file';
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return props.children;
|
return props.children;
|
||||||
|
@ -5744,7 +5744,7 @@
|
|||||||
|
|
||||||
"@remix-run/router@1.7.1":
|
"@remix-run/router@1.7.1":
|
||||||
version "1.7.1"
|
version "1.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498"
|
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498"
|
||||||
integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==
|
integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==
|
||||||
|
|
||||||
"@restart/hooks@^0.4.7":
|
"@restart/hooks@^0.4.7":
|
||||||
@ -24891,7 +24891,7 @@ react-router-dom@6.3.0, react-router-dom@^6.11.2:
|
|||||||
|
|
||||||
react-router@6.14.1, react-router@6.3.0, react-router@^6.11.2:
|
react-router@6.14.1, react-router@6.3.0, react-router@^6.11.2:
|
||||||
version "6.14.1"
|
version "6.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810"
|
resolved "https://registry.npmjs.org/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810"
|
||||||
integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g==
|
integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@remix-run/router" "1.7.1"
|
"@remix-run/router" "1.7.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user