mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
Merge branch 'main' into next
This commit is contained in:
commit
ade9b70d51
2
.github/workflows/nocobase-test-backend.yml
vendored
2
.github/workflows/nocobase-test-backend.yml
vendored
@ -112,7 +112,7 @@ jobs:
|
|||||||
DB_SCHEMA: ${{ matrix.schema }}
|
DB_SCHEMA: ${{ matrix.schema }}
|
||||||
COLLECTION_MANAGER_SCHEMA: ${{ matrix.collection_schema }}
|
COLLECTION_MANAGER_SCHEMA: ${{ matrix.collection_schema }}
|
||||||
DB_TEST_DISTRIBUTOR_PORT: 23450
|
DB_TEST_DISTRIBUTOR_PORT: 23450
|
||||||
DB_TEST_PREFIX: test_
|
DB_TEST_PREFIX: test
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
|
||||||
mysql-test:
|
mysql-test:
|
||||||
|
@ -165,6 +165,24 @@ describe('example', () => {
|
|||||||
await app.destroy();
|
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 () => {
|
it('should register every datasource instance', async () => {
|
||||||
const hook = vi.fn();
|
const hook = vi.fn();
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ export class DataSourceManager {
|
|||||||
factory: DataSourceFactory = new DataSourceFactory();
|
factory: DataSourceFactory = new DataSourceFactory();
|
||||||
protected middlewares = [];
|
protected middlewares = [];
|
||||||
private onceHooks: Array<DataSourceHook> = [];
|
private onceHooks: Array<DataSourceHook> = [];
|
||||||
|
private beforeAddHooks: Array<DataSourceHook> = [];
|
||||||
|
|
||||||
constructor(public options = {}) {
|
constructor(public options = {}) {
|
||||||
this.dataSources = new Map();
|
this.dataSources = new Map();
|
||||||
@ -32,6 +33,10 @@ export class DataSourceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async add(dataSource: DataSource, options: any = {}) {
|
async add(dataSource: DataSource, options: any = {}) {
|
||||||
|
for (const hook of this.beforeAddHooks) {
|
||||||
|
hook(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
await dataSource.load(options);
|
await dataSource.load(options);
|
||||||
this.dataSources.set(dataSource.name, dataSource);
|
this.dataSources.set(dataSource.name, dataSource);
|
||||||
|
|
||||||
@ -71,6 +76,13 @@ export class DataSourceManager {
|
|||||||
return this.factory.create(type, options);
|
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) {
|
afterAddDataSource(hook: DataSourceHook) {
|
||||||
this.addHookAndRun(hook);
|
this.addHookAndRun(hook);
|
||||||
}
|
}
|
||||||
|
232
packages/core/database/src/__tests__/target-key.test.ts
Normal file
232
packages/core/database/src/__tests__/target-key.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
@ -48,6 +48,8 @@ function EnsureAtomicity(target: any, propertyKey: string, descriptor: PropertyD
|
|||||||
const model = this.model;
|
const model = this.model;
|
||||||
const beforeAssociationKeys = Object.keys(model.associations);
|
const beforeAssociationKeys = Object.keys(model.associations);
|
||||||
const beforeRawAttributes = Object.keys(model.rawAttributes);
|
const beforeRawAttributes = Object.keys(model.rawAttributes);
|
||||||
|
const fieldName = args[0];
|
||||||
|
const beforeField = this.getField(fieldName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return originalMethod.apply(this, args);
|
return originalMethod.apply(this, args);
|
||||||
@ -64,6 +66,12 @@ function EnsureAtomicity(target: any, propertyKey: string, descriptor: PropertyD
|
|||||||
for (const key of createdRawAttributes) {
|
for (const key of createdRawAttributes) {
|
||||||
delete this.model.rawAttributes[key];
|
delete this.model.rawAttributes[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove field created in this method
|
||||||
|
if (!beforeField) {
|
||||||
|
this.removeField(fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -490,6 +490,10 @@ export async function updateMultipleAssociation(
|
|||||||
accessorOptions['through'] = throughValue;
|
accessorOptions['through'] = throughValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pk !== targetKey && !isUndefinedOrNull(item[pk]) && isUndefinedOrNull(item[targetKey])) {
|
||||||
|
throw new Error(`${targetKey} field value is empty`);
|
||||||
|
}
|
||||||
|
|
||||||
if (isUndefinedOrNull(item[targetKey])) {
|
if (isUndefinedOrNull(item[targetKey])) {
|
||||||
// create new record
|
// create new record
|
||||||
const instance = await model[createAccessor](item, accessorOptions);
|
const instance = await model[createAccessor](item, accessorOptions);
|
||||||
|
@ -157,8 +157,8 @@ export class UpdateGuard {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const associationKeyName = (<any>associationObj).targetKey
|
const associationKeyName = associationObj?.['options']?.targetKey
|
||||||
? (<any>associationObj).targetKey
|
? associationObj['options'].targetKey
|
||||||
: associationObj.target.primaryKeyAttribute;
|
: associationObj.target.primaryKeyAttribute;
|
||||||
|
|
||||||
if (value[associationKeyName]) {
|
if (value[associationKeyName]) {
|
||||||
|
@ -36,6 +36,14 @@ abstract class BaseClient<Client> {
|
|||||||
|
|
||||||
await this._createDB(name);
|
await this._createDB(name);
|
||||||
this.createdDBs.add(name);
|
this.createdDBs.add(name);
|
||||||
|
|
||||||
|
// remove db after 3 minutes
|
||||||
|
setTimeout(
|
||||||
|
async () => {
|
||||||
|
await this.removeDB(name);
|
||||||
|
},
|
||||||
|
3 * 60 * 1000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async releaseAll() {
|
async releaseAll() {
|
||||||
@ -51,6 +59,16 @@ abstract class BaseClient<Client> {
|
|||||||
this.createdDBs.delete(name);
|
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> {
|
class PostgresClient extends BaseClient<pg.Client> {
|
||||||
@ -156,8 +174,9 @@ const server = http.createServer((req, res) => {
|
|||||||
res.end(JSON.stringify({ error }));
|
res.end(JSON.stringify({ error }));
|
||||||
});
|
});
|
||||||
} else if (trimmedPath === 'release') {
|
} else if (trimmedPath === 'release') {
|
||||||
|
const name = parsedUrl.query.name as string | undefined;
|
||||||
dbClient
|
dbClient
|
||||||
.releaseAll()
|
.removeDB(name)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end();
|
res.end();
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user