Merge branch 'main' into next

This commit is contained in:
Zeke Zhang 2024-07-15 12:14:00 +08:00
commit 41e08c6d29
10 changed files with 274 additions and 12 deletions

View File

@ -1,14 +1,19 @@
--- ---
name: Bug report name: Bug report
about: Report a bug to help us improve. Please use discussions for feature requests. about: Report a bug to help us improve. Please communicate in English, and post content in other languages to NocoBase Forum https://forum.nocobase.com/.
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---
<!-- Note: Please do not clear the contents of the issue template. Items marked with * are required. Issues not filled out according to the template will be closed. --> <!--
<!-- 注意:请不要将 issue 模板内容清空,带 * 的项目为必填项没有按照模板填写的issue将被关闭。--> First off, thank you for reporting bugs.
Please do not clear the contents of the issue template. Items marked with * are required. Issues not filled out according to the template will be closed.
Please communicate in English, and post content in other languages to NocoBase Forum https://forum.nocobase.com/. Non-English issues will be closed.
-->
## * Describe the bug ## * Describe the bug

View File

@ -37,8 +37,8 @@ https://demo.nocobase.com/new
Documents: Documents:
https://docs.nocobase.com/ https://docs.nocobase.com/
Contact Us: Forum:
hello@nocobase.com https://forum.nocobase.com/
## Distinctive features ## Distinctive features

View File

@ -27,7 +27,7 @@ NocoBase 是一个极易扩展的开源无代码开发平台。
不必投入几年时间、数百万资金研发,花几分钟时间部署 NocoBase马上拥有一个私有、可控、极易扩展的无代码开发平台。 不必投入几年时间、数百万资金研发,花几分钟时间部署 NocoBase马上拥有一个私有、可控、极易扩展的无代码开发平台。
中文官网: 中文官网:
https://cn.nocobase.com/ https://www.nocobase.com/cn
在线体验: 在线体验:
https://demo-cn.nocobase.com/new https://demo-cn.nocobase.com/new
@ -35,8 +35,8 @@ https://demo-cn.nocobase.com/new
文档: 文档:
https://docs-cn.nocobase.com/ https://docs-cn.nocobase.com/
联系我们: 社区:
hello@nocobase.com https://forum.nocobase.com/
## 与众不同之处 ## 与众不同之处

View File

@ -27,6 +27,17 @@ export function transactionWrapperBuilder(transactionGenerator) {
newTransaction = true; newTransaction = true;
} }
transaction.afterCommit(() => {
if (transaction.eventCleanupBinded) {
return;
}
transaction.eventCleanupBinded = true;
if (this.database) {
this.database.removeAllListeners(`transactionRollback:${transaction.id}`);
}
});
// 需要将 newTransaction 注入到被装饰函数参数内 // 需要将 newTransaction 注入到被装饰函数参数内
if (newTransaction) { if (newTransaction) {
try { try {
@ -54,6 +65,11 @@ export function transactionWrapperBuilder(transactionGenerator) {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
await transaction.rollback(); await transaction.rollback();
if (this.database) {
await this.database.emitAsync(`transactionRollback:${transaction.id}`);
await this.database.removeAllListeners(`transactionRollback:${transaction.id}`);
}
throw err; throw err;
} }
} else { } else {

View File

@ -30,8 +30,7 @@ interface JSONTransformerOptions {
export class Model<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes> export class Model<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes>
extends SequelizeModel<TModelAttributes, TCreationAttributes> extends SequelizeModel<TModelAttributes, TCreationAttributes>
implements IModel implements IModel {
{
public static database: Database; public static database: Database;
public static collection: Collection; public static collection: Collection;
@ -47,7 +46,7 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
static async sync(options) { static async sync(options) {
const runner = new SyncRunner(this); const runner = new SyncRunner(this);
return runner.runSync(options); return await runner.runSync(options);
} }
// TODO // TODO

View File

@ -76,6 +76,7 @@ export class SyncRunner {
try { try {
const beforeColumns = await this.queryInterface.describeTable(this.tableName, options); const beforeColumns = await this.queryInterface.describeTable(this.tableName, options);
await this.checkAutoIncrementField(beforeColumns, options);
await this.handlePrimaryKeyBeforeSync(beforeColumns, options); await this.handlePrimaryKeyBeforeSync(beforeColumns, options);
await this.handleUniqueFieldBeforeSync(beforeColumns, options); await this.handleUniqueFieldBeforeSync(beforeColumns, options);
} catch (e) { } catch (e) {
@ -94,6 +95,20 @@ export class SyncRunner {
return syncResult; return syncResult;
} }
async checkAutoIncrementField(beforeColumns, options) {
// if there is auto increment field, throw error
if (!this.database.isMySQLCompatibleDialect()) {
return;
}
const autoIncrFields = Object.keys(this.rawAttributes).filter((key) => {
return this.rawAttributes[key].autoIncrement;
});
if (autoIncrFields.length > 1) {
throw new Error(`Auto increment field can't be more than one: ${autoIncrFields.join(', ')}`);
}
}
async handleUniqueFieldBeforeSync(beforeColumns, options) { async handleUniqueFieldBeforeSync(beforeColumns, options) {
if (!this.database.inDialect('sqlite')) { if (!this.database.inDialect('sqlite')) {
return; return;

View File

@ -345,6 +345,148 @@ describe('xlsx importer', () => {
}); });
}); });
it('should report validation error message on not null validation', async () => {
const User = app.db.collection({
name: 'users',
fields: [
{
type: 'string',
name: 'name',
allowNull: false,
},
{
type: 'string',
name: 'email',
},
],
});
await app.db.sync();
const templateCreator = new TemplateCreator({
collection: User,
columns: [
{
dataIndex: ['name'],
defaultTitle: '姓名',
},
{
dataIndex: ['email'],
defaultTitle: '邮箱',
},
],
});
const template = await templateCreator.run();
const worksheet = template.Sheets[template.SheetNames[0]];
XLSX.utils.sheet_add_aoa(worksheet, [[null, 'test@qq.com']], {
origin: 'A2',
});
const importer = new XlsxImporter({
collectionManager: app.mainDataSource.collectionManager,
collection: User,
columns: [
{
dataIndex: ['name'],
defaultTitle: '姓名',
},
{
dataIndex: ['email'],
defaultTitle: '邮箱',
},
],
workbook: template,
});
let error;
try {
await importer.run();
} catch (e) {
error = e;
}
expect(error).toBeTruthy();
console.log(error.message);
});
it('should report validation error message on unique validation', async () => {
const User = app.db.collection({
name: 'users',
fields: [
{
type: 'string',
name: 'name',
unique: true,
},
{
type: 'string',
name: 'email',
},
],
});
await app.db.sync();
const templateCreator = new TemplateCreator({
collection: User,
columns: [
{
dataIndex: ['name'],
defaultTitle: '姓名',
},
{
dataIndex: ['email'],
defaultTitle: '邮箱',
},
],
});
const template = await templateCreator.run();
const worksheet = template.Sheets[template.SheetNames[0]];
XLSX.utils.sheet_add_aoa(
worksheet,
[
['User1', 'test@test.com'],
['User1', 'test@test.com'],
],
{
origin: 'A2',
},
);
const importer = new XlsxImporter({
collectionManager: app.mainDataSource.collectionManager,
collection: User,
columns: [
{
dataIndex: ['name'],
defaultTitle: '姓名',
},
{
dataIndex: ['email'],
defaultTitle: '邮箱',
},
],
workbook: template,
});
let error;
try {
await importer.run();
} catch (e) {
error = e;
}
expect(error).toBeTruthy();
console.log(error.message);
});
it('should import china region field', async () => { it('should import china region field', async () => {
const Post = app.db.collection({ const Post = app.db.collection({
name: 'posts', name: 'posts',

View File

@ -188,7 +188,9 @@ export class XlsxImporter extends EventEmitter {
await new Promise((resolve) => setTimeout(resolve, 5)); await new Promise((resolve) => setTimeout(resolve, 5));
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`failed to import row ${handingRowIndex}, message: ${error.message}, rowData: ${JSON.stringify(rowValues)}`, `failed to import row ${handingRowIndex}, ${this.renderErrorMessage(error)}, rowData: ${JSON.stringify(
rowValues,
)}`,
{ cause: error }, { cause: error },
); );
} }
@ -201,6 +203,14 @@ export class XlsxImporter extends EventEmitter {
return imported; return imported;
} }
renderErrorMessage(error) {
let message = error.message;
if (error.parent) {
message += `: ${error.parent.message}`;
}
return message;
}
trimString(str: string) { trimString(str: string) {
if (typeof str === 'string') { if (typeof str === 'string') {
return str.trim(); return str.trim();

View File

@ -0,0 +1,70 @@
import Database from '@nocobase/database';
import { MockServer } from '@nocobase/test';
import { createApp } from '..';
describe('destroy', () => {
let db: Database;
let app: MockServer;
beforeEach(async () => {
app = await createApp({
database: {
tablePrefix: '',
},
});
db = app.db;
});
afterEach(async () => {
await app.destroy();
});
it.runIf(process.env.DB_DIALECT === 'mysql')('should not create auto increment field more than one', async () => {
await db.getRepository('collections').create({
values: {
name: 'posts',
autoGenId: false,
fields: [
{
name: 'id',
type: 'integer',
autoIncrement: true,
primaryKey: true,
},
],
},
context: {},
});
const postCollection = db.getCollection('posts');
expect(postCollection.getField('id')).toBeTruthy();
let error = null;
try {
await db.getRepository('fields').create({
values: {
name: 'xxx',
type: 'integer',
collectionName: 'posts',
autoIncrement: true,
},
context: {},
});
} catch (e) {
error = e;
}
expect(error).toBeTruthy();
expect(
await db.getRepository('fields').count({
filter: {
collectionName: 'posts',
name: 'xxx',
},
}),
).toBe(0);
expect(postCollection.getField('xxx')).toBeFalsy();
});
});

View File

@ -54,6 +54,11 @@ export class FieldModel extends MagicAttributeModel {
transaction, transaction,
}); });
if (transaction) {
this.db.on('transactionRollback:' + transaction['id'], async () => {
collection.removeField(name);
});
}
return field; return field;
} }