test: main data source e2e test ()

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: main data source e2e test

* test: test

* test: test

* refactor: code improve

* test: test
This commit is contained in:
katherinehhh 2024-03-28 21:04:53 +08:00 committed by GitHub
parent 467699cdf9
commit 44e7f48f99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1023 additions and 9 deletions
packages
core
client/src/collection-manager/interfaces
test/src/e2e
plugins/@nocobase
plugin-collection-manager/src/client/__e2e__/collection-template
plugin-data-source-manager/src
plugin-mock-collections

View File

@ -17,7 +17,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface {
'x-validator': 'uuid',
},
};
availableTypes = ['string', 'uid'];
availableTypes = ['string', 'uid', 'uuid'];
properties = {
'uiSchema.title': {
type: 'string',

View File

@ -53,7 +53,6 @@ export interface CollectionSetting {
* @default false
*/
inherit?: boolean;
inherits?: string[];
category?: any[];
hidden?: boolean;
description?: string;
@ -120,6 +119,23 @@ interface AclRoleSetting {
key?: string;
//菜单权限配置
menuUiSchemas?: string[];
dataSourceKey?: string;
}
interface DatabaseSetting {
database: string;
host: string;
port: string;
schema?: string;
username?: string;
password?: string;
}
interface DataSourceSetting {
key: string;
displayName: string;
type: string;
options: DatabaseSetting;
enabled?: boolean;
}
export interface PageConfig {
@ -229,6 +245,15 @@ interface ExtendUtils {
*
*/
updateRole: <T = any>(roleSetting: AclRoleSetting) => Promise<T>;
/**
* pg
*/
mockExternalDataSource: <T = any>(DataSourceSetting: DataSourceSetting) => Promise<T>;
/**
*
* @param key key
*/
destoryExternalDataSource: <T = any>(key: string) => Promise<T>;
}
const PORT = process.env.APP_PORT || 20000;
@ -437,6 +462,20 @@ const _test = base.extend<ExtendUtils>({
await use(updateRole);
},
mockExternalDataSource: async ({ browser }, use) => {
const mockExternalDataSource = async (DataSourceSetting: DataSourceSetting) => {
return createExternalDataSource(DataSourceSetting);
};
await use(mockExternalDataSource);
},
destoryExternalDataSource: async ({ browser }, use) => {
const destoryDataSource = async (key: string) => {
return destoryExternalDataSource(key);
};
await use(destoryDataSource);
},
});
export const test = Object.assign(_test, {
@ -581,10 +620,6 @@ const deleteCollections = async (collectionNames: string[]) => {
const result = await api.post(`/api/collections:destroy?${params}`, {
headers,
params: {
// 自动删除依赖于集合的对象(如视图),进而删除依赖于这些对象的所有对象
cascade: true,
},
});
if (!result.ok()) {
@ -696,8 +731,12 @@ const updateRole = async (roleSetting: AclRoleSetting) => {
const state = await api.storageState();
const headers = getHeaders(state);
const name = roleSetting.name;
const dataSourceKey = roleSetting.dataSourceKey;
const result = await api.post(`/api/roles:update?filterByTk=${name}`, {
const url = !dataSourceKey
? `/api/roles:update?filterByTk=${name}`
: `/api/dataSources/${dataSourceKey}/roles:update?filterByTk=${name}`;
const result = await api.post(url, {
headers,
data: { ...roleSetting },
});
@ -724,7 +763,48 @@ const setDefaultRole = async (name) => {
data: { roleName: name },
});
};
/**
*
* @paramn
*/
const createExternalDataSource = async (dataSourceSetting: DataSourceSetting) => {
const api = await request.newContext({
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
});
const state = await api.storageState();
const headers = getHeaders(state);
const result = await api.post(`/api/dataSources:create`, {
headers,
data: { ...dataSourceSetting },
});
if (!result.ok()) {
throw new Error(await result.text());
}
const dataSourceData = (await result.json()).data;
return dataSourceData;
};
/**
*
* @paramn
*/
const destoryExternalDataSource = async (key) => {
const api = await request.newContext({
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
});
const state = await api.storageState();
const headers = getHeaders(state);
const result = await api.post(`/api/dataSources:destroy?filterByTk=${key}`, {
headers,
});
if (!result.ok()) {
throw new Error(await result.text());
}
const dataSourceData = (await result.json()).data;
return dataSourceData;
};
/**
* collection Faker
* @param collectionSetting

View File

@ -97,6 +97,238 @@ test.describe('create collection', () => {
});
});
// //预设字段
test.describe('create collection with preset fields', () => {
test('all preset fields by default', async ({ page }) => {
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByRole('button', { name: 'plus Create collection down' }).click();
await page.getByRole('menuitem', { name: 'General collection' }).locator('span').click();
await page.getByLabel('block-item-Input-collections-Collection display name').getByRole('textbox').fill(uid());
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('api/collections:create')),
page.getByLabel('action-Action-Submit').click(),
]);
const postData = request.postDataJSON();
//默认提交的数据符合预期
expect(postData).toMatchObject({
autoGenId: true,
createdAt: true,
createdBy: true,
updatedAt: true,
updatedBy: true,
fields: expect.arrayContaining([
expect.objectContaining({
name: 'id',
type: 'bigInt',
// 其他属性
}),
expect.objectContaining({
name: 'createdAt',
type: 'date',
// 其他属性
}),
expect.objectContaining({
name: 'createdBy',
type: 'belongsTo',
// 其他属性
}),
expect.objectContaining({
name: 'updatedBy',
type: 'belongsTo',
// 其他属性
}),
expect.objectContaining({
name: 'updatedAt',
type: 'date',
// 其他属性
}),
]),
});
});
test('id preset field', async ({ page }) => {
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByRole('button', { name: 'plus Create collection down' }).click();
await page.getByRole('menuitem', { name: 'General collection' }).locator('span').click();
await page.getByLabel('block-item-Input-collections-Collection display name').getByRole('textbox').fill(uid());
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().hover();
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().click();
await page.getByRole('row', { name: 'ID Integer Primary key' }).locator('.ant-checkbox-input').check();
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('api/collections:create')),
page.getByLabel('action-Action-Submit').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
autoGenId: true,
createdAt: false,
createdBy: false,
updatedAt: false,
updatedBy: false,
fields: expect.arrayContaining([
expect.objectContaining({
name: 'id',
type: 'bigInt',
// 其他属性
}),
]),
});
});
test('createdAt preset field', async ({ page }) => {
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByRole('button', { name: 'plus Create collection down' }).click();
await page.getByRole('menuitem', { name: 'General collection' }).locator('span').click();
await page.getByLabel('block-item-Input-collections-Collection display name').getByRole('textbox').fill(uid());
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().hover();
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().click();
await page.getByRole('row', { name: 'Created at' }).locator('.ant-checkbox-input').check();
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('api/collections:create')),
page.getByLabel('action-Action-Submit').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
autoGenId: false,
createdAt: true,
createdBy: false,
updatedAt: false,
updatedBy: false,
fields: expect.arrayContaining([
expect.objectContaining({
name: 'createdAt',
type: 'date',
// 其他属性
}),
]),
});
});
test('createdBy preset field', async ({ page }) => {
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByRole('button', { name: 'plus Create collection down' }).click();
await page.getByRole('menuitem', { name: 'General collection' }).locator('span').click();
await page.getByLabel('block-item-Input-collections-Collection display name').getByRole('textbox').fill(uid());
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().hover();
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().click();
await page.getByRole('row', { name: 'created By' }).locator('.ant-checkbox-input').check();
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('api/collections:create')),
page.getByLabel('action-Action-Submit').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
autoGenId: false,
createdAt: false,
createdBy: true,
updatedAt: false,
updatedBy: false,
fields: expect.arrayContaining([
expect.objectContaining({
name: 'createdBy',
type: 'belongsTo',
// 其他属性
}),
]),
});
});
test('updatedBy preset field', async ({ page }) => {
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByRole('button', { name: 'plus Create collection down' }).click();
await page.getByRole('menuitem', { name: 'General collection' }).locator('span').click();
await page.getByLabel('block-item-Input-collections-Collection display name').getByRole('textbox').fill(uid());
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().hover();
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().click();
await page.getByRole('row', { name: 'Last updated by' }).locator('.ant-checkbox-input').check();
//添加关系字段,断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('api/collections:create')),
page.getByLabel('action-Action-Submit').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
autoGenId: false,
createdAt: false,
createdBy: false,
updatedAt: false,
updatedBy: true,
fields: expect.arrayContaining([
expect.objectContaining({
name: 'updatedBy',
type: 'belongsTo',
// 其他属性
}),
]),
});
});
test('updatedAt preset field', async ({ page }) => {
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByRole('button', { name: 'plus Create collection down' }).click();
await page.getByRole('menuitem', { name: 'General collection' }).locator('span').click();
await page.getByLabel('block-item-Input-collections-Collection display name').getByRole('textbox').fill(uid());
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().hover();
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().click();
await page.getByRole('row', { name: 'Last updated at' }).locator('.ant-checkbox-input').check();
//添加关系字段,断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('api/collections:create')),
page.getByLabel('action-Action-Submit').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
autoGenId: false,
createdAt: false,
createdBy: false,
updatedAt: true,
updatedBy: false,
fields: expect.arrayContaining([
expect.objectContaining({
name: 'updatedAt',
type: 'date',
// 其他属性
}),
]),
});
});
test('unselect preset fields', async ({ page }) => {
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByRole('button', { name: 'plus Create collection down' }).click();
await page.getByRole('menuitem', { name: 'General collection' }).locator('span').click();
await page.getByLabel('block-item-Input-collections-Collection display name').getByRole('textbox').fill(uid());
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().hover();
await page.locator('.ant-drawer-content-wrapper .ant-table-container .ant-table-selection-column').first().click();
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('api/collections:create')),
page.getByLabel('action-Action-Submit').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
autoGenId: false,
createdAt: false,
createdBy: false,
updatedAt: false,
updatedBy: false,
fields: [],
});
});
});
test.describe('configure fields', () => {
test('basic', async ({ page, mockCollections }) => {
test.slow();
@ -261,6 +493,148 @@ test.describe('configure fields', () => {
});
});
//创建非Id为主键/唯一索引
test.describe('create primary key or unique index other than ID.', () => {
test('integer field as primary key', async ({ page, mockCollection }) => {
const name = uid();
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await mockCollection({
name: name,
autoGenId: false,
fields: [],
});
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(name);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${name}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'Integer' }).click();
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').click();
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').fill(uid());
await page.getByLabel('Primary').check();
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('/fields:create')),
page.getByLabel('action-Action-Submit-fields-').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
unique: false,
interface: 'integer',
primaryKey: true,
type: 'bigInt',
});
});
test('integer field set unique index', async ({ page, mockCollection }) => {
const name = uid();
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await mockCollection({
name: name,
autoGenId: false,
fields: [],
});
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(name);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${name}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'Integer' }).click();
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').click();
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').fill(uid());
// Primary 和 Unique选中一个其中一个自动取消勾选
await page.getByLabel('Primary').check();
await page.getByLabel('Unique').check();
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('/fields:create')),
page.getByLabel('action-Action-Submit-fields-').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
unique: true,
interface: 'integer',
primaryKey: false,
type: 'bigInt',
});
});
test('input(string) field set unique index', async ({ page, mockCollection }) => {
const name = uid();
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await mockCollection({
name: name,
autoGenId: false,
fields: [],
});
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(name);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${name}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'Single line text' }).click();
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').click();
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').fill(uid());
// Primary 和 Unique选中一个其中一个自动取消勾选
await page.getByLabel('Primary').check();
await page.getByLabel('Unique').check();
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('/fields:create')),
page.getByLabel('action-Action-Submit-fields-').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
unique: true,
interface: 'input',
primaryKey: false,
type: 'string',
});
});
test('input(string) field set as primary key', async ({ page, mockCollection }) => {
const name = uid();
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await mockCollection({
name: name,
autoGenId: false,
fields: [],
});
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(name);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${name}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'Single line text' }).click();
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').click();
await page.getByLabel('block-item-Input-fields-Field display name').getByRole('textbox').fill(uid());
// Primary 和 Unique选中一个其中一个自动取消勾选
await page.getByLabel('Unique').check();
await page.getByLabel('Primary').check();
//断言提交的data是否符合预期
const [request] = await Promise.all([
page.waitForRequest((request) => request.url().includes('/fields:create')),
page.getByLabel('action-Action-Submit-fields-').click(),
]);
const postData = request.postDataJSON();
//提交的数据符合预期
expect(postData).toMatchObject({
unique: false,
interface: 'input',
primaryKey: true,
type: 'string',
});
});
});
test.describe('edit', () => {
test('basic', async ({ page, mockCollection }) => {
const collectionDisplayName = uid();
@ -283,3 +657,291 @@ test.describe('edit', () => {
await expect(page.getByRole('cell', { name: newDescription, exact: true })).toBeVisible();
});
});
//创建关系字段
//sourceKey, targetKey, optional field types are [string ',' bigInt ',' integer ',' uuid ',' uid '] and are non primary key field with a Unique index set
test.describe('association constraints support selecting non-primary key fields with Unique indexes', () => {
const fields = [
{ name: 'id', interface: 'id', type: 'bigInt' },
{ name: 'string', interface: 'input', type: 'string', unique: true },
{ name: 'bigInt', type: 'bigInt', interface: 'integer', unique: true },
{ name: 'integer', type: 'integer', interface: 'input', unique: true },
{ name: 'uuid', type: 'uuid', interface: 'input', unique: true },
{ name: 'uid', type: 'uid', interface: 'input', unique: true },
{ name: 'string1', interface: 'input', type: 'string' },
{ name: 'bigInt1', type: 'bigInt', interface: 'integer' },
{ name: 'integer1', type: 'integer', interface: 'input' },
{ name: 'uuid1', type: 'uuid', interface: 'input' },
{ name: 'uid1', type: 'uid', interface: 'input' },
];
test('oho sourceKey', async ({ page, mockCollection }) => {
const collectionName = uid();
await mockCollection({
name: collectionName,
autoGenId: true,
fields,
});
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(collectionName);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${collectionName}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'One to one (has one)' }).click();
await page.getByLabel('block-item-SourceKey-fields-').click();
// 获取所有选项的文本内容
const options = await page.locator('.rc-virtual-list').evaluate(() => {
const optionElements = document.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
// 断言下拉列表是否符合预期
expect(options).toEqual(expect.arrayContaining(['ID', 'string', 'bigInt', 'integer', 'uuid', 'uid']));
});
test('obo targetKey', async ({ page, mockCollection }) => {
const collectionName = uid();
await mockCollection({
name: collectionName,
autoGenId: true,
fields,
});
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(collectionName);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${collectionName}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'One to one (belongs to)' }).click();
await page.getByLabel('block-item-Select-fields-Target collection').click();
await page.getByRole('option', { name: collectionName }).locator('div').click();
await page.getByLabel('block-item-TargetKey-fields-').click();
// 获取所有选项的文本内容
const options = await page
.locator('.rc-virtual-list')
.nth(1)
.evaluate((element) => {
const optionElements = element.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
// 断言下拉列表是否符合预期
expect(options).toEqual(expect.arrayContaining(['ID', 'string', 'bigInt', 'integer', 'uuid', 'uid']));
});
test('o2m targetKey & sourceKey', async ({ page, mockCollection }) => {
const collectionName = uid();
await mockCollection({
name: collectionName,
autoGenId: true,
fields,
});
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(collectionName);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${collectionName}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'One to many' }).click();
await page.getByLabel('block-item-SourceKey-fields-').click();
// sourceKey 选项符合预期
const sourcekeyOptions = await page.locator('.rc-virtual-list').evaluate((element) => {
const optionElements = element.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
// 断言下拉列表是否符合预期
expect(sourcekeyOptions).toEqual(['ID', 'string', 'bigInt', 'integer', 'uuid', 'uid']);
await page.getByLabel('block-item-Select-fields-Target collection').click();
await page.getByRole('option', { name: collectionName }).locator('div').click();
await page.getByLabel('block-item-TargetKey-fields-').click();
//targetKey 选项符合预期
const targetKeyOptions = await page
.locator('.rc-virtual-list')
.nth(2)
.evaluate((element) => {
const optionElements = element.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
// 断言下拉列表是否符合预期,o2m的targetkey 不限制unique
expect(targetKeyOptions).toEqual(
expect.arrayContaining([
'ID',
'string',
'bigInt',
'integer',
'uuid',
'uid',
'string1',
'bigInt1',
'integer1',
'uuid1',
'uid1',
]),
);
});
test('m2o targetKey & foreignKey', async ({ page, mockCollection }) => {
const collectionName = uid();
const foreignKey = `f_${uid()}`;
await mockCollection({
name: collectionName,
autoGenId: true,
fields,
});
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(collectionName);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${collectionName}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'Many to one' }).click();
await page.getByLabel('block-item-ForeignKey-fields-').locator('input').click();
// ForeignKey 选项符合预期
const foreignKeyOptions = await page.locator('.rc-virtual-list').evaluate((element) => {
const optionElements = element.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
// 断言下拉列表是否符合预期
expect(foreignKeyOptions).toEqual(
expect.arrayContaining([
'ID',
'string',
'bigInt',
'integer',
'uuid',
'uid',
'string1',
'bigInt1',
'integer1',
'uuid1',
'uid1',
]),
);
//ForeignKey 支持自定义输入和选择
await page.getByLabel('block-item-ForeignKey-fields-').locator('input').clear();
await page.getByLabel('block-item-ForeignKey-fields-').locator('input').fill(foreignKey);
const inputValue = await page.getByLabel('block-item-ForeignKey-fields-').locator('input').inputValue();
expect(inputValue).toEqual(foreignKey);
await page.getByRole('option', { name: 'uuid1' }).locator('div').click();
const optionValue = await page.getByLabel('block-item-ForeignKey-fields-').locator('input').inputValue();
expect(optionValue).toEqual('uuid1');
await page.getByLabel('block-item-Select-fields-Target collection').click();
await page.getByRole('option', { name: collectionName }).locator('div').click();
await page.getByLabel('block-item-TargetKey-fields-').click();
//targetKey 选项符合预期
const targetKeyOptions = await page
.locator('.rc-virtual-list')
.nth(2)
.evaluate((element) => {
const optionElements = element.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
// 断言下拉列表是否符合预期, 限制unique
expect(targetKeyOptions).toEqual(expect.arrayContaining(['ID', 'string', 'bigInt', 'integer', 'uuid', 'uid']));
});
test('m2m sourceKey & foreignKey & targetKey', async ({ page, mockCollection }) => {
const collectionName = uid();
const foreignKey = `f_${uid()}`;
await mockCollection({
name: collectionName,
autoGenId: true,
fields,
});
await page.goto('/admin/settings/data-source-manager/list');
await page.getByRole('button', { name: 'Configure' }).first().click();
await page.getByLabel('action-Filter.Action-Filter-').click();
await page.getByRole('textbox').nth(1).click();
await page.getByRole('textbox').nth(1).fill(collectionName);
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel(`action-Action.Link-Configure fields-collections-${collectionName}`).click();
await page.getByRole('button', { name: 'plus Add field' }).click();
await page.getByRole('menuitem', { name: 'Many to many' }).click();
await page.getByLabel('block-item-SourceKey-fields-').click();
// sourceKey 选项符合预期
const sourcekeyOptions = await page.locator('.rc-virtual-list').evaluate((element) => {
const optionElements = element.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
expect(sourcekeyOptions).toEqual(['ID', 'string', 'bigInt', 'integer', 'uuid', 'uid']);
// ForeignKey1 选项符合预期
await page.getByLabel('block-item-ThroughCollection-').click();
await page.getByRole('option', { name: collectionName }).locator('div').click();
await page.getByLabel('block-item-ForeignKey-fields-Foreign key 1').locator('input').click();
const foreignKeyOptions = await page
.locator('.rc-virtual-list')
.nth(2)
.evaluate((element) => {
const optionElements = element.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
// 断言下拉列表是否符合预期
expect(foreignKeyOptions).toEqual(
expect.arrayContaining([
'ID',
'string',
'bigInt',
'integer',
'uuid',
'uid',
'string1',
'bigInt1',
'integer1',
'uuid1',
'uid1',
]),
);
//ForeignKey1 支持自定义输入和选择
await page.getByLabel('block-item-ForeignKey-fields-Foreign key 1').locator('input').clear();
await page.getByLabel('block-item-ForeignKey-fields-Foreign key 1').locator('input').fill(foreignKey);
const inputValue = await page
.getByLabel('block-item-ForeignKey-fields-Foreign key 1')
.locator('input')
.inputValue();
expect(inputValue).toEqual(foreignKey);
await page.getByRole('option', { name: 'uuid1' }).locator('div').click();
const optionValue = await page
.getByLabel('block-item-ForeignKey-fields-Foreign key 1')
.locator('input')
.inputValue();
expect(optionValue).toEqual('uuid1');
await page.getByLabel('block-item-Select-fields-Target collection').click();
await page.getByRole('option', { name: collectionName }).locator('div').click();
await page.getByLabel('block-item-TargetKey-fields-').click();
//targetKey 选项符合预期
const targetKeyOptions = await page
.locator('.rc-virtual-list')
.nth(2)
.evaluate((element) => {
const optionElements = element.querySelectorAll('.ant-select-item-option');
return Array.from(optionElements).map((option) => option.textContent);
});
// 断言下拉列表是否符合预期, 限制unique
expect(targetKeyOptions).toEqual(expect.arrayContaining(['ID', 'string', 'bigInt', 'integer', 'uuid', 'uid']));
});
});

View File

@ -38,6 +38,9 @@ export const CollectionFieldInterfaceSelect = observer(
</Tag>
) : (
<Select
aria-label={`field-interface-${record?.type}`}
//@ts-ignore
role="button"
defaultValue={value}
style={{ width: '100%' }}
popupMatchSelectWidth={false}

View File

@ -38,7 +38,9 @@ export const FieldTitleInput = observer(
useEffect(() => {
setTitleValue(value);
}, [value]);
return <Input value={titleValue} onChange={handleChange} style={{ minWidth: '100px' }} />;
return (
<Input value={titleValue} onChange={handleChange} style={{ minWidth: '100px' }} aria-label="field-title-input" />
);
},
{ displayName: 'FieldTitleInput' },
);

View File

@ -12,6 +12,9 @@ export const FieldType = observer(
<Tag>{value}</Tag>
) : (
<Select
aria-label={`field-type-${value}`}
//@ts-ignore
role="button"
defaultValue={value}
popupMatchSelectWidth={false}
style={{ width: '100%' }}

View File

@ -31,6 +31,8 @@ export const ViewDatabaseConnectionAction = () => {
onClick={() => {
navigate(getConnectionCollectionPath(record.key));
}}
role="button"
aria-label={`${record?.key}-Configure`}
>
{t('Configure')}
</Button>

View File

@ -125,6 +125,33 @@ describe('data source with acl', () => {
expect(postRes.status).toBe(200);
});
it('should update roles resources', async () => {
const adminUser = await app.db.getRepository('users').create({
values: {
roles: ['root'],
},
});
const adminAgent: any = app.agent().login(adminUser);
await adminAgent.resource('dataSources.roles', 'mockInstance1').update({
filterByTk: 'member',
values: {
resources: [
{
posts: {
actions: {
view: {
fields: ['title'],
},
},
},
},
],
},
});
});
it('should set main data source strategy', async () => {
const adminUser = await app.db.getRepository('users').create({
values: {

View File

@ -26,6 +26,25 @@ export default {
});
}
if (params.values.resources) {
await ctx.db.getRepository('dataSourcesRolesResources').destroy({
filter: {
roleName: name,
dataSourceKey,
},
});
for (const resource of params.values.resources) {
await ctx.db.getRepository('dataSourcesRolesResources').create({
values: {
...resource,
roleName: name,
dataSourceKey,
},
});
}
}
ctx.body = connectionRoleRecord.toJSON();
await next();

View File

@ -13,5 +13,8 @@
"@nocobase/test": "0.x",
"@nocobase/utils": "0.x"
},
"devDependencies": {
"pg": "^8.11.3"
},
"gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1"
}

View File

@ -0,0 +1,175 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 创建 users 表
CREATE TABLE users (
user_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(100) NOT NULL
);
-- 创建 profiles 表
CREATE TABLE profiles (
profile_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
address VARCHAR(255),
user_uuid UUID REFERENCES users(user_uuid)
);
-- 创建 roles 表
CREATE TABLE roles (
role_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
role_name VARCHAR(100) NOT NULL
);
-- 创建 user_roles 关联表
CREATE TABLE user_roles (
user_uuid UUID REFERENCES users(user_uuid),
role_uuid UUID REFERENCES roles(role_uuid),
PRIMARY KEY (user_uuid, role_uuid)
);
-- 创建 orders 表
CREATE TABLE orders (
order_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_uuid UUID REFERENCES users(user_uuid),
order_name VARCHAR(255) NOT NULL
-- ... 其他字段
);
-- 向 users 表插入测试数据
INSERT INTO users (username) VALUES
('Alice'),
('Bob');
INSERT INTO orders (user_uuid, order_name) VALUES
((SELECT user_uuid FROM users WHERE username = 'Alice'), 'Order1'),
((SELECT user_uuid FROM users WHERE username = 'Bob'), 'Order2');
-- 向 profiles 表插入测试数据
INSERT INTO profiles (address, user_uuid) VALUES
('123 Main St', (SELECT user_uuid FROM users WHERE username = 'Alice')),
('456 High St', (SELECT user_uuid FROM users WHERE username = 'Bob'));
-- 向 roles 表插入测试数据
INSERT INTO roles (role_name) VALUES
('Admin'),
('User');
-- 建立 users 和 roles 之间的关系
INSERT INTO user_roles (user_uuid, role_uuid) VALUES
((SELECT user_uuid FROM users WHERE username = 'Alice'), (SELECT role_uuid FROM roles WHERE role_name = 'Admin')),
((SELECT user_uuid FROM users WHERE username = 'Bob'), (SELECT role_uuid FROM roles WHERE role_name = 'User'));
CREATE TABLE "test_table" (
"id2" SERIAL PRIMARY KEY,
"smallint" int2,
"integer" int4,
"bigint" int8,
"boolean" bool,
"numeric" numeric,
"real" float4,
"double" float8,
"money" money,
"date" date,
"time" time,
"timestamp" timestamp,
"timestamptz" timestamptz,
"interval" interval,
"char" bpchar,
"varchar" varchar,
"text" text,
"tsquery" tsquery,
"tsvector" tsvector,
"uuid" uuid,
"xml" xml,
"json" json,
"jsonb" jsonb,
"bit" bit,
"bitvarying" varbit,
"bytea" bytea,
"cidr" cidr,
"inet" inet,
"macaddr" macaddr,
"txidsnapshot" txid_snapshot,
"box" box,
"circle" circle,
"line" line,
"lseg" lseg,
"path" path,
"point" point,
"polygon" polygon
);
-- 创建 authors 表
CREATE TABLE authors (
author_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
author_name VARCHAR(100) NOT NULL
);
-- 创建 posts 表
CREATE TABLE posts (
post_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
post_title VARCHAR(100) NOT NULL,
post_content TEXT NOT NULL,
author_id UUID REFERENCES authors(author_id)
);
-- 创建 comments 表
CREATE TABLE comments (
comment_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
comment_content TEXT NOT NULL,
post_id UUID REFERENCES posts(post_id),
author_id UUID REFERENCES authors(author_id)
);
-- 创建 tags 表
CREATE TABLE tags (
tag_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tag_name VARCHAR(100) NOT NULL
);
-- 创建 post_tags 关联表
CREATE TABLE post_tags (
post_id UUID REFERENCES posts(post_id),
tag_id UUID REFERENCES tags(tag_id),
PRIMARY KEY (post_id, tag_id)
);
-- 向 authors 表插入测试数据
INSERT INTO authors (author_name) VALUES
('Author1'),
('Author2');
-- 向 posts 表插入测试数据
INSERT INTO posts (post_title, post_content, author_id) VALUES
('Post1', 'This is the content of Post1', (SELECT author_id FROM authors WHERE author_name = 'Author1')),
('Post2', 'This is the content of Post2', (SELECT author_id FROM authors WHERE author_name = 'Author2'));
-- 向 comments 表插入测试数据
INSERT INTO comments (comment_content, post_id, author_id) VALUES
('This is a comment on Post1', (SELECT post_id FROM posts WHERE post_title = 'Post1'), (SELECT author_id FROM authors WHERE author_name = 'Author1')),
('This is a comment on Post2', (SELECT post_id FROM posts WHERE post_title = 'Post2'), (SELECT author_id FROM authors WHERE author_name = 'Author2'));
-- 向 tags 表插入测试数据
INSERT INTO tags (tag_name) VALUES
('Tag1'),
('Tag2');
-- 建立 posts 和 tags 之间的关系
INSERT INTO post_tags (post_id, tag_id) VALUES
((SELECT post_id FROM posts WHERE post_title = 'Post1'), (SELECT tag_id FROM tags WHERE tag_name = 'Tag1')),
((SELECT post_id FROM posts WHERE post_title = 'Post2'), (SELECT tag_id FROM tags WHERE tag_name = 'Tag2'));
-- 创建无主键的表 test_table_no_pk
CREATE TABLE test_table_no_pk (
column1 VARCHAR(100),
column2 INT
);
-- 向 test_table_no_pk 表插入测试数据
INSERT INTO test_table_no_pk (column1, column2) VALUES
('TestData1', 1),
('TestData2', 2);

View File

@ -1,11 +1,14 @@
import Database, { Collection, Repository } from '@nocobase/database';
import { CollectionRepository } from '@nocobase/plugin-collection-manager';
import { Plugin } from '@nocobase/server';
import { InstallOptions, Plugin } from '@nocobase/server';
import { merge, uid } from '@nocobase/utils';
import _ from 'lodash';
import collectionTemplates from './collection-templates';
import * as fieldInterfaces from './field-interfaces';
import { mockAttachment } from './field-interfaces';
import { Client } from 'pg';
import path from 'path';
import { promises as fs } from 'fs';
export class PluginMockCollectionsServer extends Plugin {
async load() {
@ -304,6 +307,41 @@ export class PluginMockCollectionsServer extends Plugin {
},
});
}
async install(options?: InstallOptions) {
const dbName = this.app.db.options.database;
const externalDB = `${dbName}_external_test`;
// create external database
let client = new Client({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
database: process.env.DB_DATABASE,
port: parseInt(process.env.DB_PORT),
});
await client.connect();
await client.query(`DROP DATABASE IF EXISTS ${externalDB}`);
await client.query(`CREATE DATABASE ${externalDB}`);
await client.end();
// import sql import external database
client = new Client({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
database: externalDB,
port: parseInt(process.env.DB_PORT),
});
await client.connect();
const sqlFile = path.resolve(__dirname, './external.sql');
const sql: string = await fs.readFile(sqlFile, 'utf8');
await client.query(sql);
await client.end();
}
}
export default PluginMockCollectionsServer;