Merge branch 'main' into next

This commit is contained in:
xilesun 2024-07-05 09:20:21 +08:00
commit ade9b70d51
9 changed files with 388 additions and 4 deletions

View File

@ -112,7 +112,7 @@ jobs:
DB_SCHEMA: ${{ matrix.schema }}
COLLECTION_MANAGER_SCHEMA: ${{ matrix.collection_schema }}
DB_TEST_DISTRIBUTOR_PORT: 23450
DB_TEST_PREFIX: test_
DB_TEST_PREFIX: test
timeout-minutes: 60
mysql-test:

View File

@ -165,6 +165,24 @@ describe('example', () => {
await app.destroy();
});
it('should call beforeAddDataSource hook', async () => {
const hook = vi.fn();
const app = await createMockServer({
acl: false,
resourcer: {
prefix: '/api/',
},
name: 'update-filter',
});
app.dataSourceManager.beforeAddDataSource(hook);
// it should be called on main datasource
expect(hook).toBeCalledTimes(1);
await app.destroy();
});
it('should register every datasource instance', async () => {
const hook = vi.fn();

View File

@ -21,6 +21,7 @@ export class DataSourceManager {
factory: DataSourceFactory = new DataSourceFactory();
protected middlewares = [];
private onceHooks: Array<DataSourceHook> = [];
private beforeAddHooks: Array<DataSourceHook> = [];
constructor(public options = {}) {
this.dataSources = new Map();
@ -32,6 +33,10 @@ export class DataSourceManager {
}
async add(dataSource: DataSource, options: any = {}) {
for (const hook of this.beforeAddHooks) {
hook(dataSource);
}
await dataSource.load(options);
this.dataSources.set(dataSource.name, dataSource);
@ -71,6 +76,13 @@ export class DataSourceManager {
return this.factory.create(type, options);
}
beforeAddDataSource(hook: DataSourceHook) {
this.beforeAddHooks.push(hook);
for (const dataSource of this.dataSources.values()) {
hook(dataSource);
}
}
afterAddDataSource(hook: DataSourceHook) {
this.addHookAndRun(hook);
}

View File

@ -0,0 +1,232 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Database } from '../database';
import { mockDatabase } from './index';
describe('targetKey', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase();
await db.clean({ drop: true });
});
afterEach(async () => {
await db.close();
});
test('default targetKey', async () => {
db.collection({
name: 'a1',
fields: [
{
type: 'hasMany',
name: 'b1',
target: 'b1',
},
],
});
db.collection({
name: 'b1',
fields: [],
});
await db.sync();
const r1 = db.getRepository('a1');
const r2 = db.getRepository('b1');
const b1 = await r2.create({
values: {},
});
await r1.create({
values: {
name: 'a1',
b1: [b1.toJSON()],
},
});
const b1r = await b1.reload();
expect(b1r.a1Id).toBe(b1.id);
});
test('targetKey=code', async () => {
db.collection({
name: 'a1',
fields: [
{
type: 'hasMany',
name: 'b1',
target: 'b1',
targetKey: 'code',
},
],
});
db.collection({
name: 'b1',
fields: [
{
type: 'string',
name: 'code',
},
],
});
await db.sync();
const r1 = db.getRepository('a1');
const r2 = db.getRepository('b1');
const b1 = await r2.create({
values: {},
});
await r1.create({
values: {
name: 'a1',
b1: [b1.toJSON()],
},
});
const b1r = await b1.reload();
expect(b1r.a1Id).toBe(b1.id);
});
test('should throw an error', async () => {
db.collection({
name: 'a1',
fields: [
{
type: 'hasMany',
name: 'b1',
target: 'b1',
targetKey: 'code',
},
],
});
db.collection({
name: 'b1',
fields: [
{
type: 'string',
name: 'code',
unique: true,
},
],
});
await db.sync();
const r1 = db.getRepository('a1');
const r2 = db.getRepository('b1');
const b1 = await r2.create({
values: {},
});
await expect(async () => {
await r1.create({
values: {
name: 'a1',
b1: [b1.toJSON()],
},
});
}).rejects.toThrowError('code field value is empty');
});
test('should find by code', async () => {
db.collection({
name: 'a1',
fields: [
{
type: 'hasMany',
name: 'b1',
target: 'b1',
targetKey: 'code',
},
],
});
db.collection({
name: 'b1',
fields: [
{
type: 'string',
name: 'code',
unique: true,
},
],
});
await db.sync();
const r1 = db.getRepository('a1');
const r2 = db.getRepository('b1');
const b1 = await r2.create({
values: {
code: 'code1',
},
});
await r1.create({
values: {
name: 'a1',
b1: [b1.toJSON()],
},
});
const b1r = await b1.reload();
expect(b1r.a1Id).toBe(b1.id);
});
test('should find by a1Code and code', async () => {
db.collection({
name: 'a1',
fields: [
{
type: 'string',
name: 'code',
unique: true,
},
{
type: 'hasMany',
name: 'b1',
target: 'b1',
sourceKey: 'code',
foreignKey: 'a1Code',
targetKey: 'code',
},
],
});
db.collection({
name: 'b1',
indexes: [
{
type: 'UNIQUE',
fields: ['a1Code', 'code'],
},
],
fields: [
{
type: 'string',
name: 'a1Code',
},
{
type: 'string',
name: 'code',
},
],
});
await db.sync();
const r1 = db.getRepository('a1');
const r2 = db.getRepository('b1');
await r2.create({
values: {
code: 'b1',
},
});
const b1 = await r2.create({
values: {
code: 'b1',
},
});
await r1.create({
values: {
code: 'a1',
b1: [b1.toJSON()],
},
});
const b1r = await b1.reload();
expect(b1r.a1Code).toBe('a1');
expect(b1r.code).toBe('b1');
});
});

View File

@ -48,6 +48,8 @@ function EnsureAtomicity(target: any, propertyKey: string, descriptor: PropertyD
const model = this.model;
const beforeAssociationKeys = Object.keys(model.associations);
const beforeRawAttributes = Object.keys(model.rawAttributes);
const fieldName = args[0];
const beforeField = this.getField(fieldName);
try {
return originalMethod.apply(this, args);
@ -64,6 +66,12 @@ function EnsureAtomicity(target: any, propertyKey: string, descriptor: PropertyD
for (const key of createdRawAttributes) {
delete this.model.rawAttributes[key];
}
// remove field created in this method
if (!beforeField) {
this.removeField(fieldName);
}
throw error;
}
};

View File

@ -490,6 +490,10 @@ export async function updateMultipleAssociation(
accessorOptions['through'] = throughValue;
}
if (pk !== targetKey && !isUndefinedOrNull(item[pk]) && isUndefinedOrNull(item[targetKey])) {
throw new Error(`${targetKey} field value is empty`);
}
if (isUndefinedOrNull(item[targetKey])) {
// create new record
const instance = await model[createAccessor](item, accessorOptions);

View File

@ -157,8 +157,8 @@ export class UpdateGuard {
return value;
}
const associationKeyName = (<any>associationObj).targetKey
? (<any>associationObj).targetKey
const associationKeyName = associationObj?.['options']?.targetKey
? associationObj['options'].targetKey
: associationObj.target.primaryKeyAttribute;
if (value[associationKeyName]) {

View File

@ -36,6 +36,14 @@ abstract class BaseClient<Client> {
await this._createDB(name);
this.createdDBs.add(name);
// remove db after 3 minutes
setTimeout(
async () => {
await this.removeDB(name);
},
3 * 60 * 1000,
);
}
async releaseAll() {
@ -51,6 +59,16 @@ abstract class BaseClient<Client> {
this.createdDBs.delete(name);
}
}
async removeDB(name: string) {
if (!this._client) {
return;
}
if (this.createdDBs.has(name)) {
await this._removeDB(name);
this.createdDBs.delete(name);
}
}
}
class PostgresClient extends BaseClient<pg.Client> {
@ -156,8 +174,9 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ error }));
});
} else if (trimmedPath === 'release') {
const name = parsedUrl.query.name as string | undefined;
dbClient
.releaseAll()
.removeDB(name)
.then(() => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end();

View File

@ -0,0 +1,91 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Database, Field, Repository } from '@nocobase/database';
import { Application } from '@nocobase/server';
import { createApp } from '.';
class MockField extends Field {
get dataType() {
return 'mock';
}
bind() {
throw new Error('MockField not implemented.');
}
}
describe('load field', async () => {
let db: Database;
let app: Application;
let collectionRepository: Repository;
let fieldsRepository: Repository;
beforeEach(async () => {
app = await createApp({
database: {
tablePrefix: '',
},
});
db = app.db;
db.registerFieldTypes({
mock: MockField,
});
collectionRepository = db.getCollection('collections').repository;
fieldsRepository = db.getCollection('fields').repository;
});
afterEach(async () => {
await app.destroy();
});
it('should not in collection when binding error', async () => {
const collection = await collectionRepository.create({
values: {
name: 'test1',
fields: [
{
type: 'bigInt',
name: 'id',
},
],
},
});
await collection.load();
expect(db.hasCollection('test1')).toBeTruthy();
try {
await db.sequelize.transaction(async (transaction) => {
const field = await fieldsRepository.create({
values: {
name: 'mock',
collectionName: 'test1',
type: 'mock',
},
transaction,
});
await field.load({ transaction });
});
} catch (error) {
expect(error.message).toBe('MockField not implemented.');
}
const instance = await fieldsRepository.findOne({
filter: {
name: 'mock',
},
});
expect(instance).toBeFalsy();
const field = db.getCollection('test1').getField('mock');
expect(field).toBeUndefined();
});
});