mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-11-30 03:08:31 +08:00
Merge branch 'main' into next
This commit is contained in:
commit
41e08c6d29
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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/
|
||||||
|
|
||||||
## 与众不同之处
|
## 与众不同之处
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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',
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user