feat(数据表): REST API (#1567)

Reviewed-on: daoyoucloud/tachybase#1567
Reviewed-by: sealday <zhanglin@daoyoucloud.com>
Co-authored-by: bai.zixv <bai.zixv@foxmail.com>
Co-committed-by: bai.zixv <bai.zixv@foxmail.com>
This commit is contained in:
bai.zixv 2024-10-01 21:45:10 +08:00 committed by sealday
parent 4d619cffe5
commit 71caef1b2b
93 changed files with 4179 additions and 47 deletions

View File

@ -10,3 +10,4 @@ export * from './types';
export * from './data-source-with-database';
export * from './utils';
export * from './collection';

View File

@ -17,11 +17,15 @@ import {
SyncFieldsAction,
SyncFieldsActionCom,
SyncSQLFieldsAction,
usePlugin,
ViewCollectionField,
ViewFieldAction,
} from '@tachybase/client';
import { ISchema, uid } from '@tachybase/schema';
import { useLocation } from 'react-router-dom';
import PluginDatabaseConnectionsClient from '../../';
import { ConfigurationTable } from './ConfigurationTable';
import { ConfigurationTabs } from './ConfigurationTabs';
@ -35,6 +39,11 @@ const schema2: ISchema = {
};
export const CollectionManagerPage = () => {
const plugin = usePlugin(PluginDatabaseConnectionsClient);
const location = useLocation();
const dataSourceType = new URLSearchParams(location.search).get('type');
const type = dataSourceType && plugin.types.get(dataSourceType);
return (
<SchemaComponent
schema={schema2}
@ -44,11 +53,11 @@ export const CollectionManagerPage = () => {
ConfigurationTabs,
AddFieldAction,
AddCollectionField,
AddCollection,
AddCollection: type?.AddCollection || AddCollection,
AddCollectionAction,
EditCollection,
EditCollection: type?.EditCollection || EditCollection,
EditCollectionAction,
DeleteCollection,
DeleteCollection: type?.DeleteCollection || DeleteCollection,
DeleteCollectionAction,
EditFieldAction,
EditCollectionField,
@ -60,6 +69,12 @@ export const CollectionManagerPage = () => {
SyncFieldsActionCom,
SyncSQLFieldsAction,
}}
scope={{
allowCollectionDeletion: !!type?.allowCollectionDeletion,
disabledConfigureFields: type?.disabledConfigureFields,
disableAddFields: type?.disableAddFields,
allowCollectionCreate: !!type?.allowCollectionCreate,
}}
/>
);
};

View File

@ -168,7 +168,7 @@ export const collectionTableSchema: ISchema = {
'x-component-props': {
type: 'primary',
},
'x-visible': false,
'x-visible': '{{allowCollectionCreate}}',
},
},
},

View File

@ -30,7 +30,7 @@ export const ViewDatabaseConnectionAction = () => {
style={{ padding: '0px' }}
disabled={!record.enabled}
onClick={() => {
navigate(getConnectionCollectionPath(record.key));
navigate(getConnectionCollectionPath(record));
}}
role="button"
aria-label={`${record?.key}-Configure`}

View File

@ -1,2 +1,2 @@
export const getConnectionCollectionPath = (name: string | number) =>
`/admin/settings/data-source-manager/${name}/collections`;
export const getConnectionCollectionPath = ({ key, type }: { key: string | number; type: string }) =>
`/admin/settings/data-source-manager/${key}/collections?type=${type}`;

View File

@ -3,11 +3,13 @@
"version": "0.22.5",
"main": "dist/server/index.js",
"dependencies": {
"@ant-design/icons": "^5.4.0",
"antd": "5.19.4",
"lodash": "^4.17.21",
"mysql2": "^3.9.1",
"pg": "^8.11.3",
"react-i18next": "^14.1.2"
"react-i18next": "^14.1.2",
"react-router-dom": "^6.25.1"
},
"devDependencies": {
"@types/lodash": "^4.17.5"

View File

@ -0,0 +1,9 @@
import React from 'react';
import { useRecord } from '@tachybase/client';
import { AddCollectionAction } from './AddCollectionAction.component';
export const AddCollection = (props) => {
const record = useRecord();
return <AddCollectionAction item={record} {...props} />;
};

View File

@ -0,0 +1,26 @@
import React, { useState } from 'react';
import { ActionContextProvider, RecordProvider } from '@tachybase/client';
import { PlusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { useTranslation } from '../../../locale';
import { ViewCreateCollection } from './CreateCollection.view';
export const AddCollectionAction = (props) => {
const { t } = useTranslation(true);
const { scope, getContainer, item } = props;
const [visible, setVisible] = useState(false);
const handleClick = () => setVisible(true);
return (
<RecordProvider record={item}>
<ActionContextProvider value={{ visible, setVisible }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleClick}>
{t('Create collection')}
</Button>
<ViewCreateCollection scope={scope} getContainer={getContainer} item={item} />
</ActionContextProvider>
</RecordProvider>
);
};

View File

@ -0,0 +1,139 @@
import { useRequest } from '@tachybase/client';
import { uid } from '@tachybase/schema';
import lodash from 'lodash';
import { NAMESPACE, tval } from '../../../locale';
import { PreviewComponent } from './PreviewComponent';
import { PreviewFields } from './PreviewFields';
import { getSchemaRequestAction } from './getSchemaRequestAction';
export function getSchemaCollection(title, useAction, item: Record<string, any> = {}) {
const cloneItem = lodash.cloneDeep(item);
const data: Record<string, any> = {
name: `t_${uid()}`,
...cloneItem,
};
if (data.reverseField) {
data.reverseField.name = `f_${uid()}`;
}
return {
type: 'object',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Drawer',
'x-component-props': {
getContainer: '{{ getContainer }}',
},
'x-decorator': 'Form',
'x-decorator-props': {
useValues(val) {
return useRequest(() => Promise.resolve({ data }), val);
},
},
title,
properties: {
title: {
type: 'string',
title: '{{ t("Collection display name") }}',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
name: {
type: 'string',
title: '{{t("Collection name")}}',
required: true,
'x-disabled': '{{ !createOnly }}',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'uid',
description:
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
},
description: {
title: '{{t("Description")}}',
type: 'string',
name: 'description',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
actions: {
type: 'void',
title: tval('Request actions'),
'x-decorator': 'FormItem',
'x-component': 'FormCollapse',
'x-component-props': {
size: 'small',
accordion: true,
defaultActiveKey: [],
},
properties: {
list: getSchemaRequestAction('list', `{{t("List",{ ns: "${NAMESPACE}" })}}`),
get: getSchemaRequestAction('get', `{{t("Get",{ ns: "${NAMESPACE}" })}}`),
create: getSchemaRequestAction('create', `{{t("Create",{ ns: "${NAMESPACE}" })}}`),
update: getSchemaRequestAction('update', `{{t("Update",{ ns: "${NAMESPACE}" })}}`),
destroy: getSchemaRequestAction('destroy', `{{t("Destroy",{ ns: "${NAMESPACE}" })}}`),
},
},
fields: {
type: 'array',
required: true,
'x-component': PreviewFields,
'x-decorator': 'FormItem',
title: tval('Fields', true),
},
filterTargetKey: {
title: tval('Record unique key'),
required: true,
type: 'single',
description: tval(
'If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.',
),
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'],
},
preview: {
type: 'void',
'x-visible': '{{ createOnly }}',
'x-component': PreviewComponent,
'x-reactions': {
dependencies: ['fields'],
fulfill: {
schema: {
'x-component-props': '{{$form.values}}',
},
},
},
},
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,
},
},
},
},
},
},
},
};
}

View File

@ -0,0 +1,37 @@
import React from 'react';
import { FormItem, SchemaComponent, TemplateSummary, useCancelAction } from '@tachybase/client';
import { ArrayItems, ArrayTable, FormCollapse, FormLayout } from '@tachybase/components';
import { tval } from '../../../locale';
import { getSchemaCollection } from './CreateCollection.schema';
import { useActionCreateCollection } from './useActionCreateCollection';
export const ViewCreateCollection = (props) => {
const { scope, getContainer, item } = props;
const title = tval('Create collection', true);
const schema = getSchemaCollection(title, useActionCreateCollection);
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
// NOTE: 依赖的组件内部命名有误, 在这里进行命名映射
TemplateSummay: TemplateSummary,
FormCollapse,
ArrayItems,
FormLayout,
FormItem,
}}
scope={{
createOnly: true,
record: item,
showReverseFieldConfig: true,
presetFieldsDisabled: true,
getContainer,
useCancelAction,
...scope,
}}
/>
);
};

View File

@ -0,0 +1,105 @@
import React, { useEffect, useState } from 'react';
import { EllipsisWithTooltip, useCollectionManager_deprecated, useCompile } from '@tachybase/client';
import { RecursionField, uid, useForm } from '@tachybase/schema';
import { Table } from 'antd';
import { useTranslation } from '../../../locale';
export const PreviewComponent = (props) => {
const form = useForm();
const compile = useCompile();
const { t } = useTranslation();
const { fields, preview } = props;
const [columns, setColumns] = useState([]);
const [dataSource, setDataSource] = useState(preview);
const { getInterface } = useCollectionManager_deprecated();
const filterAndMapSchemas = (schemas) => {
// 过滤出有 source 或 interface 属性的项
const filteredSchemas = schemas.filter((schema) => schema.source || schema.interface);
// 如果没有过滤出的项,则返回 undefined
if (!filteredSchemas) {
return undefined;
}
// 映射过滤出的项,创建新的表格列配置数组
const columnsConfig = filteredSchemas.map((schema) => {
const schemaTitle = schema?.uiSchema?.title || schema.name;
const defaultUiSchema = getInterface(schema.interface)?.default.uiSchema;
return {
key: schema.name,
title: compile(schemaTitle),
dataIndex: schema.name,
width: 200,
render: (text, record, name) => {
const value = record[schema.name];
const mySchema = {
type: 'object',
properties: {
[schema.name]: {
name: `${schema.name}`,
...defaultUiSchema,
'x-read-pretty': true,
default: schema.interface === 'json' || typeof value == 'object' ? JSON.stringify(value) : value,
},
},
};
return (
<EllipsisWithTooltip ellipsis>
<RecursionField schema={mySchema} name={name} onlyRenderProperties />
</EllipsisWithTooltip>
);
},
};
});
return columnsConfig;
};
useEffect(() => {
setDataSource(preview);
}, [preview, fields]);
useEffect(() => {
setColumns([]);
const schemaColumns = filterAndMapSchemas(fields);
setColumns(schemaColumns);
}, [form.values.fields]);
return (
<div key={uid()} style={{ marginBottom: 'var(--nb-spacing)' }}>
{dataSource?.length > 0 && (
<>
<div
className="ant-formily-item-label"
style={{ marginTop: 'var(--nb-spacing)', display: 'flex', padding: '0 0 8px' }}
>
<div className="ant-formily-item-label-content">
<span>
<label>{t('Preview')}</label>
</span>
</div>
<span className="ant-formily-item-colon">:</span>
</div>
<Table
key="preview"
size="middle"
pagination={false}
bordered
columns={columns}
dataSource={dataSource}
scroll={{
x: 1000,
y: 300,
}}
/>
</>
)}
</div>
);
};

View File

@ -0,0 +1,211 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useCollectionManager_deprecated, useCompile, useFieldInterfaceOptions } from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import { App, Button, Input, Select, Table, Tag } from 'antd';
import lodash from 'lodash';
import jsxRuntime from 'react/jsx-runtime';
import { useTranslation } from '../../../locale';
import { filterTree } from '../utils/filterTree';
export const PreviewFields = (props) => {
const compile = useCompile();
const { getInterface } = useCollectionManager_deprecated();
const { t } = useTranslation();
const form = useForm();
const field: any = useField();
const [dataSource, setDataSource] = useState(field.value);
const fieldInterfaceOptions = useFieldInterfaceOptions().filter(
(options) => !['relation', 'systemInfo'].includes(options.key),
);
const { modal } = App.useApp();
const updateDataSource = (item, index) => {
setTimeout(() => {
dataSource.splice(index, 1, item);
setDataSource(dataSource);
field.value = dataSource.concat();
}, 300);
};
const deleteDataSoureItem = (item) => {
dataSource.splice(item, 1);
const newDataSource = dataSource.concat();
field.value = newDataSource;
setDataSource(newDataSource);
};
const onClick = () => {
modal.confirm({
title: t('Are you sure you want to clear fields?'),
onOk: () => {
form.setValuesIn('fields', []);
form.setValuesIn('preview', []);
},
});
};
const columnsData: any = useMemo(
() => [
{
dataIndex: 'index',
width: 60,
key: 'index',
render: (text, record, index) => index + 1,
},
{
title: t('Field display name'),
dataIndex: 'title',
key: 'title',
width: 150,
render: (text, record, index) => {
const targetDataSource = dataSource[index];
return jsxRuntime.jsx(Input, {
defaultValue: targetDataSource.uiSchema.title,
onChange: lodash.debounce((changeValue) => {
updateDataSource(
{
...targetDataSource,
uiSchema: {
...lodash.omit(targetDataSource?.uiSchema, 'rawTitle'),
title: changeValue.target.value,
},
},
index,
);
}, 300),
});
},
},
{
title: t('Field name'),
dataIndex: 'name',
key: 'name',
width: 130,
},
{
title: t('Field type'),
dataIndex: 'type',
width: 140,
key: 'type',
render: (text, record, index) => {
const targetDataSource = dataSource[index];
return targetDataSource?.source || !targetDataSource?.possibleTypes ? (
<Tag> {text}</Tag>
) : (
<Select
defaultValue={text}
popupMatchSelectWidth={false}
style={{ width: '100%' }}
options={
targetDataSource?.possibleTypes.map((type) => ({
label: type,
value: type,
})) || []
}
onChange={(type) => {
updateDataSource(
{
...targetDataSource,
type,
},
index,
);
}}
/>
);
},
},
{
title: t('Field interface'),
dataIndex: 'interface',
key: 'interface',
width: 150,
render: (text, record, index) => {
const targetItem = dataSource[index];
const treeChild = targetItem?.type && filterTree(fieldInterfaceOptions, targetItem?.type);
const handleSelectChange = (interfaceVal) => {
const fieldInterface = getInterface(interfaceVal);
updateDataSource(
{
...targetItem,
interface: interfaceVal,
uiSchema: {
...targetItem.uiSchema,
...fieldInterface?.default?.uiSchema,
},
},
index,
);
};
return (
<Select
defaultValue={text}
style={{ width: '100%' }}
popupMatchSelectWidth={false}
onChange={handleSelectChange}
>
{treeChild?.map((child) => (
<Select.OptGroup key={child.key} label={compile(child.label)}>
{child.children.map(({ label, value, name }) => (
<Select.Option key={value} value={name}>
{compile(label)}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
);
},
},
{
title: t('Actions'),
dataIndex: 'actions',
fixed: 'right',
key: 'actions',
align: 'center',
width: 100,
render: (text, record, index) => <a onClick={() => deleteDataSoureItem(index)}> {t('Delete')}</a>,
},
],
[dataSource],
);
useEffect(() => {
setDataSource(field.value);
}, [field.value]);
return (
<>
{dataSource.length ? (
<Button
style={{
position: 'absolute',
top: '-10px',
right: '0px',
}}
onClick={onClick}
>
{t('Clear')}
</Button>
) : null}
<Table
bordered
size="middle"
columns={columnsData}
dataSource={dataSource}
scroll={{
y: 300,
}}
pagination={false}
/>
</>
);
};
PreviewFields.displayName = 'PreviewFields';

View File

@ -0,0 +1,29 @@
import React from 'react';
import { createForm, useForm } from '@tachybase/schema';
import { ProviderContextRequestInfo } from '../contexts/RequestForm.context';
export const ProviderRequestActionItems = (props) => {
const form = useForm();
const { actionKey } = props;
const requestActionForm = React.useMemo(() => createForm({}), []);
const [responseTransformer, setResponseTransformer] = React.useState(null);
return (
<ProviderContextRequestInfo
value={{
form: {
...form,
[actionKey]: requestActionForm,
},
actionKey,
requestActionForm,
responseTransformer,
setResponseTransformer,
}}
>
{props.children}
</ProviderContextRequestInfo>
);
};

View File

@ -0,0 +1,107 @@
import React from 'react';
import { css, useRequest } from '@tachybase/client';
import { onFieldChange, uid, useForm } from '@tachybase/schema';
import { unflatten } from '@tachybase/utils/client';
import lodash from 'lodash';
import { NAMESPACE } from '../../../locale';
import { DebugComponent } from '../request-configs/debug-area/DebugComponent';
import { MethodPathComponent } from '../request-configs/method-path/MethodPathComponent';
import { RequestTab } from '../request-configs/request-tab/RequestTab';
import { ResponseTransformerComponent } from '../request-configs/request-transformer/ResponseTransformerComponent';
import { ProviderRequestActionItems } from './RequestActionItems.provider';
export const getSchemaRequestAction = (key, header) => {
const ref: any = React.useRef();
return {
type: 'void',
'x-decorator': ProviderRequestActionItems,
'x-decorator-props': {
actionKey: key,
},
'x-component': 'FormCollapse.CollapsePanel',
'x-component-props': {
header,
key,
},
properties: {
form: {
'x-decorator': 'Form',
'x-decorator-props': {
className: css`
.ant-formily-item-feedback-layout-loose {
margin-bottom: 10px;
}
`,
useValues(val) {
ref.current = useForm();
return useRequest(() => Promise.resolve(), val);
},
effects: () => {
const updateForm = (targetField) => {
const form = ref.current;
const { actions } = form.values || {};
const { path, value } = targetField.getState();
path.includes('add') ||
form.setValuesIn('actions', {
...actions,
[key]: unflatten({
...actions?.[key],
[path]: value,
}),
});
};
const updateFunc = lodash.debounce(async (field, form) => {
if (form.modified) {
await updateForm(field);
}
}, 400);
onFieldChange('*', (field, form) => {
if (form.modified) {
updateFunc(field, form);
}
});
},
},
type: 'void',
properties: {
[uid()]: {
type: 'void',
properties: {
requestAction: {
type: 'string',
'x-component': MethodPathComponent,
},
requestTab: {
type: 'void',
'x-decorator': 'FormItem',
title: `{{t("Adapt request parameters",{ ns: "${NAMESPACE}" })}}`,
'x-component': RequestTab,
'x-decorator-props': {
tooltip: `{{t("Provide request variables from TachyBase for use by third-party APIs.",{ ns: "${NAMESPACE}" })}}`,
},
},
responseTransformer: {
type: 'string',
'x-decorator': 'FormItem',
title: `{{t("Convert third-party response results to NocoBase standard",{ ns: "${NAMESPACE}" })}}`,
'x-component': ResponseTransformerComponent,
'x-decorator-props': {
tooltip: `{{t("The response results from third-party APIs need to be converted to the NocoBase standard to display correctly on the frontend.",{ ns: "${NAMESPACE}" })}}`,
},
},
debug: {
type: 'void',
'x-component': DebugComponent,
},
},
},
},
},
},
};
};

View File

@ -0,0 +1,78 @@
import {
useActionContext,
useAPIClient,
useCollectionManager_deprecated,
useDataSourceManager,
useResourceActionContext,
} from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import lodash from 'lodash';
import { useParams } from 'react-router-dom';
import { useTranslation } from '../../../locale';
import { filterObjectWithMethodAndPath } from '../utils/filterObjectWithMethodAndPath';
import { getRequestActions } from '../utils/getRequestActions';
export function useActionCreateCollection() {
const form = useForm();
const { refreshCM: refreshCM } = useCollectionManager_deprecated();
const ctx = useActionContext();
const { refresh: refresh } = useResourceActionContext();
const apiClient = useAPIClient();
const { name: name } = useParams();
const targetCollectionRepo = apiClient.resource('dataSources.collections', name);
const field = useField();
const dm = useDataSourceManager();
useTranslation();
return {
async run() {
field.data = field.data || {};
field.data.loading = true;
try {
await form.submit();
const cloneFormValues = lodash.cloneDeep(form.values);
const requestActionsForm = filterObjectWithMethodAndPath(cloneFormValues?.actions);
const requestActionsRestMethod = getRequestActions(Object.keys(requestActionsForm));
const pickedFormValues = lodash.pick(cloneFormValues, ['fields', 'name', 'title', 'filterTargetKey']);
const targetMethod = requestActionsRestMethod[0];
if (targetMethod) {
form.query(`*.actions.${targetMethod}`).take((field) => {
field.setComponentProps({
style: { border: '1px solid #ff4d4f' },
});
});
await form[targetMethod].submit();
} else {
await targetCollectionRepo.create({
values: {
logging: true,
...pickedFormValues,
actions: requestActionsForm,
},
});
ctx.setVisible(false);
await form.reset();
field.data.loading = false;
refresh();
await refreshCM();
dm.getDataSource(name).reload();
}
} catch (error) {
console.error(error);
field.data.loading = false;
}
},
};
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import { useRecord } from '@tachybase/client';
import { DeleteCollectionAction } from './DeleteCollectionAction.component';
export const DeleteCollection = (props) => {
const item = useRecord();
return <DeleteCollectionAction item={item} {...props} />;
};

View File

@ -0,0 +1,41 @@
import React, { useState } from 'react';
import { ActionContextProvider, RecordProvider } from '@tachybase/client';
import { DeleteOutlined } from '@ant-design/icons';
import { App, Button } from 'antd';
import _ from 'lodash';
import { useTranslation } from '../../../locale';
import { useBulkDestroyActionAndRefreshCM } from './useBulkDestroyActionAndRefreshCM';
import { useDestroyActionAndRefreshCM } from './useDestroyActionAndRefreshCM';
export const DeleteCollectionAction = (props) => {
const { t } = useTranslation();
const { modal } = App.useApp();
const { item, isBulk, children, ...restProps } = props;
const targetProps = _.omit(restProps, ['scope', 'getContainer', 'useAction']);
const [visible, setVisible] = useState(false);
const { run } = isBulk ? useBulkDestroyActionAndRefreshCM() : useDestroyActionAndRefreshCM();
const onClick = () => {
modal.confirm({
title: t('Delete collection'),
content: t('Are you sure you want to delete it?'),
onOk: run,
});
};
return (
<RecordProvider record={item}>
<ActionContextProvider value={{ visible, setVisible }}>
{isBulk ? (
<Button icon={<DeleteOutlined />} onClick={onClick} children={children || t('Delete')} />
) : (
<a onClick={onClick} {...targetProps}>
{children || t('Delete')}
</a>
)}
</ActionContextProvider>
</RecordProvider>
);
};
DeleteCollectionAction.displayName = 'DeleteCollectionAction';

View File

@ -0,0 +1,52 @@
import {
useActionContext,
useAPIClient,
useCollectionManager_deprecated,
useResourceActionContext,
} from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { message } from 'antd';
import _ from 'lodash';
import { useParams } from 'react-router-dom';
import { useTranslation } from '../../../locale';
export function useBulkDestroyActionAndRefreshCM() {
const { run: runFunc } = useBulkDestroyAction();
const { refreshCM } = useCollectionManager_deprecated();
return {
async run() {
await runFunc();
await refreshCM();
},
};
}
function useBulkDestroyAction() {
const apiClient = useAPIClient();
const { t } = useTranslation();
const form = useForm();
const ctx = useActionContext();
const { name } = useParams();
const { state, setState, refresh } = useResourceActionContext();
return {
async run() {
if (!state?.selectedRowKeys || state.selectedRowKeys.length === 0) {
return message.error(t('Please select the records you want to delete'));
}
// 调用 API 客户端删除选中的记录
await apiClient.resource('dataSources.collections', name).destroy({ filterByTk: state?.selectedRowKeys || [] });
// 重置表单
form.reset();
// 如果存在 setVisible 方法,则调用它关闭对话框
ctx?.setVisible?.(false);
// 重置选中的行键状态
setState?.({ selectedRowKeys: [] });
refresh();
},
};
}

View File

@ -0,0 +1,31 @@
import { useCollectionManager_deprecated, useAPIClient, useActionContext, useResourceContext, useRecord, useResourceActionContext } from "@tachybase/client";
import { useParams } from "react-router-dom";
export function useDestroyActionAndRefreshCM() {
const { run: runFunc } = useDestroyAction();
const { refreshCM } = useCollectionManager_deprecated();
return {
async run() {
await runFunc();
await refreshCM();
},
};
}
function useDestroyAction() {
const apiClient = useAPIClient();
const ctx = useActionContext();
const { targetKey } = useResourceContext();
const { [targetKey]: filterByTk } = useRecord();
const { name } = useParams();
const { refresh } = useResourceActionContext();
return {
async run() {
await apiClient.resource('dataSources.collections', name).destroy({ filterByTk });
ctx?.setVisible?.(false);
refresh();
},
};
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import { useRecord } from '@tachybase/client';
import { EditCollectionAction } from './EditCollectionAction.component';
export const EditCollection = (props) => {
const record = useRecord();
return <EditCollectionAction item={record} {...props} />;
};

View File

@ -0,0 +1,20 @@
import React, { useState } from 'react';
import { ActionContextProvider, RecordProvider } from '@tachybase/client';
import { useTranslation } from '../../../locale';
import { ViewEditCollectionForm } from './EditCollectionForm.view';
export const EditCollectionAction = (props) => {
const { t } = useTranslation();
const { scope, getContainer, item } = props;
const [visible, setVisible] = useState(false);
return (
<RecordProvider record={item}>
<ActionContextProvider value={{ visible, setVisible }}>
<a onClick={() => setVisible(true)}>{t('Edit')}</a>
<ViewEditCollectionForm scope={scope} item={item} getContainer={getContainer} />
</ActionContextProvider>
</RecordProvider>
);
};

View File

@ -0,0 +1,34 @@
import React from 'react';
import { FormItem, SchemaComponent, TemplateSummary, useCancelAction } from '@tachybase/client';
import { ArrayItems, ArrayTable, FormCollapse, FormLayout } from '@tachybase/components';
import { getSchemaCollection } from '../collection-add/CreateCollection.schema';
import { useActionEditCollection } from './useActionEditCollection';
export const ViewEditCollectionForm = (props) => {
const { scope, getContainer, item } = props;
const schema = getSchemaCollection('{{ t("Edit collection") }}', useActionEditCollection, item);
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
TemplateSummay: TemplateSummary,
FormCollapse,
ArrayItems,
FormLayout,
FormItem,
}}
scope={{
createOnly: true,
record: item,
showReverseFieldConfig: true,
presetFieldsDisabled: true,
getContainer,
useCancelAction: useCancelAction,
...scope,
}}
/>
);
};

View File

@ -0,0 +1,77 @@
import {
useActionContext,
useAPIClient,
useCollectionManager_deprecated,
useDataSourceManager,
useRecord,
useResourceActionContext,
useResourceContext,
} from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import lodash from 'lodash';
import { useParams } from 'react-router-dom';
import { filterObjectWithMethodAndPath } from '../utils/filterObjectWithMethodAndPath';
import { getRequestActions } from '../utils/getRequestActions';
export const useActionEditCollection = (e) => {
const apiClient = useAPIClient();
const dm = useDataSourceManager();
const { refreshCM } = useCollectionManager_deprecated();
const { refresh } = useResourceActionContext();
const form = useForm();
const ctx = useActionContext();
const { targetKey } = useResourceContext();
const { name } = useParams();
const { [targetKey]: keyValue } = useRecord();
const targetRepo = apiClient.resource('dataSources.collections', name);
return {
async run() {
await form.submit();
const cloneValue = lodash.cloneDeep(form.values);
const actionValue = filterObjectWithMethodAndPath(cloneValue?.actions);
const requestActionsRestMethod = getRequestActions(Object.keys(actionValue));
const pickedFormValues = lodash.pick(form.values, ['fields', 'name', 'title', 'filterTargetKey']);
const targetMethod = requestActionsRestMethod[0];
if (targetMethod) {
form.query(`*.actions.${targetMethod}`).take((formVal) => {
formVal.setComponentProps({
style: {
border: '1px solid #ff4d4f',
},
});
});
await form[targetMethod].submit();
} else {
await targetRepo.update({
filterByTk: keyValue,
values: {
...pickedFormValues,
actions: actionValue,
},
});
ctx.setVisible(false);
const dataSource = dm.getDataSource(name);
if (!dataSource?.reload) {
dataSource.reload.call(dataSource);
}
await form.reset();
refresh();
await refreshCM();
}
},
};
};

View File

@ -0,0 +1,7 @@
export const paramsMap = {
list: ['page', 'pageSize', 'filter', 'sort', 'appends', 'fields', 'except'],
get: ['filterByTk', 'filter', 'sort', 'appends', 'fields', 'except'],
create: ['whiteList', 'blacklist', 'body'],
update: ['filterByTk', 'filter', 'whiteList', 'blacklist', 'body'],
destroy: ['filterByTk', 'filter'],
};

View File

@ -0,0 +1,26 @@
export const requestHeaderList = [
{
name: 'X-App',
title: 'X-App',
},
{
name: 'X-Locale',
title: 'X-Locale',
},
{
name: 'X-Hostname',
title: 'X-Hostname',
},
{
name: 'X-Timezone',
title: 'X-Timezone',
},
{
name: 'X-Role',
title: 'X-Role',
},
{
name: 'X-Authenticator',
title: 'X-Authenticator',
},
];

View File

@ -0,0 +1,19 @@
import React from 'react';
// NOTE: 请求信息参数
interface IContextRequestInfo {
actionKey?: string;
form?: any;
requestActionForm?: any;
responseTransformer?: any;
setResponseTransformer?: any;
}
// TODO: 删除全部导出, 改为 provider 和 context 形式
export const ContextRequestInfo: any = React.createContext<IContextRequestInfo>({});
export const ProviderContextRequestInfo = ContextRequestInfo.Provider;
export function useContextRequestInfo(): IContextRequestInfo {
return React.useContext(ContextRequestInfo);
}

View File

@ -0,0 +1,18 @@
import React from 'react';
interface IContextResponseInfo {
rawResponse?: any;
debugResponse?: any;
responseValidationErrorMessage?: any;
setRawResponse?: any;
setDebugResponse?: any;
setResponseValidationErrorMessage?: any;
}
const ContextResponseInfo = React.createContext<IContextResponseInfo>({});
export const ProviderContextResponseInfo = ContextResponseInfo.Provider;
export function useContextResponseInfo(): IContextResponseInfo {
return React.useContext(ContextResponseInfo);
}

View File

@ -0,0 +1,188 @@
import { css } from '@tachybase/client';
import { NAMESPACE } from '../../../locale';
export const schemaDataSourceSettingsForm = {
type: 'object',
properties: {
displayName: {
type: 'string',
title: `{{t("Data source display name",{ ns: "${NAMESPACE}" })}}`,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
key: {
type: 'string',
title: `{{t("Data source name",{ ns: "${NAMESPACE}" })}}`,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'uid',
'x-disabled': '{{ createOnly }}',
description: `{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.',{ ns: "${NAMESPACE}" })}}`,
},
options: {
type: 'object',
properties: {
baseUrl: {
type: 'string',
title: `{{t("BaseURL",{ ns: "${NAMESPACE}" })}}`,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input.URL',
'x-validator': 'url',
},
headers: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `{{t("Headers", { ns: "${NAMESPACE}" })}}`,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: { flexWrap: 'nowrap', maxWidth: '100%', display: 'flex' },
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
useTypedConstant: true,
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add request header", { ns: "${NAMESPACE}" })}}`,
'x-component': 'ArrayItems.Addition',
},
},
},
variables: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `{{t("Variables", { ns: "${NAMESPACE}" })}}`,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
display: 'flex',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': { placeholder: '{{t("Name")}}' },
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': { useTypedConstant: true },
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add variable", { ns: "${NAMESPACE}" })}}`,
'x-component': 'ArrayItems.Addition',
},
},
},
timeout: {
type: 'string',
title: `{{t("Timeout",{ ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
addonAfter: 'ms',
style: {
minWidth: 200,
},
},
default: 5e3,
},
responseType: {
type: 'string',
title: `{{t("Response type",{ ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Select',
default: 'json',
enum: [
{
value: 'json',
label: `{{t("JSON",{ ns: "${NAMESPACE}" })}}`,
},
],
},
},
},
enabled: {
type: 'string',
'x-content': `{{t("Enabled the data source",{ ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
default: true,
},
},
};

View File

@ -0,0 +1,10 @@
import React from 'react';
import { SchemaComponent, Space } from '@tachybase/client';
import { useTranslation } from '../../../locale';
import { schemaDataSourceSettingsForm as schema } from './DataSourceSettingsForm.schema';
export const DataSourceSettingsForm = () => {
const { t } = useTranslation();
return <SchemaComponent schema={schema} components={{ Space }} scope={{ t }} />;
};

View File

@ -0,0 +1,26 @@
import { Plugin } from '@tachybase/client';
import PluginDataSourceManagerClient from '@tachybase/plugin-data-source-manager/client';
import { tval } from '../../locale';
import { AddCollection } from './collection-add/AddCollection';
import { DeleteCollection } from './collection-delete/DeleteCollection';
import { EditCollection } from './collection-edit/EditCollection';
import { DataSourceSettingsForm } from './form-data-source/DataSourceSettingsForm';
export class KitHttpDatasource extends Plugin {
async load() {
this.app.pm.get(PluginDataSourceManagerClient).registerType('http', {
label: tval('REST API'),
allowCollectionCreate: true,
allowCollectionDeletion: false,
disabledConfigureFields: false,
disableAddFields: true,
disableTestConnection: true,
// 组件
DataSourceSettingsForm,
AddCollection,
EditCollection,
DeleteCollection,
});
}
}

View File

@ -0,0 +1,28 @@
import React, { useState } from 'react';
import { ActionContextProvider } from '@tachybase/client';
import { ProviderContextResponseInfo } from '../../contexts/ResponseInfo.context';
export const ProviderDebug = (props) => {
const { visible, setVisible } = props;
const [rawResponse, setRawResponse] = useState(null);
const [debugResponse, setDebugResponse] = useState(null);
const [responseValidationErrorMessage, setResponseValidationErrorMessage] = useState(null);
return (
<ActionContextProvider visible={visible} setVisible={setVisible}>
<ProviderContextResponseInfo
value={{
rawResponse,
debugResponse,
responseValidationErrorMessage,
setRawResponse,
setDebugResponse,
setResponseValidationErrorMessage,
}}
>
{props.children}
</ProviderContextResponseInfo>
</ActionContextProvider>
);
};

View File

@ -0,0 +1,236 @@
import { css, cx } from '@tachybase/client';
import { onFieldChange } from '@tachybase/schema';
import { unflatten } from '@tachybase/utils/client';
import lodash from 'lodash';
import { useTranslation } from '../../../../locale';
import { AlertError } from './components/AlertError';
import { DebugResponseTabs } from './components/DebugResponseTabs';
import { ResponseTab } from './components/ResponseTab';
import { ExtractFieldMetadata } from './components/ExtractFieldMetadata';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { getSchemaParam } from './schemas/getSchemaParam';
import { MethodPathComponent } from '../method-path/MethodPathComponent';
import { RequestTab } from '../request-tab/RequestTab';
import { ResponseTransformerComponent } from '../request-transformer/ResponseTransformerComponent';
export const getSchemaDebug = () => {
const { t } = useTranslation();
const { actionKey, form, requestActionForm } = useContextRequestInfo();
const schemaChild = getSchemaParam(actionKey, form);
const handleResponseEffects = () => {
const update = async (field) => {
const { actions } = form.values || {};
const { path, value } = field.getState();
if (!path.includes('add')) {
await form.setValuesIn('actions', {
...actions,
[actionKey]: unflatten({
...actions?.[actionKey],
[path]: value,
}),
});
if (['method', 'path'].includes(path)) {
await requestActionForm.setValuesIn([path], value);
}
}
};
const debounceUpdate = lodash.debounce(update, 400);
onFieldChange('*', (field, form) => {
if (form.modified) {
debounceUpdate(field);
}
});
};
return {
type: 'object',
properties: {
modal: {
type: 'void',
'x-decorator': 'Form',
'x-component': 'Action.Modal',
'x-component-props': {
className: cx(
'nb-action-popup',
css`
.ant-modal-content {
padding: 0;
}
.ant-modal-footer {
position: absolute;
bottom: 24px;
right: 24px;
}
`,
),
width: '90%',
styles: {
body: {
padding: 0,
},
},
centered: true,
destroyOnClose: true,
},
properties: {
bodyContainer: {
type: 'void',
'x-component': 'Row',
'x-component-props': {
gutter: 1,
},
properties: {
request: {
type: 'void',
'x-decorator': 'Col',
'x-decorator-props': {
span: 8,
},
'x-component': 'Card',
'x-component-props': {
bordered: false,
title: t('TachyBase request'),
style: {
height: '80vh',
overflowY: 'auto',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
},
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelCol: 6,
wrapperCol: 10,
layout: 'vertical',
className: css`
.ant-formily-item-feedback-layout-loose {
margin-bottom: 5px;
}
`,
},
properties: schemaChild,
},
},
},
thirdPartyApi: {
type: 'void',
'x-decorator': 'Col',
'x-decorator-props': {
span: 8,
},
'x-component': 'Card',
'x-component-props': {
title: t('Third party API'),
bordered: false,
style: {
height: '80vh',
overflowY: 'auto',
borderRadius: 0,
},
},
properties: {
responseContainer: {
type: 'void',
'x-decorator': 'Form',
'x-decorator-props': {
effects: handleResponseEffects,
className: css`
.ant-formily-item-feedback-layout-loose {
margin-bottom: 10px;
}
`,
},
properties: {
requestAction: {
type: 'string',
'x-component': MethodPathComponent,
'x-component-props': {
actionForm: true,
},
},
requestTab: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': RequestTab,
},
responseTab: {
type: 'string',
'x-component': ResponseTab,
},
},
},
},
},
nocoBaseResponse: {
type: 'void',
'x-decorator': 'Col',
'x-decorator-props': {
span: 8,
},
'x-component': 'Card',
'x-component-props': {
bordered: false,
title: t('TachyBase response'),
style: {
height: '80vh',
overflowY: 'auto',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
},
properties: {
responseContainer: {
type: 'void',
'x-decorator': 'Form',
properties: {
alertError: {
type: 'string',
'x-component': AlertError,
},
responseTransformer: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': ResponseTransformerComponent,
},
debugResponse: {
type: 'string',
'x-component': DebugResponseTabs,
},
},
},
},
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Modal.Footer',
properties: {
debug: {
title: '{{t("Debug")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useDebugAction }}',
},
},
extractFieldMetadata: {
'x-component': ExtractFieldMetadata,
},
},
},
},
},
},
};
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import { Action, FormItem, Input, InputNumber, SchemaComponent, Space, Variable } from '@tachybase/client';
import { Form, FormLayout } from '@tachybase/components';
import { Card, Col, Row, Select } from 'antd';
import { useTranslation } from '../../../../locale';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { getSchemaDebug } from './Debug.schema';
import { useCancelAction } from './scopes/useCancelAction';
import { useDebugAction } from './scopes/useDebugAction';
export const ViewDebug = () => {
const { t } = useTranslation();
const schema = getSchemaDebug();
return (
<SchemaComponent
schema={schema}
components={{
Form,
Input,
Action,
FormItem,
InputNumber,
Card,
Variable,
Space,
Select,
FormLayout,
Row,
Col,
}}
scope={{
t,
useDebugAction,
useCancelAction,
useVariableOptions,
}}
/>
);
};

View File

@ -0,0 +1,30 @@
import React, { useState } from 'react';
import { Button } from 'antd';
import { useTranslation } from '../../../../locale';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { ProviderDebug } from './Debug.provider';
import { ViewDebug } from './Debug.view';
export const DebugComponent = () => {
const { t } = useTranslation();
const { requestActionForm } = useContextRequestInfo();
const [visible, setVisible] = useState(false);
const handleClick = async () => {
await requestActionForm.submit();
setVisible(true);
};
return (
<>
<Button type="primary" onClick={handleClick}>
{t('Try it out')}
</Button>
<ProviderDebug visible={visible} setVisible={setVisible}>
<ViewDebug />
</ProviderDebug>
</>
);
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Alert } from 'antd';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
export const AlertError = () => {
const { responseValidationErrorMessage: errorMessage } = useContextResponseInfo();
if (!errorMessage) {
return null;
}
return (
<>
<Alert type="error" showIcon message={errorMessage} />
<br />
</>
);
};

View File

@ -0,0 +1,33 @@
import { useRequest } from '@tachybase/client';
import { uid } from '@tachybase/schema';
export const getSchemaDebugResponse = (data) => ({
type: 'object',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(params) {
const result = useRequest(
() =>
Promise.resolve({
data: {},
}),
params,
);
return result;
},
},
properties: {
[uid()]: {
type: 'string',
default: data,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
style: {
maxHeight: '350px',
},
},
},
},
});

View File

@ -0,0 +1,21 @@
import React from 'react';
import { FormItem, Input, SchemaComponent } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getSchemaDebugResponse } from './DebugResponse.schema';
export const ViewDebugResponse = () => {
const { debugResponse } = useContextResponseInfo();
const schema = getSchemaDebugResponse(debugResponse);
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
FormItem,
Input,
}}
/>
);
};

View File

@ -0,0 +1,14 @@
import React from 'react';
import { ViewDebugResponse } from './DebugResponse.view';
export const getItemsDebugResponse = (params) => {
const { t } = params;
return [
{
key: 'body',
label: t('Body'),
children: <ViewDebugResponse />,
},
];
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Tabs } from 'antd';
import { useTranslation } from '../../../../../locale';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getItemsDebugResponse } from './DebugResponseTabs.items';
export const DebugResponseTabs = () => {
const { t } = useTranslation();
const { debugResponse } = useContextResponseInfo();
const items = getItemsDebugResponse({ t });
if (!debugResponse) {
return null;
}
return <Tabs defaultActiveKey="body" items={items} />;
};

View File

@ -0,0 +1,98 @@
import React, { useContext, useState } from 'react';
import { ActionContext, useAPIClient } from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { Button } from 'antd';
import lodash from 'lodash';
import { useParams } from 'react-router-dom';
import { useTranslation } from '../../../../../locale';
import { TooltipContainer } from './TooltipContainer';
import { useContextRequestInfo } from '../../../contexts/RequestForm.context';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
export const ExtractFieldMetadata = () => {
const form = useForm();
const apiClient = useAPIClient();
const { t } = useTranslation();
const { setVisible } = useContext(ActionContext);
const { name } = useParams();
const { rawResponse, responseValidationErrorMessage } = useContextResponseInfo();
const { form: formValue, actionKey } = useContextRequestInfo();
const { fields } = formValue.values;
const [loading, setLoading] = useState(false);
const debugVars = lodash.omit(form.values, 'responseTab');
const handleClick = async () => {
try {
setLoading(true);
const actionValue = formValue?.values?.actions?.[actionKey];
const repo = apiClient.resource('dataSources.httpCollections', name);
const {
data: { data: resData },
} = await repo.runAction({
values: {
debug: false,
inferFields: true,
actionOptions: {
...actionValue,
type: actionKey,
responseTransformer: actionValue?.responseTransformer,
},
debugVars,
},
});
setVisible(false);
setLoading(false);
const { transformedResponse, fields: fieldsList, filterTargetKey } = resData;
if (filterTargetKey) {
formValue.setValuesIn('filterTargetKey', filterTargetKey);
}
if (fieldsList) {
const fieldVal = lodash.unionBy(fields, fieldsList, 'name');
formValue.setValuesIn('fields', fieldVal);
}
if (typeof transformedResponse?.data == 'object') {
let responseData = [];
if (actionKey === 'get') {
responseData = [transformedResponse?.data];
} else {
responseData = transformedResponse?.data;
}
formValue.setValuesIn('preview', responseData);
}
} catch (error) {
setLoading(false);
console.log(error);
}
};
if (!['list', 'get'].includes(actionKey)) {
return null;
}
return (
<TooltipContainer>
<Button
type="primary"
disabled={!rawResponse || responseValidationErrorMessage}
loading={loading}
onClick={handleClick}
>
{t('Extract field metadata')}
</Button>
</TooltipContainer>
);
};

View File

@ -0,0 +1,59 @@
import { NAMESPACE } from '../../../../../locale';
export const getSchemaHeaders = ({ key, defaultValue }) => ({
type: 'object',
properties: {
[key]: {
type: 'array',
'x-decorator': 'FormItem',
'x-component': 'ArrayTable',
default: defaultValue,
'x-component-props': {
scroll: {
y: 300,
},
},
items: {
type: 'object',
properties: {
column1: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 200,
title: `{{t("Name",{ ns: "${NAMESPACE}" })}}`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
},
},
},
},
column2: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 200,
title: `{{t("Value",{ ns: "${NAMESPACE}" })}}`,
},
properties: {
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Value")}}',
},
},
},
},
},
},
},
},
});

View File

@ -0,0 +1,25 @@
import React from 'react';
import { FormItem, Input, SchemaComponent } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getSchemaHeaders } from './Headers.schema';
export const ViewRequestHeaders = () => {
const { rawResponse } = useContextResponseInfo();
const { request } = rawResponse || {};
const requestHeaderMapList = Object.entries(request?.headers || {}).map(([name, value]) => ({ name, value }));
const schema = getSchemaHeaders({ key: 'requestHeaders', defaultValue: requestHeaderMapList });
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
FormItem,
Input,
}}
/>
);
};

View File

@ -0,0 +1,27 @@
import { useRequest } from '@tachybase/client';
import { uid } from '@tachybase/schema';
export const getSchemaResponseBody = (data) => ({
type: 'object',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(params) {
const result = useRequest(() => Promise.resolve({ data: {} }), params);
return result;
},
},
properties: {
[uid()]: {
type: 'string',
default: data,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
style: {
maxHeight: '400px',
},
},
},
},
});

View File

@ -0,0 +1,23 @@
import React from 'react';
import { FormItem, Input, SchemaComponent } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getSchemaResponseBody } from './ResponseBody.schema';
export const ViewResponseBody = () => {
const { rawResponse } = useContextResponseInfo();
const { data } = rawResponse || {};
const schema = getSchemaResponseBody(data);
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
FormItem,
Input,
}}
/>
);
};

View File

@ -0,0 +1,23 @@
import React from 'react';
import { FormItem, Input, SchemaComponent } from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getSchemaHeaders } from './Headers.schema';
export const ViewResponseHeaders = () => {
const { rawResponse } = useContextResponseInfo();
const { headers } = rawResponse || {};
const headerMapList = Object.entries(headers || {}).map(([name, value]) => ({ name, value }));
const schema = getSchemaHeaders({ key: 'responseHeaders', defaultValue: headerMapList });
return (
<SchemaComponent
schema={schema}
components={{
ArrayTable,
FormItem,
Input,
}}
/>
);
};

View File

@ -0,0 +1,26 @@
import React from 'react';
import { ViewRequestHeaders } from './RequestHeaders.view';
import { ViewResponseBody } from './ResponseBody.view';
import { ViewResponseHeaders } from './ResponseHeaders.view';
export const getItemsResponseTab = (params) => {
const { t } = params;
return [
{
key: 'body',
label: t('Body'),
children: <ViewResponseBody />,
},
{
key: 'responseHeaders',
label: t('Response headers'),
children: <ViewResponseHeaders />,
},
{
key: 'requestHeaders',
label: t('Request headers'),
children: <ViewRequestHeaders />,
},
];
};

View File

@ -0,0 +1,27 @@
import React from 'react';
import { Card, Tabs, Tag } from 'antd';
import { useTranslation } from '../../../../../locale';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
import { getItemsResponseTab } from './ResponseTab.item';
export const ResponseTab = () => {
const { t } = useTranslation();
const { rawResponse } = useContextResponseInfo();
const { status } = rawResponse || {};
const items = getItemsResponseTab({ t });
if (!rawResponse) {
return null;
}
const color = status.toString().startsWith('2') ? 'success' : 'error';
const TitleComp = <Tag color={color}>{`HTTP Code:${status}`}</Tag>;
return (
<Card title={TitleComp}>
<Tabs defaultActiveKey="body" items={items} />
</Card>
);
};

View File

@ -0,0 +1,12 @@
import React from 'react';
import { Tooltip } from 'antd';
import { useTranslation } from '../../../../../locale';
export const TooltipContainer = (props) => {
const { t } = useTranslation();
const title = t('Extract field metadata from the response data');
return <Tooltip title={title}>{props.children}</Tooltip>;
};

View File

@ -0,0 +1,105 @@
import { getRequestValues } from '../../../utils/getRequestValues';
export const getSchemaAction = (form, actionKey) => {
const reactionsFunc = (keyName, params) => {
const result = getRequestValues(form, actionKey);
const targetRes = result.find((item) => {
const itemArr = item?.split('.') || [];
return itemArr.pop() === keyName;
});
if (targetRes) {
params.description = null;
}
};
return {
page: {
type: 'number',
title: 'Page',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
description: "{{t('Page variable not used. Pagination based on total current request data.')}}",
'x-reactions': (params) => reactionsFunc('page', params),
},
pageSize: {
type: 'number',
title: 'PageSize',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
description: "{{t('PageSize variable not used. Pagination based on total current request data.')}}",
'x-reactions': (params) => reactionsFunc('pageSize', params),
},
filter: {
type: 'object',
title: 'Filter',
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
description: "{{t('Filter variable not used. Filtering will apply to the current request data.')}}",
'x-reactions': (params) => reactionsFunc('filter', params),
},
sort: {
type: 'string',
title: 'Sort',
'x-decorator': 'FormItem',
'x-component': 'Input',
description: "{{t('Sort variable not used. Sorting will apply to the current request data.')}}",
'x-reactions': (params) => reactionsFunc('sort', params),
},
appends: {
type: 'string',
title: 'Appends',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
fields: {
type: 'string',
title: 'Fields',
'x-decorator': 'FormItem',
'x-component': 'Input',
description: "{{t('Fields variable not used. Filtering will be based on the current request data.')}}",
'x-reactions': (params) => reactionsFunc('fields', params),
},
except: {
type: 'string',
title: 'Except',
'x-decorator': 'FormItem',
'x-component': 'Input',
description: "{{t('Except variable not used. Filtering will be based on the current request data.')}}",
'x-reactions': (params) => reactionsFunc('except', params),
},
filterByTk: {
type: 'string',
title: 'FilterByTk',
'x-decorator': 'FormItem',
'x-component': 'Input',
description: "{{t('The current parameters are not adapted')}}",
'x-reactions': (params) => reactionsFunc('filterByTk', params),
},
whiteList: {
type: 'string',
title: 'whiteList',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
blacklist: {
type: 'string',
title: 'blacklist',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
body: {
type: 'object',
title: 'Body',
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 5,
maxRows: 20,
},
},
},
};
};

View File

@ -0,0 +1,16 @@
import { paramsMap } from '../../../constants/mapListve';
import { getSchemaAction } from './getSchemaAction';
export function getSchemaParam(actionKey, form) {
const schemaChild = {};
const targetParams = paramsMap[actionKey] || [];
const schemaMap = getSchemaAction(form, actionKey);
for (let param of targetParams) {
schemaChild[param] = schemaMap[param];
}
return schemaChild;
}

View File

@ -0,0 +1,20 @@
import { useContext } from 'react';
import { ActionContext } from '@tachybase/client';
import { useForm } from '@tachybase/schema';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
export const useCancelAction = () => {
const form = useForm();
const { setVisible } = useContext(ActionContext);
const { setRawResponse, setDebugResponse } = useContextResponseInfo();
return {
run() {
setVisible(false);
form.reset();
form.setValuesIn('responseTab', null);
setRawResponse(null);
setDebugResponse(null);
},
};
};

View File

@ -0,0 +1,60 @@
import { useAPIClient } from '@tachybase/client';
import { useField, useForm } from '@tachybase/schema';
import lodash from 'lodash';
import { useParams } from 'react-router-dom';
import { useContextRequestInfo } from '../../../contexts/RequestForm.context';
import { useContextResponseInfo } from '../../../contexts/ResponseInfo.context';
export const useDebugAction = () => {
const field = useField();
const apiClient = useAPIClient();
const form = useForm();
const { form: formValue, actionKey, requestActionForm } = useContextRequestInfo();
const { name } = useParams();
const { setRawResponse, setDebugResponse, setResponseValidationErrorMessage }: any = useContextResponseInfo();
return {
async run() {
const actionValue = formValue?.values?.actions?.[actionKey];
try {
field.data = field?.data || {};
field.data.loading = true;
await requestActionForm.submit();
const repo = apiClient.resource('dataSources.httpCollections', name);
const debugVars = lodash.omit(form.values, 'responseTab');
const {
data: { data: responseData },
} = await repo.runAction({
values: {
debug: true,
inferFields: false,
actionOptions: {
...actionValue,
type: actionKey,
responseTransformer: actionValue?.responseTransformer,
},
debugVars,
},
});
field.data.loading = false;
const { rawResponse, debugBody, responseValidationErrorMessage } = responseData;
setRawResponse(rawResponse);
setDebugResponse(debugBody);
setResponseValidationErrorMessage(responseValidationErrorMessage);
} catch (error) {
field.data.loading = false;
console.log(error);
}
},
};
};

View File

@ -0,0 +1,46 @@
export const getSchemaFieldMethod = (params) => {
const { t, method, setFormValue } = params;
return {
name: 'method',
title: 'HTTP method',
default: method,
'x-decorator': 'FormItem',
'x-decorator-props': {
validator: {
required: true,
message: t('Method is required'),
},
feedbackLayout: 'popover',
},
required: true,
'x-component': 'Select',
'x-component-props': {
defaultValue: method,
onChange: (value) => {
setFormValue(value, 'method');
},
options: [
{
value: 'GET',
label: 'GET',
},
{
value: 'POST',
label: 'POST',
},
{
value: 'PUT',
label: 'PUT',
},
{
value: 'PATCH',
label: 'PATCH',
},
{
value: 'DELETE',
label: 'DELETE',
},
],
},
};
};

View File

@ -0,0 +1,17 @@
import React from 'react';
import { SchemaComponent } from '@tachybase/client';
import { useTranslation } from '../../../../locale';
import { getSchemaFieldMethod } from './FieldMethod.schema';
export const ViewFieldMethod = (props) => {
const { t } = useTranslation();
const { method, setFormValue } = props;
const schema = getSchemaFieldMethod({
t,
method,
setFormValue,
});
return <SchemaComponent schema={schema} />;
};

View File

@ -0,0 +1,67 @@
import React from 'react';
import { css, EllipsisWithTooltip, Input, Variable } from '@tachybase/client';
import { useVariableOptions } from '../../scopes/useVariableOptions';
export const getSchemaFieldPath = (params) => {
const { t, path, setFormValue, scCtx } = params;
return {
name: 'path',
title: 'URL',
required: true,
default: path,
'x-decorator': 'FormItem',
'x-decorator-props': {
feedbackLayout: 'popover',
addonBefore: (
<EllipsisWithTooltip ellipsis>
<Input.ReadPretty value={scCtx?.dataSourceData?.data?.options?.baseUrl} />
</EllipsisWithTooltip>
),
validator: {
required: true,
message: t('Path is required'),
},
className: css`
.ant-formily-item-addon-before {
border-width: 1px;
border-style: solid;
border-color: #d9d9d9;
padding: 0px 5px;
margin-right: 0px;
background: rgba(0, 0, 0, 0.02);
border-right: 0px;
margin-inline-end: 0px !important;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
.ant-description-input {
white-space: nowrap;
max-width: 230px;
overflow: hidden;
text-overflow: ellipsis;
}
}
`,
style: { width: '100%' },
},
'x-component': Variable.RawTextArea,
'x-component-props': {
defaultValue: path,
autoSize: true,
onChange: (val) => {
setFormValue(val.target.value, 'path');
},
scope: useVariableOptions,
fieldNames: {
value: 'name',
label: 'title',
},
style: {
borderTopLeftRadius: '0',
borderBottomLeftRadius: '0',
},
},
};
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import { SchemaComponent, useSchemaComponentContext } from '@tachybase/client';
import { useTranslation } from '../../../../locale';
import { getSchemaFieldPath } from './FieldPath.schema';
export const ViewFieldPath = (props) => {
const { t } = useTranslation();
const scCtx: any = useSchemaComponentContext();
const { path, setFormValue } = props;
const schema = getSchemaFieldPath({
t,
path,
setFormValue,
scCtx,
});
return <SchemaComponent schema={schema} />;
};

View File

@ -0,0 +1,47 @@
import React from 'react';
import { FormProvider, useCollectionRecord, useSchemaComponentContext } from '@tachybase/client';
import { createForm, SchemaOptionsContext } from '@tachybase/schema';
import lodash from 'lodash';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { ViewFieldMethod } from './FieldMethod.view';
import { ViewFieldPath } from './FieldPath.view';
export const MethodPathComponent = (props) => {
const { data }: any = useCollectionRecord();
const { form, actionKey, requestActionForm } = useContextRequestInfo();
const scCtx: any = useSchemaComponentContext();
const { path, method } = data?.actions?.[actionKey] || {};
const setFormValue = lodash.debounce(async (value, label) => {
const { actions } = form?.values || {};
form.setValuesIn('actions', {
...actions,
[actionKey]: {
...actions?.[actionKey],
[label]: value,
},
});
requestActionForm.setValuesIn(label, value);
}, 100);
const initialForm = React.useMemo(
() =>
createForm({
initialValues: requestActionForm.values,
}),
[],
);
return (
<FormProvider form={props?.actionForm ? initialForm : requestActionForm}>
<ViewFieldMethod method={method} setFormValue={setFormValue} />
<SchemaOptionsContext.Provider value={scCtx}>
<ViewFieldPath path={path} setFormValue={setFormValue} />
</SchemaOptionsContext.Provider>
</FormProvider>
);
};

View File

@ -0,0 +1,172 @@
import { css } from '@tachybase/client';
import { NAMESPACE } from '../../../../locale';
import { debounceClick } from './debounceClick.util';
export const getSchemaRequestBody = ({ defaultValue, actionKey, parentForm, field }) => ({
type: 'object',
properties: {
contentType: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
placeholder: `{{t("Content-Type",{ ns: "${NAMESPACE}" })}}`,
default: defaultValue?.contentType,
'x-component-props': {
onChange: (headerValue) => {
debounceClick(parentForm, actionKey, 'contentType', headerValue);
},
},
enum: [
{
value: 'application/x-www-form-urlencoded',
label: 'application/x-www-form-urlencoded ',
},
{
value: 'application/json',
label: 'application/json',
},
],
'x-reactions': (field) => {
const header = parentForm.values.actions[actionKey];
const contentType = header?.contentType;
if (field?.value !== contentType) {
field.setValue(contentType ?? 'application/json');
}
if (!field.value) {
debounceClick(parentForm, actionKey, 'contentType', 'application/json');
}
},
},
body: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
default: defaultValue.body,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
display: 'flex',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.RawTextArea',
'x-component-props': {
scope: '{{useVariableOptions}}',
autoSize: true,
fieldNames: {
value: 'name',
label: 'title',
},
style: {
minWidth: '220px',
},
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
'x-component-props': {
onClick: () => {
debounceClick(parentForm, actionKey, 'body', field.form.values.body);
},
},
},
},
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add", { ns: "${NAMESPACE}" })}}`,
'x-component': 'ArrayItems.Addition',
},
},
'x-reactions': [
{
dependencies: ['.contentType'],
fulfill: {
state: {
visible: '{{ $deps[0]==="application/x-www-form-urlencoded"}}',
},
},
},
],
},
jsonBody: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.JSON',
default: defaultValue.jsonBody,
'x-component-props': {
scope: '{{useVariableOptions}}',
autoSize: true,
fieldNames: {
value: 'name',
label: 'title',
},
placeholder: '{{t("Value")}}',
style: {
minHeight: 200,
},
onChange: (value) => {
const { actions } = parentForm.values || {};
parentForm.setValuesIn('actions', {
...actions,
[actionKey]: {
...actions?.[actionKey],
body: value,
},
});
},
},
'x-reactions': [
{
dependencies: ['.contentType'],
fulfill: {
state: {
visible: '{{ $deps[0]==="application/json"}}',
},
},
},
(filed) => {
const bodyValue = parentForm?.values?.actions?.[actionKey]?.body;
if (filed?.value !== bodyValue) {
filed.setValue(parentForm.values.actions[actionKey].body);
}
},
],
},
},
});

View File

@ -0,0 +1,40 @@
import React from 'react';
import { FormItem, FormProvider, SchemaComponent } from '@tachybase/client';
import { ArrayItems } from '@tachybase/components';
import { useField } from '@tachybase/schema';
import { Input } from 'antd';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { getSchemaRequestBody } from './RequestBody.schema';
export const ViewRequestBody = () => {
const field = useField();
const { form, actionKey } = useContextRequestInfo() as any;
const valueList = form?.values?.actions?.[actionKey];
const schema = getSchemaRequestBody({
parentForm: form,
actionKey: actionKey,
defaultValue: {
contentType: valueList?.contentType,
body: valueList?.body,
jsonBody: valueList?.body,
},
field: field,
});
return (
<SchemaComponent
schema={schema}
components={{
ArrayItems,
FormItem,
Input,
FormProvider,
}}
scope={{ useVariableOptions }}
/>
);
};

View File

@ -0,0 +1,98 @@
import { css } from '@tachybase/client';
import { debounceClick } from './debounceClick.util';
export const getSchemaRequestHeaders = ({ title, defaultValue, field, parentForm, actionKey }) => {
const handleReactions = (field) => {
const headerList = parentForm?.values?.actions?.[actionKey]?.headers;
if (headerList?.length > 0 && headerList !== defaultValue) {
field.setValue(headerList);
}
};
const handleClickFunc = () => {
debounceClick(parentForm, actionKey, 'headers', field.form.values.headers);
};
return {
type: 'object',
'x-decorator': 'Form',
properties: {
headers: {
type: 'array',
'x-component': 'ArrayItems',
default: defaultValue,
'x-decorator': 'FormItem',
'x-reactions': handleReactions,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
display: 'flex',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.RawTextArea',
'x-component-props': {
scope: '{{useVariableOptions}}',
autoSize: true,
fieldNames: {
value: 'name',
label: 'title',
},
style: {
minWidth: '220px',
},
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
'x-component-props': {
onClick: handleClickFunc,
},
},
},
},
},
},
properties: {
add: {
title: title,
type: 'void',
'x-component': 'ArrayItems.Addition',
},
},
},
},
};
};

View File

@ -0,0 +1,38 @@
import React from 'react';
import { FormItem, SchemaComponent } from '@tachybase/client';
import { ArrayItems } from '@tachybase/components';
import { useField } from '@tachybase/schema';
import { Input } from 'antd';
import { useTranslation } from '../../../../locale';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { getSchemaRequestHeaders } from './RequestHeaders.schema';
export const ViewRequestHeaders = () => {
const { t } = useTranslation();
const field = useField();
const { form, actionKey } = useContextRequestInfo();
const valueList = form?.values?.actions?.[actionKey];
const schema = getSchemaRequestHeaders({
title: t('Add header'),
defaultValue: valueList?.headers || [],
field,
parentForm: form,
actionKey,
});
return (
<SchemaComponent
schema={schema}
components={{
ArrayItems,
FormItem,
Input,
}}
scope={{ useVariableOptions }}
/>
);
};

View File

@ -0,0 +1,95 @@
import { css } from '@tachybase/client';
import { debounceClick } from './debounceClick.util';
export const getSchemaRequestParams = ({ title, defaultValue, field, parentForm, actionKey }) => ({
type: 'object',
'x-decorator': 'Form',
properties: {
params: {
type: 'array',
'x-component': 'ArrayItems',
default: defaultValue,
'x-decorator': 'FormItem',
'x-reactions': (field) => {
const params = parentForm.values?.actions?.[actionKey]?.params;
if (params?.length && params !== defaultValue) {
field.setValue(params);
}
},
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
display: 'flex',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
& > .ant-space-item:first-child,
& > .ant-space-item:nth-of-type(2) {
flex: 1;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{t("Name")}}',
style: {
width: '100%',
},
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.RawTextArea',
'x-component-props': {
scope: '{{useVariableOptions}}',
autoSize: true,
fieldNames: {
value: 'name',
label: 'title',
},
style: {
minWidth: '230px',
width: '100%',
},
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
'x-component-props': {
onClick: () => {
debounceClick(parentForm, actionKey, 'params', field.form.values?.params);
},
},
},
},
},
},
},
properties: {
add: {
type: 'void',
title: title,
'x-component': 'ArrayItems.Addition',
},
},
},
},
});

View File

@ -0,0 +1,38 @@
import React from 'react';
import { FormItem, SchemaComponent } from '@tachybase/client';
import { ArrayItems } from '@tachybase/components';
import { useField } from '@tachybase/schema';
import { Input } from 'antd';
import { useTranslation } from '../../../../locale';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { getSchemaRequestParams } from './RequestParams.schema';
export const ViewRequestParams = () => {
const { t } = useTranslation();
const field = useField();
const { form, actionKey } = useContextRequestInfo() as any;
const valueList = form?.values?.actions?.[actionKey];
const schema = getSchemaRequestParams({
title: t('Add parameter'),
defaultValue: valueList?.params,
field,
parentForm: form,
actionKey,
});
return (
<SchemaComponent
schema={schema}
components={{
ArrayItems,
FormItem,
Input,
}}
scope={{ useVariableOptions }}
/>
);
};

View File

@ -0,0 +1,27 @@
import React from 'react';
import { ViewRequestBody } from './RequestBody.view';
import { ViewRequestHeaders } from './RequestHeaders.view';
import { ViewRequestParams } from './RequestParams.view';
export const getItemsRequestTab = (params: any) => {
const { t } = params;
return [
{
key: 'parameters',
label: t('Parameters'),
children: <ViewRequestParams />,
},
{
key: 'body',
label: t('Body'),
children: <ViewRequestBody />,
},
{
key: 'headers',
label: t('Headers'),
children: <ViewRequestHeaders />,
},
];
};

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Tabs } from 'antd';
import { useTranslation } from '../../../../locale';
import { getItemsRequestTab } from './RequestTab.items';
export const RequestTab = () => {
const { t } = useTranslation();
const items = getItemsRequestTab({
t,
});
return <Tabs items={items} />;
};

View File

@ -0,0 +1,13 @@
import { lodash } from '@tachybase/utils/client';
export const debounceClick = lodash.debounce((parentForm, actionKey, keyName, headerValue) => {
const { actions } = parentForm.values || {};
parentForm.setValuesIn('actions', {
...actions,
[actionKey]: {
...actions?.[actionKey],
[keyName]: headerValue,
},
});
}, 400);

View File

@ -0,0 +1,45 @@
import React, { useEffect } from 'react';
import { useCollectionRecord, Variable } from '@tachybase/client';
import { useContextRequestInfo } from '../../contexts/RequestForm.context';
import { useVariableOptions } from '../../scopes/useVariableOptions';
import { setFormValue } from '../../utils/setFormValue';
import { responseTransformerAe } from '../../utils/responseTransformerAe';
export const ResponseTransformerComponent = () => {
const { form, actionKey, responseTransformer, setResponseTransformer } = useContextRequestInfo();
const transformer = form?.values?.actions?.[actionKey]?.responseTransformer;
const transformerValue = responseTransformer ?? transformer;
const { data } = useCollectionRecord() as any;
useEffect(() => {
if (!data?.actions?.[actionKey]?.responseTransformer && !transformer) {
const { actions } = form.values || {};
form.setValuesIn('actions', {
...actions,
[actionKey]: {
...actions?.[actionKey],
responseTransformer: responseTransformerAe,
},
});
}
}, []);
return (
<Variable.JSON
value={transformerValue}
onChange={(value) => {
setResponseTransformer(value);
setFormValue(form, actionKey, value);
}}
autoSize={{
minRows: 5,
}}
fieldNames={{
value: 'name',
label: 'title',
}}
scope={() => useVariableOptions(true)}
/>
);
};

View File

@ -0,0 +1,79 @@
import React, { useContext } from 'react';
import { SchemaComponentContext, useCompile } from '@tachybase/client';
import { useTranslation } from '../../../locale';
import { paramsMap } from '../constants/mapListve';
import { requestHeaderList } from '../constants/requestHeaderList';
import { useContextRequestInfo } from '../contexts/RequestForm.context';
export const useVariableOptions = (showResponse) => {
const compile = useCompile();
const { t } = useTranslation();
const ctx: any = useContext(SchemaComponentContext);
const { actionKey } = useContextRequestInfo();
const { variables } = ctx.dataSourceData?.data?.options || {};
const options = React.useMemo(() => getOptions({ t, variables, compile, actionKey, showResponse }), [actionKey]);
return options;
};
const getOptions = (params) => {
const { t, variables, compile, actionKey, showResponse } = params;
const options = [];
if (variables) {
options.push({
name: 'dataSourceVariables',
title: t('Custom variables'),
children: variables?.map((val) => ({
name: compile(val.name),
title: val.name,
})),
});
}
options.push({
name: 'request',
title: t('TachyBase request'),
children: [
{
name: 'params',
title: 'params',
children: (paramsMap[actionKey] || []).map((value) => ({
name: value,
title: value,
})),
},
{
name: 'header',
title: 'headers',
children: requestHeaderList,
},
{
name: 'body',
title: 'Body',
},
{
name: 'token',
title: 'Token',
},
],
});
if (showResponse) {
options.push({
name: 'rawResponse',
title: t('Third party response'),
children: [
{
name: 'body',
title: 'body',
},
],
});
}
return options;
};

View File

@ -0,0 +1,21 @@
// 这个函数的目的是筛选出参数对象中的属性,这些属性必须满足两个条件:有一个 method 属性和一个 path 属性。然后,它将这些属性的值作为新对象的键值对
export function filterObjectWithMethodAndPath(obj: Record<string, any>): Record<string, any> {
const keys = Object.keys(obj);
const filteredKeys = keys.filter((key) => {
const value = obj[key];
return value.method && value.path;
});
const resultObj = filteredKeys.reduce(
(filteredObj, key) => ({
...filteredObj,
[key]: obj[key],
}),
{},
);
return resultObj;
}

View File

@ -0,0 +1,17 @@
// 过滤一个树状结构的数据,只保留那些子项的 availableTypes 包含 type 的项
export const filterTree = (items, type) => {
const filteredItems = items
.filter((item) => {
const matchingChildren = item.children.filter(
(child) => child.availableTypes && child.availableTypes.includes(type),
);
return matchingChildren.length > 0;
})
.map((item) => ({
label: item.label,
key: item.key,
children: item.children.filter((child) => child.availableTypes && child.availableTypes.includes(type)),
}));
return filteredItems;
};

View File

@ -0,0 +1,3 @@
export function getRequestActions(sourceMethod) {
return ['list', 'get'].filter((method) => !sourceMethod.includes(method));
}

View File

@ -0,0 +1,23 @@
export function getRequestValues(form, actionKey) {
const actionValue = form?.values?.actions?.[actionKey];
let result = [];
const { params = [], headers = [], body = [] } = actionValue || {};
result = [...params, ...headers].map((key) => {
return key?.value?.replace(/{{|}}/g, '');
});
if (typeof body == 'string') {
result.concat(body?.replace(/{{|}}/g, ''));
} else {
result.concat(
body?.map((item) => {
return item?.value.replace(/{{|}}/g, '');
}),
);
}
return result.filter(Boolean);
}

View File

@ -0,0 +1,4 @@
export const responseTransformerAe = {
data: '{{rawResponse.body}}',
meta: {},
};

View File

@ -0,0 +1,10 @@
export const setFormValue = async (form, action, value) => {
const { actions } = form.values || {};
form.setValuesIn('actions', {
...actions,
[action]: {
...actions?.[action],
responseTransformer: value,
},
});
};

View File

@ -1,10 +1,10 @@
import React from 'react';
import { SchemaComponent } from '@tachybase/client';
import { tval, usePluginTranslation } from '../locale';
import { tval, useTranslation } from '../locale';
export const MysqlDataSourceSettingsForm = () => {
const { t } = usePluginTranslation();
const { t } = useTranslation();
return (
<SchemaComponent
scope={{ t }}

View File

@ -3,10 +3,10 @@ import { SchemaComponent } from '@tachybase/client';
import { Space } from 'antd';
import { tval, usePluginTranslation } from '../locale';
import { tval, useTranslation } from '../locale';
export const PgDataSourceSettingsForm = () => {
const { t } = usePluginTranslation();
const { t } = useTranslation();
return (
<SchemaComponent
scope={{ t }}

View File

@ -1,19 +1,28 @@
import { Plugin } from '@tachybase/client';
import { PluginDataSourceManagerClient } from '@tachybase/plugin-data-source-manager/client';
import { KitHttpDatasource } from './features/rest-api/kit';
import { MysqlDataSourceSettingsForm } from './forms/msql';
import { PgDataSourceSettingsForm } from './forms/pg';
import { tval } from './locale';
export class PluginExternalDataSourceClient extends Plugin {
async afterAdd() {
// 注册 REST API 数据源
this.app.pm.add(KitHttpDatasource);
}
async load() {
this.app.pm
.get(PluginDataSourceManagerClient)
.registerType('postgres', { DataSourceSettingsForm: PgDataSourceSettingsForm, label: tval('PostgreSQL') });
// 注册 PostgreSQL 数据源
this.app.pm.get(PluginDataSourceManagerClient).registerType('postgres', {
DataSourceSettingsForm: PgDataSourceSettingsForm,
label: tval('PostgreSQL'),
});
this.app.pm
.get(PluginDataSourceManagerClient)
.registerType('mysql', { DataSourceSettingsForm: MysqlDataSourceSettingsForm, label: tval('MySQL') });
// 注册 MySQL 数据源
this.app.pm.get(PluginDataSourceManagerClient).registerType('mysql', {
DataSourceSettingsForm: MysqlDataSourceSettingsForm,
label: tval('MySQL'),
});
}
}

View File

@ -1,15 +1,20 @@
import { i18n } from '@tachybase/client';
import { useTranslation } from 'react-i18next';
import { i18n, tval as nTval, useApp } from '@tachybase/client';
export const NAMESPACE = '@hera/plugin-external-data-source';
// NOTE: 保持翻译统一经由这里处理, 所有本插件内的翻译方法从这里统一导出
export const tval = (key: string, useCore = false) => nTval(key, { ns: useCore ? undefined : NAMESPACE });
export function lang(key: string, options = {}) {
return i18n.t(key, { ...options, ns: NAMESPACE });
}
export function tval(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
}
export function usePluginTranslation() {
return useTranslation(NAMESPACE);
}
export const useTranslation = (useCore = false): any => {
const { i18n } = useApp();
const t = (key: string, props = {}) =>
i18n.t(key, {
ns: useCore ? undefined : NAMESPACE,
...props,
});
return { t };
};

View File

@ -1,12 +1,16 @@
{
"Data source name": "Data source name",
"Data source display name": "Data source display name",
"Get":"Get(required)",
"Host": "Host",
"If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.": "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.",
"List":"List(required)",
"Port": "Port",
"Database": "Database",
"Database connections": "Database connections",
"Test Connection": "Test Connection",
"Connection successful'": "Connection successful'",
"Create collection": "Create collection",
"Display name": "Display name",
"Username": "Username",
"Password": "Password",
@ -22,5 +26,7 @@
"Postgres": "Postgres",
"Enabled the data source": "Enabled the data source",
"Table prefix": "Table prefix",
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter."
}
"Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.": "Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.",
"Record unique key": "Record unique key",
"Request actions": "Request actions"
}

View File

@ -1,12 +1,16 @@
{
"Data source name": "数据源标识",
"Data source display name": "数据源名称",
"Get":"Get(必填)",
"Host": "服务器地址",
"If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.": "当数据表没有主键时,你需要配置记录唯一标识符,用于在区块中定位行记录,不配置将无法创建该表的数据区块。",
"List":"List(必填)",
"Port": "端口",
"Database": "数据库",
"Database connections": "连接第三方数据库",
"Test Connection": "测试连接",
"Connection successful'": "连接成功",
"Create collection": "创建数据表",
"Display name": "名称",
"Username": "用户名",
"Password": "密码",
@ -22,5 +26,7 @@
"Postgres":"PostgreSQL",
"Enabled the data source":"启用数据源",
"Table prefix":"表前缀",
"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.": "随机生成,可修改。支持英文、数字和下划线,必须以英文字母开头。",
"Record unique key": "记录唯一标识符",
"Request actions": "请求操作"
}

View File

@ -0,0 +1,39 @@
import { Plugin } from '@tachybase/server';
import _ from 'lodash';
import { HttpCollection } from './services/http-collection';
import { HttpDataSource } from './services/http-data-source';
export class PluginHttpDatasource extends Plugin {
async afterAdd() {}
async beforeLoad() {
this.app.dataSourceManager.factory.register('http', HttpDataSource);
this.app.resourcer.define({
name: 'dataSources.httpCollections',
actions: {
async runAction(ctx, next) {
const { sourceId } = ctx.action;
const { actionOptions, inferFields, debugVars, debug } = ctx.action.params.values;
if (!actionOptions.type) {
_.set(actionOptions, 'type', 'list');
}
const dataSource = ctx.app.dataSourceManager.dataSources.get(sourceId);
ctx.body = await HttpCollection.runAction({
dataSource,
actionOptions,
parseField: inferFields,
runAsDebug: debug,
debugVars,
});
await next();
},
},
});
}
async load() {}
async install() {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}

View File

@ -0,0 +1,90 @@
import _ from 'lodash';
import { HttpCollection } from './http-collection';
export class HttpApiRepository {
constructor(public collection: HttpCollection) {}
async count(options) {
const { transformedResponse } = await this.collection.runAction('list');
return _.get(transformedResponse, 'meta.count', transformedResponse.data.length);
}
async find(options) {
const { transformedResponse } = await this.collection.runAction('list');
return transformedResponse.data;
}
async findAndCount(options = {}) {
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
filter: options.filter,
sort: options.sort,
});
const { transformedResponse } = await this.collection.runAction('list', templateContext);
return [transformedResponse.data, _.get(transformedResponse, 'meta.count', transformedResponse.length)];
}
async findOne(options) {
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
filter: options.filter,
sort: options.sort,
filterByTk: options.filterByTk,
});
const { transformedResponse } = await this.collection.runAction('get', templateContext);
return transformedResponse.data;
}
async create(options) {
options.values = this.handleValuesWithWhiteListAndBlackList(options);
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
values: options.values,
});
const { transformedResponse } = await this.collection.runAction('create', templateContext);
return transformedResponse.data;
}
async update(options) {
options.values = this.handleValuesWithWhiteListAndBlackList(options);
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
filter: options.filter,
sort: options.sort,
filterByTk: options.filterByTk,
values: options.values,
});
const { transformedResponse } = await this.collection.runAction('update', templateContext);
return transformedResponse.data;
}
async destroy(options) {
const templateContext = this.buildTemplateContextFromRequestContext(options.context, {
filter: options.filter,
sort: options.sort,
filterByTk: options.filterByTk,
});
const { transformedResponse } = await this.collection.runAction('destroy', templateContext);
return transformedResponse.data;
}
buildTemplateContextFromRequestContext(ctx, others = {}) {
const templateContext = { ...others };
if (_.get(ctx, 'action.params')) {
_.set(templateContext, 'request.params', ctx.action.params);
if (ctx.action.params.page) {
_.set(templateContext, 'page', ctx.action.params.page);
}
if (ctx.action.params.pageSize) {
_.set(templateContext, 'pageSize', ctx.action.params.pageSize);
}
}
if (_.get(ctx, 'request.headers')) {
_.set(templateContext, 'request.headers', ctx.request.headers);
}
return templateContext;
}
handleValuesWithWhiteListAndBlackList(options) {
const { whiteList, blackList } = options;
let { values } = options;
if (!values) {
return values;
}
if (whiteList && whiteList.length) {
values = _.pick(values, whiteList);
}
if (blackList && blackList.length) {
values = _.omit(values, blackList);
}
return values;
}
}

View File

@ -0,0 +1,12 @@
import { CollectionManager, CollectionOptions } from '@tachybase/data-source-manager';
import { HttpCollection } from './http-collection';
export class HttpCollectionManager extends CollectionManager {
constructor(options = {}) {
super(options);
}
newCollection(options: CollectionOptions) {
return new HttpCollection(options, this);
}
}

View File

@ -0,0 +1,236 @@
import { Collection } from '@tachybase/data-source-manager';
import { parse } from '@tachybase/utils';
import axios from 'axios';
import _ from 'lodash';
import { transformResponseThroughMiddleware } from './transform-response';
import { typeInterfaceMap } from './type-interface-map';
import { normalizeRequestOptions, normalizeRequestOptionsKey } from './utils';
function compileTemplate(template, context) {
return parse(template)(context);
}
function mergeRequestOptions(options) {
const { baseRequestConfig, actionOptions, templateContext = {} } = options;
const rawConfig = {
method: actionOptions.method,
url: actionOptions.path,
baseURL: baseRequestConfig.baseUrl,
headers: {
...baseRequestConfig.headers,
...actionOptions.headers,
},
params: actionOptions.params,
};
if (actionOptions.body) {
rawConfig.data = actionOptions.body;
}
if (actionOptions.contentType) {
rawConfig.headers['Content-Type'] = actionOptions.contentType;
}
if (actionOptions.contentType === 'application/x-www-form-urlencoded') {
rawConfig.data = normalizeRequestOptionsKey(rawConfig.data);
}
const config = compileTemplate(rawConfig, templateContext);
if (templateContext.values && !rawConfig.data) {
config.data = templateContext.values;
}
if (actionOptions.contentType === 'application/x-www-form-urlencoded') {
config.data = new URLSearchParams(config.data).toString();
}
return config;
}
function buildTemplateContext(options) {
const { dataSourceRequestConfig, templateContext, debugVars = {} } = options;
const variables = {
dataSourceVariables: dataSourceRequestConfig.variables || {},
};
for (const variableKey of Object.keys(variables)) {
templateContext[variableKey] = variables[variableKey];
}
for (const variableKey of Object.keys(debugVars)) {
if (variableKey == 'body') {
_.set(templateContext, ['request', 'body'], debugVars[variableKey]);
} else {
_.set(templateContext, ['request', 'params', variableKey], debugVars[variableKey]);
}
}
if (templateContext.values && !_.get(templateContext, 'request.body')) {
templateContext.body = templateContext.values;
_.set(templateContext, 'request.body', templateContext.values);
}
}
function rawTypeToFieldType(rawType, exampleValue) {
const typeInfers = {
string: 'string',
number: () => {
if (Number.isInteger(exampleValue)) {
return 'integer';
}
return 'float';
},
boolean: 'boolean',
object: () => {
if (_.isNull(exampleValue)) {
return ['string', 'integer', 'float', 'boolean', 'json'];
}
return 'json';
},
};
const inferType = typeInfers[rawType];
if (typeof inferType === 'function') {
return inferType();
}
return inferType;
}
function getInterfaceOptionsByType(type) {
const interfaceConfig = typeInterfaceMap[type];
if (typeof interfaceConfig === 'function') {
return interfaceConfig();
} else {
return interfaceConfig;
}
}
function parseResponseToFieldsOptions(responseData) {
let objectItem = {};
if (Array.isArray(responseData)) {
objectItem = responseData[0];
}
if (_.isPlainObject(responseData)) {
objectItem = responseData;
}
return Object.keys(objectItem).map((key) => {
const rawType = typeof objectItem[key];
const inferredFieldType = rawTypeToFieldType(rawType, objectItem[key]);
let fieldOptions = {
name: key,
rawType,
title: key,
field: key,
};
let fieldType = inferredFieldType;
if (Array.isArray(inferredFieldType)) {
fieldType = inferredFieldType[0];
fieldOptions['possibleTypes'] = inferredFieldType;
}
fieldOptions = {
...fieldOptions,
type: fieldType,
...getInterfaceOptionsByType(fieldType),
};
_.set(fieldOptions, 'uiSchema.title', key);
return fieldOptions;
});
}
function guessFilterTargetKeyName(fields) {
const keyNames = ['id', 'nodeId', 'node_id'];
for (const keyName of keyNames) {
if (fields.find((field) => field.field === keyName)) {
return keyName;
}
}
return void 0;
}
export class HttpCollection extends Collection {
availableActions() {
const allActionOptions = this.options.actions || {};
const actions = ['list', 'get', 'create', 'update', 'destroy'];
return actions.filter((action) => allActionOptions[action]);
}
// send http request to external server
static async runAction(options) {
const { dataSource, actionOptions, templateContext = {}, parseField, debugVars, runAsDebug } = options;
normalizeRequestOptions(actionOptions);
const dataSourceRequestConfig = dataSource.requestConfig();
buildTemplateContext({
dataSourceRequestConfig,
templateContext,
debugVars,
});
const requestConfig = mergeRequestOptions({
baseRequestConfig: dataSourceRequestConfig,
actionOptions,
templateContext,
});
if (runAsDebug) {
requestConfig.validateStatus = () => true;
}
const handleResponse = (response2) => {
if (parseField && response2.transformedResponse.data) {
response2.fields = parseResponseToFieldsOptions(response2.transformedResponse.data);
const filterTargetKeyName = guessFilterTargetKeyName(response2.fields);
if (filterTargetKeyName) {
response2.filterTargetKey = filterTargetKeyName;
}
}
if (options.runAsDebug && !response2.responseValidationErrorMessage) {
response2.debugBody = response2.transformedResponse;
}
return response2;
};
const response = await axios.request(requestConfig).catch((error) => {
throw new Error(`Failed to request external http datasource`, { cause: error });
});
const clientRequest = response.request;
const baseResponse = {
rawResponse: {
status: response.status,
headers: response.headers,
data: response.data,
body: response.data,
request: {
url: axios.getUri(requestConfig),
method: clientRequest.method,
headers: response.request.getHeaders(),
},
},
data: null,
};
const transformResponse = (baseResponse2, actionOptions2) => {
const transformer = actionOptions2.responseTransformer;
baseResponse2.transformedResponse = compileTemplate(transformer, {
rawResponse: baseResponse2.rawResponse,
});
try {
transformResponseThroughMiddleware({
transformedResponse: baseResponse2.transformedResponse,
actionOptions: actionOptions2,
templateContext,
});
} catch (e) {
if (options.runAsDebug) {
baseResponse2.responseValidationErrorMessage = e.message;
} else {
throw e;
}
}
};
transformResponse(baseResponse, actionOptions);
dataSource.logger.debug({
actionOptions,
requestConfig,
baseResponse,
});
return handleResponse(baseResponse);
}
getActionOptions(action) {
const allActionOptions = this.options.actions || {};
const actionOption = allActionOptions[action];
if (!actionOption) {
throw new Error(`request config of action "${action}" is not set`);
}
return actionOption;
}
runAction(action: string, templateContext?: any) {
const actionOptions = this.getActionOptions(action);
return HttpCollection.runAction({
actionOptions: {
...actionOptions,
type: action,
},
dataSource: this.collectionManager.dataSource,
templateContext,
});
}
}

View File

@ -0,0 +1,34 @@
import { DataSource } from '@tachybase/data-source-manager';
import _ from 'lodash';
import { HttpApiRepository } from './http-api-repository';
import { HttpCollectionManager } from './http-collection-manager';
import { normalizeRequestOptions } from './utils';
export class HttpDataSource extends DataSource {
async load(options = {}) {
const { localData } = options;
for (const collectionName of Object.keys(localData)) {
this.collectionManager.defineCollection(localData[collectionName]);
}
}
createCollectionManager(options) {
const collectionManager = new HttpCollectionManager({
dataSource: this,
});
collectionManager.registerRepositories({
Repository: HttpApiRepository,
});
return collectionManager;
}
requestConfig() {
const configKeys = ['baseUrl', 'headers', 'variables', 'timeout', 'responseType'];
const config = _.pick(this.options, configKeys);
normalizeRequestOptions(config);
return config;
}
publicOptions() {
return _.pick(this.options, ['baseUrl', 'variables']);
}
}

View File

@ -0,0 +1,3 @@
export class HttpRequestBuilder {
constructor(dataSource) {}
}

View File

@ -0,0 +1,151 @@
import { filterMatch } from '@tachybase/database';
import _ from 'lodash';
const middlewares = {
list: [
function validateListActionTransformedResponse(options) {
const { transformedResponse } = options;
const dataItem = _.get(transformedResponse, 'data');
if (!Array.isArray(dataItem)) {
throw new Error(
`The transformed response for the list action should be an array. got ${getValueType(dataItem)}`,
);
}
if (!dataItem.every((item) => _.isPlainObject(item))) {
throw new Error('Every item in the transformed response should be an object');
}
},
function handleFilter(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.filter'])) {
return;
}
const filter = _.get(options.templateContext, 'request.params.filter') || {};
const { data } = options.transformedResponse;
options.transformedResponse.data = data.filter((item) => {
return filterMatch(item, filter);
});
},
function handleSort(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.sort'])) {
return;
}
const sort = _.get(options.templateContext, 'request.params.sort') || [];
if (sort.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = data.sort((a, b) => {
for (const sortItem of sort) {
const isDesc = sortItem.startsWith('-');
const field = isDesc ? sortItem.slice(1) : sortItem;
if (a[field] > b[field]) {
return isDesc ? -1 : 1;
}
if (a[field] < b[field]) {
return isDesc ? 1 : -1;
}
}
return 0;
});
},
function handleFullList(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.page'])) {
return;
}
const page = parseInt(_.get(options.templateContext, 'request.params.page', 1));
const pageSize = parseInt(_.get(options.templateContext, 'request.params.pageSize', 20));
const { data } = options.transformedResponse;
const start = (page - 1) * pageSize;
const end = start + pageSize;
options.transformedResponse.data = data.slice(start, end);
_.set(options.transformedResponse, 'meta.count', data.length);
_.set(options.transformedResponse, 'meta.page', page);
_.set(options.transformedResponse, 'meta.pageSize', pageSize);
_.set(options.transformedResponse, 'meta.totalPage', Math.ceil(data.length / pageSize));
},
function handleFields(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.fields'])) {
return;
}
const fields = _.get(options.templateContext, 'request.params.fields') || [];
if (fields.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = data.map((item) => {
return _.pick(item, fields);
});
},
function handleExpect(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.expect'])) {
return;
}
const expect = _.get(options.templateContext, 'request.params.expect') || [];
if (expect.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = data.map((item) => {
return _.omit(item, expect);
});
},
],
get: [
function validateGetActionTransformedResponse(options) {
const { transformedResponse } = options;
const dataItem = _.get(transformedResponse, 'data');
if (!_.isPlainObject(dataItem)) {
throw new Error(
`The transformed response for the get action should be an object. got ${getValueType(dataItem)}`,
);
}
},
function handleFields2(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.fields'])) {
return;
}
const fields = _.get(options.templateContext, 'request.params.fields') || [];
if (fields.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = _.pick(data, fields);
},
function handleExpect2(options) {
if (checkVariablesAreUsed(options.actionOptions, ['request.params.expect'])) {
return;
}
const expect = _.get(options.templateContext, 'request.params.expect') || [];
if (expect.length === 0) {
return;
}
const { data } = options.transformedResponse;
options.transformedResponse.data = _.omit(data, expect);
},
],
create: [],
update: [],
destroy: [],
};
export function transformResponseThroughMiddleware(options) {
const { actionOptions } = options;
const { type } = actionOptions;
const actionMiddlewares = middlewares[type] || [];
actionMiddlewares.forEach((middleware) => {
middleware(options);
});
}
function checkVariablesAreUsed(options, variables) {
const template = JSON.stringify(options);
return variables.every((variable) => template.includes(`{{${variable}}}`));
}
function getValueType(value) {
if (Array.isArray(value)) {
return 'array';
}
if (_.isPlainObject(value)) {
return 'object';
}
return typeof value;
}

View File

@ -0,0 +1,220 @@
export const typeInterfaceMap = {
array: '',
belongsTo: '',
belongsToMany: '',
boolean: () => {
return {
interface: 'checkbox',
uiSchema: {
type: 'boolean',
'x-component': 'Checkbox',
},
};
},
context: '',
date: () => {
return {
interface: 'datetime',
uiSchema: {
'x-component': 'DatePicker',
'x-component-props': {
dateFormat: 'YYYY-MM-DD',
showTime: false,
},
},
};
},
hasMany: '',
hasOne: '',
json: () => {
return {
interface: 'json',
uiSchema: {
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 5,
// maxRows: 20,
},
},
default: null,
},
};
},
jsonb: () => {
return {
interface: 'json',
uiSchema: {
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 5,
// maxRows: 20,
},
},
default: null,
},
};
},
integer: () => ({
interface: 'integer',
// name,
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
'x-validator': 'integer',
},
}),
bigInt: (columnInfo) => {
return {
interface: 'integer',
uiSchema: {
'x-component': 'InputNumber',
'x-component-props': {
style: {
width: '100%',
},
},
},
};
},
float: () => {
return {
interface: 'number',
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
},
};
},
double: () => {
return {
interface: 'number',
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
},
};
},
real: () => {
return {
interface: 'number',
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
},
};
},
decimal: () => {
return {
interface: 'number',
uiSchema: {
type: 'number',
// title,
'x-component': 'InputNumber',
'x-component-props': {
stringMode: true,
step: '1',
},
},
};
},
password: () => ({
interface: 'password',
hidden: true,
// name,
uiSchema: {
type: 'string',
// title,
'x-component': 'Password',
},
}),
radio: '',
set: '',
sort: '',
string: () => {
return {
interface: 'input',
uiSchema: {
'x-component': 'Input',
'x-component-props': {
style: {
width: '100%',
},
},
},
};
},
text: () => {
return {
interface: 'textarea',
// name,
uiSchema: {
type: 'string',
'x-component': 'Input.TextArea',
},
};
},
time: () => ({
interface: 'time',
// name,
uiSchema: {
type: 'string',
'x-component': 'TimePicker',
'x-component-props': {
format: 'HH:mm:ss',
},
},
}),
uid: () => {
return {
interface: 'input',
uiSchema: {
'x-component': 'Input',
'x-component-props': {
style: {
width: '100%',
},
},
},
};
},
uuid: () => {
return {
interface: 'uuid',
uiSchema: {
'x-component': 'Input',
'x-component-props': {
style: {
width: '100%',
},
},
},
};
},
virtual: '',
point: '',
polygon: '',
lineString: '',
circle: '',
};

View File

@ -0,0 +1,21 @@
export function normalizeRequestOptionsKey(value) {
if (Array.isArray(value)) {
return Object.fromEntries(
value.map((item) => {
const key = item.name;
const value2 = item.value;
return [key, value2];
}),
);
}
return value;
}
export function normalizeRequestOptions(actionOptions) {
const arrayKeys = ['headers', 'variables', 'params'];
for (const key of arrayKeys) {
if (actionOptions[key]) {
actionOptions[key] = normalizeRequestOptionsKey(actionOptions[key]);
}
}
return actionOptions;
}

View File

@ -1,9 +1,14 @@
import { Plugin } from '@tachybase/server';
import Application, { Plugin, PluginOptions } from '@tachybase/server';
import { PluginHttpDatasource } from './http/plugin';
import { MySQLDataSource } from './mysql/mysql-data-source';
import { PostgresDataSource } from './pg/postgres-data-source';
export class PluginExternalDataSourceServer extends Plugin {
constructor(app: Application, options?: PluginOptions) {
super(app, options);
this.addFeature(PluginHttpDatasource);
}
async afterAdd() {}
async beforeLoad() {

View File

@ -133,10 +133,10 @@ importers:
version: 8.4.0(eslint@9.10.0)(typescript@5.4.5)
umi:
specifier: ^4.3.3
version: 4.3.3(@babel/core@7.25.2)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@9.10.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.5))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
version: 4.3.3(@babel/core@7.22.10)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@9.10.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.5))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
vitest:
specifier: ^1.6.0
version: 1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.2.0)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
version: 1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
packages/core/acl:
dependencies:
@ -1010,7 +1010,7 @@ importers:
version: 5.4.4
umi:
specifier: ^4.3.3
version: 4.3.3(@babel/core@7.22.10)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@8.55.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.4))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
version: 4.3.3(@babel/core@7.25.2)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@8.55.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.4))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
packages/core/evaluators:
dependencies:
@ -2783,6 +2783,9 @@ importers:
packages/plugins/@tachybase/plugin-external-data-source:
dependencies:
'@ant-design/icons':
specifier: ^5.4.0
version: 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tachybase/client':
specifier: workspace:*
version: link:../../../core/client
@ -2813,6 +2816,9 @@ importers:
react-i18next:
specifier: ^14.1.2
version: 14.1.2(i18next@23.13.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom:
specifier: ^6.25.1
version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@types/lodash':
specifier: ^4.17.5
@ -20462,7 +20468,7 @@ snapshots:
'@ant-design/pro-layout@7.17.16(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ant-design/icons': 5.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/icons': 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-provider': 2.13.5(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-utils': 2.15.2(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@babel/runtime': 7.25.0
@ -20493,7 +20499,7 @@ snapshots:
'@ant-design/pro-utils@2.15.2(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@ant-design/icons': 5.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/icons': 5.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@ant-design/pro-provider': 2.13.5(antd@5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@babel/runtime': 7.25.0
antd: 5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -38420,19 +38426,19 @@ snapshots:
uglify-to-browserify@1.0.2:
optional: true
umi@4.3.3(@babel/core@7.22.10)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@8.55.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.4))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0):
umi@4.3.3(@babel/core@7.22.10)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@9.10.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.5))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0):
dependencies:
'@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.3.3
'@umijs/bundler-webpack': 4.3.3(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
'@umijs/bundler-webpack': 4.3.3(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
'@umijs/core': 4.3.3
'@umijs/lint': 4.3.3(eslint@8.55.0)(stylelint@16.8.2(typescript@5.4.4))(typescript@5.4.4)
'@umijs/preset-umi': 4.3.3(@types/node@20.14.2)(@types/react@18.3.3)(lightningcss@1.26.0)(rollup@3.29.4)(sass@1.77.8)(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
'@umijs/lint': 4.3.3(eslint@9.10.0)(stylelint@16.8.2(typescript@5.4.5))(typescript@5.4.5)
'@umijs/preset-umi': 4.3.3(@types/node@20.14.2)(@types/react@18.3.3)(lightningcss@1.26.0)(rollup@3.29.4)(sass@1.77.8)(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
'@umijs/renderer-react': 4.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.3.3
'@umijs/test': 4.3.3(@babel/core@7.22.10)
'@umijs/utils': 4.3.3
prettier-plugin-organize-imports: 3.2.4(prettier@3.2.5)(typescript@5.4.4)
prettier-plugin-organize-imports: 3.2.4(prettier@3.2.5)(typescript@5.4.5)
prettier-plugin-packagejson: 2.4.3(prettier@3.2.5)
transitivePeerDependencies:
- '@babel/core'
@ -38467,19 +38473,19 @@ snapshots:
- webpack-hot-middleware
- webpack-plugin-serve
umi@4.3.3(@babel/core@7.25.2)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@9.10.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.5))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0):
umi@4.3.3(@babel/core@7.25.2)(@types/node@20.14.2)(@types/react@18.3.3)(eslint@8.55.0)(lightningcss@1.26.0)(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@3.29.4)(sass@1.77.8)(stylelint@16.8.2(typescript@5.4.4))(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0):
dependencies:
'@babel/runtime': 7.23.6
'@umijs/bundler-utils': 4.3.3
'@umijs/bundler-webpack': 4.3.3(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
'@umijs/bundler-webpack': 4.3.3(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
'@umijs/core': 4.3.3
'@umijs/lint': 4.3.3(eslint@9.10.0)(stylelint@16.8.2(typescript@5.4.5))(typescript@5.4.5)
'@umijs/preset-umi': 4.3.3(@types/node@20.14.2)(@types/react@18.3.3)(lightningcss@1.26.0)(rollup@3.29.4)(sass@1.77.8)(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.5)(webpack@5.93.0)
'@umijs/lint': 4.3.3(eslint@8.55.0)(stylelint@16.8.2(typescript@5.4.4))(typescript@5.4.4)
'@umijs/preset-umi': 4.3.3(@types/node@20.14.2)(@types/react@18.3.3)(lightningcss@1.26.0)(rollup@3.29.4)(sass@1.77.8)(terser@5.31.6)(type-fest@4.25.0)(typescript@5.4.4)(webpack@5.93.0)
'@umijs/renderer-react': 4.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@umijs/server': 4.3.3
'@umijs/test': 4.3.3(@babel/core@7.25.2)
'@umijs/utils': 4.3.3
prettier-plugin-organize-imports: 3.2.4(prettier@3.2.5)(typescript@5.4.5)
prettier-plugin-organize-imports: 3.2.4(prettier@3.2.5)(typescript@5.4.4)
prettier-plugin-packagejson: 2.4.3(prettier@3.2.5)
transitivePeerDependencies:
- '@babel/core'
@ -38835,6 +38841,23 @@ snapshots:
string_decoder: 1.3.0
util-deprecate: 1.0.2
vite-node@1.6.0(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies:
cac: 6.7.14
debug: 4.3.6(supports-color@8.1.1)
pathe: 1.1.2
picocolors: 1.0.1
vite: 5.2.13(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
vite-node@1.6.0(@types/node@20.14.2)(less@4.2.0)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies:
cac: 6.7.14
@ -38875,6 +38898,19 @@ snapshots:
sass: 1.77.8
terser: 5.31.6
vite@5.2.13(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies:
esbuild: 0.20.2
postcss: 8.4.39
rollup: 4.14.1
optionalDependencies:
'@types/node': 20.14.2
fsevents: 2.3.3
less: 4.1.3
lightningcss: 1.26.0
sass: 1.77.8
terser: 5.31.6
vite@5.2.13(@types/node@20.14.2)(less@4.2.0)(lightningcss@1.26.0)(sass@1.75.0)(terser@5.31.6):
dependencies:
esbuild: 0.20.2
@ -38935,6 +38971,40 @@ snapshots:
- supports-color
- terser
vitest@1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies:
'@vitest/expect': 1.6.0
'@vitest/runner': 1.6.0
'@vitest/snapshot': 1.6.0
'@vitest/spy': 1.6.0
'@vitest/utils': 1.6.0
acorn-walk: 8.3.2
chai: 4.3.10
debug: 4.3.5(supports-color@5.5.0)
execa: 8.0.1
local-pkg: 0.5.0
magic-string: 0.30.8
pathe: 1.1.2
picocolors: 1.0.1
std-env: 3.7.0
strip-literal: 2.0.0
tinybench: 2.6.0
tinypool: 0.8.3
vite: 5.2.13(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
vite-node: 1.6.0(@types/node@20.14.2)(less@4.1.3)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6)
why-is-node-running: 2.2.2
optionalDependencies:
'@types/node': 20.14.2
jsdom: 24.1.1(canvas@2.11.2(encoding@0.1.13))
transitivePeerDependencies:
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color
- terser
vitest@1.6.0(@types/node@20.14.2)(jsdom@24.1.1(canvas@2.11.2(encoding@0.1.13)))(less@4.2.0)(lightningcss@1.26.0)(sass@1.77.8)(terser@5.31.6):
dependencies:
'@vitest/expect': 1.6.0

View File

@ -26,7 +26,7 @@
"module": "commonjs"
}
},
"include": ["packages/**/*", "playwright.config.ts", "vitest.config.mts"],
"include": ["packages/**/*", "playwright.config.ts", "vitest.config.mts", "packages/plugins/@tachybase/plugin-external-data-source/src/client/features/rest-api/kit.tsx"],
"exclude": [
"packages/**/node_modules",
"packages/**/dist",