fix: unable to create a MySQL view (#6477)

* fix: unable to create a MySQL view
This commit is contained in:
ajie 2025-03-18 11:47:25 +08:00 committed by GitHub
parent fd5c247503
commit 30c612cb08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 102 additions and 24 deletions

View File

@ -392,7 +392,6 @@ export class Collection<
} }
const fieldName = options.field || snakeCase(name); const fieldName = options.field || snakeCase(name);
const field = this.findField((f) => { const field = this.findField((f) => {
if (f.name === name) { if (f.name === name) {
return false; return false;
@ -406,10 +405,20 @@ export class Collection<
if (!field) { if (!field) {
return; return;
} }
if (options.type === field.type) {
if (options.type !== field.type) { return;
throw new Error(`fields with same column must be of the same type ${JSON.stringify(options)}`);
} }
const isContextTypeMatch = (data, dataType: string): boolean => {
return [data.dataType?.key, data.type?.toUpperCase()].includes(dataType?.toUpperCase());
};
if (options.type === 'context' && isContextTypeMatch(field, options.dataType)) {
return;
}
if (field.type === 'context' && isContextTypeMatch(options, field.dataType.key)) {
return;
}
throw new Error(`fields with same column must be of the same type ${JSON.stringify(options)}`);
} }
/** /**

View File

@ -13,7 +13,7 @@ export async function createApp(options: any = {}) {
const app = await createMockServer({ const app = await createMockServer({
acl: false, acl: false,
...options, ...options,
plugins: ['error-handler', 'field-sort', 'data-source-main', 'ui-schema-storage'], plugins: (options.plugins || []).concat(['error-handler', 'field-sort', 'data-source-main', 'ui-schema-storage']),
}); });
return app; return app;
} }

View File

@ -11,10 +11,11 @@ import Database, { Repository, ViewCollection, ViewFieldInference } from '@nocob
import Application from '@nocobase/server'; import Application from '@nocobase/server';
import { uid } from '@nocobase/utils'; import { uid } from '@nocobase/utils';
import { createApp } from '../index'; import { createApp } from '../index';
import { MockServer } from '@nocobase/test';
describe('view collection', function () { describe('view collection', function () {
let db: Database; let db: Database;
let app: Application; let app: MockServer;
let collectionRepository: Repository; let collectionRepository: Repository;
@ -25,6 +26,7 @@ describe('view collection', function () {
database: { database: {
tablePrefix: '', tablePrefix: '',
}, },
plugins: ['users'],
}); });
db = app.db; db = app.db;
@ -48,7 +50,7 @@ describe('view collection', function () {
await collectionRepository.create({ await collectionRepository.create({
values: { values: {
name: 'users', name: 'users_1',
fields: [ fields: [
{ name: 'name', type: 'string' }, { name: 'name', type: 'string' },
{ type: 'belongsTo', name: 'group', foreignKey: 'group_id' }, { type: 'belongsTo', name: 'group', foreignKey: 'group_id' },
@ -57,7 +59,7 @@ describe('view collection', function () {
context: {}, context: {},
}); });
const User = db.getCollection('users'); const User = db.getCollection('users_1');
const assoc = User.model.associations.group; const assoc = User.model.associations.group;
const foreignKey = assoc.foreignKey; const foreignKey = assoc.foreignKey;
@ -67,7 +69,7 @@ describe('view collection', function () {
await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`); await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`);
const createSQL = `CREATE VIEW ${viewName} AS SELECT id, ${foreignField}, name FROM ${db const createSQL = `CREATE VIEW ${viewName} AS SELECT id, ${foreignField}, name FROM ${db
.getCollection('users') .getCollection('users_1')
.quotedTableName()}`; .quotedTableName()}`;
await db.sequelize.query(createSQL); await db.sequelize.query(createSQL);
@ -107,7 +109,7 @@ describe('view collection', function () {
await collectionRepository.create({ await collectionRepository.create({
values: { values: {
name: 'users', name: 'users_1',
fields: [ fields: [
{ name: 'name', type: 'string' }, { name: 'name', type: 'string' },
{ type: 'belongsTo', name: 'group', foreignKey: 'group_id' }, { type: 'belongsTo', name: 'group', foreignKey: 'group_id' },
@ -116,7 +118,7 @@ describe('view collection', function () {
context: {}, context: {},
}); });
const User = db.getCollection('users'); const User = db.getCollection('users_1');
const assoc = User.model.associations.group; const assoc = User.model.associations.group;
const foreignKey = assoc.foreignKey; const foreignKey = assoc.foreignKey;
@ -126,7 +128,7 @@ describe('view collection', function () {
await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`); await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`);
const createSQL = `CREATE VIEW ${viewName} AS SELECT id, ${foreignField}, name FROM ${db const createSQL = `CREATE VIEW ${viewName} AS SELECT id, ${foreignField}, name FROM ${db
.getCollection('users') .getCollection('users_1')
.quotedTableName()}`; .quotedTableName()}`;
await db.sequelize.query(createSQL); await db.sequelize.query(createSQL);
@ -161,7 +163,7 @@ describe('view collection', function () {
it('should load view collection belongs to field', async () => { it('should load view collection belongs to field', async () => {
await collectionRepository.create({ await collectionRepository.create({
values: { values: {
name: 'users', name: 'users_1',
fields: [ fields: [
{ {
type: 'string', type: 'string',
@ -190,14 +192,14 @@ describe('view collection', function () {
type: 'belongsTo', type: 'belongsTo',
name: 'user', name: 'user',
foreignKey: 'userId', foreignKey: 'userId',
target: 'users', target: 'users_1',
}, },
], ],
}, },
context: {}, context: {},
}); });
await db.getRepository('users').create({ await db.getRepository('users_1').create({
values: [ values: [
{ {
name: 'u1', name: 'u1',
@ -216,7 +218,7 @@ describe('view collection', function () {
await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`); await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`);
const viewSQL = ` const viewSQL = `
CREATE VIEW ${viewName} as SELECT users.* FROM ${Post.quotedTableName()} as users CREATE VIEW ${viewName} as SELECT users_1.* FROM ${Post.quotedTableName()} as users_1
`; `;
await db.sequelize.query(viewSQL); await db.sequelize.query(viewSQL);
@ -256,7 +258,7 @@ describe('view collection', function () {
it('should use view collection as through collection', async () => { it('should use view collection as through collection', async () => {
const User = await collectionRepository.create({ const User = await collectionRepository.create({
values: { values: {
name: 'users', name: 'users_1',
fields: [{ name: 'name', type: 'string' }], fields: [{ name: 'name', type: 'string' }],
}, },
context: {}, context: {},
@ -270,9 +272,9 @@ describe('view collection', function () {
context: {}, context: {},
}); });
const UserCollection = db.getCollection('users'); const UserCollection = db.getCollection('users_1');
await db.getRepository('users').create({ await db.getRepository('users_1').create({
values: [{ name: 'u1' }, { name: 'u2' }], values: [{ name: 'u1' }, { name: 'u2' }],
}); });
@ -324,7 +326,7 @@ describe('view collection', function () {
await fieldsRepository.create({ await fieldsRepository.create({
values: { values: {
collectionName: 'users', collectionName: 'users_1',
name: 'roles', name: 'roles',
type: 'belongsToMany', type: 'belongsToMany',
target: 'my_roles', target: 'my_roles',
@ -335,14 +337,14 @@ describe('view collection', function () {
context: {}, context: {},
}); });
const users = await db.getRepository('users').find({ const users_1 = await db.getRepository('users_1').find({
appends: ['roles'], appends: ['roles'],
filter: { filter: {
name: 'u1', name: 'u1',
}, },
}); });
const roles = users[0].get('roles'); const roles = users_1[0].get('roles');
expect(roles).toHaveLength(2); expect(roles).toHaveLength(2);
await collectionRepository.destroy({ await collectionRepository.destroy({
@ -355,7 +357,7 @@ describe('view collection', function () {
expect( expect(
await fieldsRepository.count({ await fieldsRepository.count({
filter: { filter: {
collectionName: 'users', collectionName: 'users_1',
name: 'roles', name: 'roles',
}, },
}), }),
@ -366,7 +368,6 @@ describe('view collection', function () {
if (!db.inDialect('postgres')) { if (!db.inDialect('postgres')) {
return; return;
} }
const viewName = 'test_view'; const viewName = 'test_view';
const dbSchema = db.options.schema || 'public'; const dbSchema = db.options.schema || 'public';
const randomSchema = `s_${uid(6)}`; const randomSchema = `s_${uid(6)}`;
@ -541,4 +542,72 @@ describe('view collection', function () {
expect(db.getCollection('view_collection')).toBeUndefined(); expect(db.getCollection('view_collection')).toBeUndefined();
}); });
it('should create view collection successfully when underscored env and DB_DIALECT=mysql', async () => {
if (!db.options.underscored) {
return;
}
const tableName = db.inDialect('postgres') ? `${process.env.DB_SCHEMA}.users` : 'users';
const dropViewSQL = `DROP VIEW IF EXISTS test_view`;
await db.sequelize.query(dropViewSQL);
const viewSQL = `CREATE VIEW test_view AS select * from ${tableName}`;
await db.sequelize.query(viewSQL);
const response = await app
.agent()
.resource('collections')
.create({
values: {
name: 'fff1',
template: 'view',
view: true,
fields: [
{
name: 'id',
rawType: 'BIGINT',
field: 'id',
type: 'bigInt',
source: 'users.id',
uiSchema: { title: 'id' },
},
{
name: 'createdBy',
type: 'belongsTo',
source: 'users.createdBy',
uiSchema: { title: 'createdBy' },
},
{
name: 'created_by_id',
rawType: 'BIGINT',
field: 'created_by_id',
type: 'bigInt',
possibleTypes: ['bigInt', 'unixTimestamp', 'sort'],
uiSchema: { title: 'created_by_id' },
},
{
name: 'updatedBy',
type: 'belongsTo',
source: 'users.updatedBy',
uiSchema: { title: 'updatedBy' },
},
{
name: 'updated_by_id',
rawType: 'BIGINT',
field: 'updated_by_id',
type: 'bigInt',
possibleTypes: ['bigInt', 'unixTimestamp', 'sort'],
uiSchema: { title: 'updated_by_id' },
},
],
schema: null,
writableView: false,
sources: ['users'],
title: 'view_collection_display_name',
databaseView: 'test_view',
viewName: 'test_view',
},
});
expect(response.status).toBe(200);
});
}); });