feat: reference check (#989)
* chore: test * chore: test * chore: test code * feat: on delete restrict * feat: on delete cascade * feat: on delete set null * feat: reference unbind * fix: test * fix: acl test * fix: test on Windows * fix: database recreate * fix: application reload * fix: multi-app-manager test * fix: test * feat: ondelete * fix: hasOne field onDelete Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
67f3c84d27
commit
9f5f2d6028
@ -6,16 +6,10 @@ import bodyParser from 'koa-bodyparser';
|
|||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import supertest, { SuperAgentTest } from 'supertest';
|
import supertest, { SuperAgentTest } from 'supertest';
|
||||||
import db2resource from '../../../server/src/middlewares/db2resource';
|
import db2resource from '../../../server/src/middlewares/db2resource';
|
||||||
|
import { uid } from '@nocobase/utils';
|
||||||
|
|
||||||
export function generatePrefixByPath() {
|
export function generatePrefixByPath() {
|
||||||
const { id } = require.main;
|
return `mock_${uid(6)}`;
|
||||||
const key = id
|
|
||||||
.replace(`${process.env.PWD}/packages`, '')
|
|
||||||
.replace(/src\/__tests__/g, '')
|
|
||||||
.replace('.test.ts', '')
|
|
||||||
.replace(/[^\w]/g, '_')
|
|
||||||
.replace(/_+/g, '_');
|
|
||||||
return key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfig(config = {}, options?: any): DatabaseOptions {
|
export function getConfig(config = {}, options?: any): DatabaseOptions {
|
||||||
|
@ -14,6 +14,5 @@ export async function create(ctx: Context, next) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.body = instance;
|
ctx.body = instance;
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
|
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
export const m2o: IField = {
|
export const m2o: IField = {
|
||||||
@ -203,6 +203,7 @@ export const m2o: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...constraintsProps,
|
||||||
...reverseFieldProperties,
|
...reverseFieldProperties,
|
||||||
},
|
},
|
||||||
filterable: {
|
filterable: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
|
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
export const o2m: IField = {
|
export const o2m: IField = {
|
||||||
@ -240,6 +240,7 @@ export const o2m: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...constraintsProps,
|
||||||
...reverseFieldProperties,
|
...reverseFieldProperties,
|
||||||
},
|
},
|
||||||
filterable: {
|
filterable: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
|
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
|
||||||
import { IField } from './types';
|
import { IField } from './types';
|
||||||
|
|
||||||
const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => {
|
const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => {
|
||||||
@ -382,6 +382,7 @@ export const oho: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...constraintsProps,
|
||||||
...reverseFieldProperties,
|
...reverseFieldProperties,
|
||||||
},
|
},
|
||||||
filterable: {
|
filterable: {
|
||||||
@ -548,6 +549,7 @@ export const obo: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...constraintsProps,
|
||||||
...reverseFieldProperties,
|
...reverseFieldProperties,
|
||||||
},
|
},
|
||||||
filterable: {
|
filterable: {
|
||||||
|
@ -53,6 +53,23 @@ export const relationshipType: ISchema = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const constraintsProps = {
|
||||||
|
onDelete: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("ON DELETE")}}',
|
||||||
|
required: true,
|
||||||
|
default: 'SET NULL',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: [
|
||||||
|
{ label: "{{t('SET NULL')}}", value: 'SET NULL' },
|
||||||
|
{ label: "{{t('RESTRICT')}}", value: 'RESTRICT' },
|
||||||
|
{ label: "{{t('CASCADE')}}", value: 'CASCADE' },
|
||||||
|
{ label: "{{t('NO ACTION')}}", value: 'NO ACTION' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const reverseFieldProperties: Record<string, ISchema> = {
|
export const reverseFieldProperties: Record<string, ISchema> = {
|
||||||
reverse: {
|
reverse: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
|
@ -64,10 +64,13 @@ describe('belongs to field', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('custom targetKey and foreignKey', async () => {
|
it('custom targetKey and foreignKey', async () => {
|
||||||
const Post = db.collection({
|
db.collection({
|
||||||
name: 'posts',
|
name: "posts",
|
||||||
fields: [{ type: 'string', name: 'key', unique: true }],
|
fields: [
|
||||||
|
{ type: "string", name: "key" },
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const Comment = db.collection({
|
const Comment = db.collection({
|
||||||
name: 'comments',
|
name: 'comments',
|
||||||
fields: [
|
fields: [
|
||||||
@ -79,6 +82,7 @@ describe('belongs to field', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const association = Comment.model.associations.post;
|
const association = Comment.model.associations.post;
|
||||||
expect(association).toBeDefined();
|
expect(association).toBeDefined();
|
||||||
expect(association.foreignKey).toBe('postKey');
|
expect(association.foreignKey).toBe('postKey');
|
||||||
@ -99,7 +103,7 @@ describe('belongs to field', () => {
|
|||||||
let error;
|
let error;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const Comment = db.collection({
|
db.collection({
|
||||||
name: 'comments1',
|
name: 'comments1',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@ -114,6 +118,7 @@ describe('belongs to field', () => {
|
|||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(IdentifierError);
|
expect(error).toBeInstanceOf(IdentifierError);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -194,4 +199,107 @@ describe('belongs to field', () => {
|
|||||||
const association = Post.model.associations;
|
const association = Post.model.associations;
|
||||||
expect(association['comments']).toBeDefined();
|
expect(association['comments']).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('foreign constraints', () => {
|
||||||
|
it('should set null on delete', async () => {
|
||||||
|
const Product = db.collection({
|
||||||
|
name: 'products',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Order = db.collection({
|
||||||
|
name: 'order',
|
||||||
|
fields: [{ type: 'belongsTo', name: 'product', onDelete: 'SET NULL' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const p = await Product.repository.create({ values: { name: 'p1' } });
|
||||||
|
const o = await Order.repository.create({ values: { product: p.id } });
|
||||||
|
|
||||||
|
expect(o.productId).toBe(p.id);
|
||||||
|
|
||||||
|
await Product.repository.destroy({
|
||||||
|
filterByTk: p.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newO = await o.reload();
|
||||||
|
|
||||||
|
expect(newO.productId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete reference map item when field unbind', async () => {
|
||||||
|
const Product = db.collection({
|
||||||
|
name: 'products',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Order = db.collection({
|
||||||
|
name: 'order',
|
||||||
|
fields: [{ type: 'belongsTo', name: 'product', onDelete: 'CASCADE' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
Order.removeField('product');
|
||||||
|
|
||||||
|
expect(db.referenceMap.getReferences(Product.name)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete cascade', async () => {
|
||||||
|
const Product = db.collection({
|
||||||
|
name: 'products',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Order = db.collection({
|
||||||
|
name: 'order',
|
||||||
|
fields: [{ type: 'belongsTo', name: 'product', onDelete: 'CASCADE' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
const p = await Product.repository.create({ values: { name: 'p1' } });
|
||||||
|
await Order.repository.create({ values: { product: p.id } });
|
||||||
|
await Order.repository.create({ values: { product: p.id } });
|
||||||
|
|
||||||
|
expect(await Order.repository.count({ filter: { productId: p.id } })).toBe(2);
|
||||||
|
|
||||||
|
await Product.repository.destroy({
|
||||||
|
filterByTk: p.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await Order.repository.count({ filter: { productId: p.id } })).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete restrict', async () => {
|
||||||
|
const Product = db.collection({
|
||||||
|
name: 'products',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Order = db.collection({
|
||||||
|
name: 'order',
|
||||||
|
fields: [{ type: 'belongsTo', name: 'product', onDelete: 'RESTRICT' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const p = await Product.repository.create({ values: { name: 'p1' } });
|
||||||
|
const o = await Order.repository.create({ values: { product: p.id } });
|
||||||
|
|
||||||
|
expect(o.productId).toBe(p.id);
|
||||||
|
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Product.repository.destroy({
|
||||||
|
filterByTk: p.id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -171,4 +171,87 @@ describe('has many field', () => {
|
|||||||
|
|
||||||
expect(error).toBeInstanceOf(IdentifierError);
|
expect(error).toBeInstanceOf(IdentifierError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('foreign key constraint', function () {
|
||||||
|
it('should cascade delete', async () => {
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'hasMany', name: 'comments', onDelete: 'CASCADE' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Comment = db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{ type: 'belongsTo', name: 'post', onDelete: "CASCADE" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const post = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'post1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const comment = await Comment.repository.create({
|
||||||
|
values: {
|
||||||
|
content: 'comment1',
|
||||||
|
postId: post.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Post.repository.destroy({
|
||||||
|
filterByTk: post.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await Comment.repository.count()).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when foreign key constraint is violated', async function () {
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'hasMany', name: 'comments', onDelete: 'RESTRICT' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Comment = db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [{ type: 'string', name: 'content' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const post = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'post1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const comment = await Comment.repository.create({
|
||||||
|
values: {
|
||||||
|
content: 'comment1',
|
||||||
|
postId: post.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let error;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Post.repository.destroy({
|
||||||
|
filterByTk: post.id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -74,6 +74,8 @@ export class Collection<
|
|||||||
|
|
||||||
this.bindFieldEventListener();
|
this.bindFieldEventListener();
|
||||||
this.modelInit();
|
this.modelInit();
|
||||||
|
this.db.modelCollection.set(this.model, this);
|
||||||
|
|
||||||
this.setFields(options.fields);
|
this.setFields(options.fields);
|
||||||
this.setRepository(options.repository);
|
this.setRepository(options.repository);
|
||||||
this.setSortable(options.sortable);
|
this.setSortable(options.sortable);
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
Sequelize,
|
Sequelize,
|
||||||
SyncOptions,
|
SyncOptions,
|
||||||
Transactionable,
|
Transactionable,
|
||||||
Utils
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { SequelizeStorage, Umzug } from 'umzug';
|
import { SequelizeStorage, Umzug } from 'umzug';
|
||||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||||
@ -52,8 +52,10 @@ import {
|
|||||||
SyncListener,
|
SyncListener,
|
||||||
UpdateListener,
|
UpdateListener,
|
||||||
UpdateWithAssociationsListener,
|
UpdateWithAssociationsListener,
|
||||||
ValidateListener
|
ValidateListener,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
||||||
|
import ReferencesMap from './features/ReferencesMap';
|
||||||
|
|
||||||
export interface MergeOptions extends merge.Options {}
|
export interface MergeOptions extends merge.Options {}
|
||||||
|
|
||||||
@ -146,6 +148,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
collections = new Map<string, Collection>();
|
collections = new Map<string, Collection>();
|
||||||
pendingFields = new Map<string, RelationField[]>();
|
pendingFields = new Map<string, RelationField[]>();
|
||||||
modelCollection = new Map<ModelCtor<any>, Collection>();
|
modelCollection = new Map<ModelCtor<any>, Collection>();
|
||||||
|
referenceMap = new ReferencesMap();
|
||||||
|
|
||||||
modelHook: ModelHook;
|
modelHook: ModelHook;
|
||||||
version: DatabaseVersion;
|
version: DatabaseVersion;
|
||||||
@ -235,6 +238,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.initListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
initListener() {
|
||||||
this.on('afterCreate', async (instance) => {
|
this.on('afterCreate', async (instance) => {
|
||||||
instance?.toChangedWithAssociations?.();
|
instance?.toChangedWithAssociations?.();
|
||||||
});
|
});
|
||||||
@ -242,6 +249,14 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
this.on('afterUpdate', async (instance) => {
|
this.on('afterUpdate', async (instance) => {
|
||||||
instance?.toChangedWithAssociations?.();
|
instance?.toChangedWithAssociations?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.on('beforeDestroy', async (instance, options) => {
|
||||||
|
await referentialIntegrityCheck({
|
||||||
|
db: this,
|
||||||
|
referencedInstance: instance,
|
||||||
|
transaction: options.transaction,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addMigration(item: MigrationItem) {
|
addMigration(item: MigrationItem) {
|
||||||
@ -283,7 +298,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.collections.set(collection.name, collection);
|
this.collections.set(collection.name, collection);
|
||||||
this.modelCollection.set(collection.model, collection);
|
|
||||||
|
|
||||||
this.emit('afterDefineCollection', collection);
|
this.emit('afterDefineCollection', collection);
|
||||||
|
|
||||||
@ -497,7 +511,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
on(event: ModelSaveWithAssociationsEventTypes, listener: SaveWithAssociationsListener): this;
|
on(event: ModelSaveWithAssociationsEventTypes, listener: SaveWithAssociationsListener): this;
|
||||||
on(event: DatabaseBeforeDefineCollectionEventType, listener: BeforeDefineCollectionListener): this;
|
on(event: DatabaseBeforeDefineCollectionEventType, listener: BeforeDefineCollectionListener): this;
|
||||||
on(event: DatabaseAfterDefineCollectionEventType, listener: AfterDefineCollectionListener): this;
|
on(event: DatabaseAfterDefineCollectionEventType, listener: AfterDefineCollectionListener): this;
|
||||||
on(event: DatabaseBeforeRemoveCollectionEventType | DatabaseAfterRemoveCollectionEventType, listener: RemoveCollectionListener): this;
|
on(
|
||||||
|
event: DatabaseBeforeRemoveCollectionEventType | DatabaseAfterRemoveCollectionEventType,
|
||||||
|
listener: RemoveCollectionListener,
|
||||||
|
): this;
|
||||||
on(event: EventType, listener: any): this {
|
on(event: EventType, listener: any): this {
|
||||||
// NOTE: to match if event is a sequelize or model type
|
// NOTE: to match if event is a sequelize or model type
|
||||||
const type = this.modelHook.match(event);
|
const type = this.modelHook.match(event);
|
||||||
|
64
packages/core/database/src/features/ReferencesMap.ts
Normal file
64
packages/core/database/src/features/ReferencesMap.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
export interface Reference {
|
||||||
|
sourceCollectionName: string;
|
||||||
|
sourceField: string;
|
||||||
|
targetField: string;
|
||||||
|
targetCollectionName: string;
|
||||||
|
onDelete: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReferencesMap {
|
||||||
|
protected map: Map<string, Reference[]> = new Map();
|
||||||
|
|
||||||
|
addReference(reference: Reference) {
|
||||||
|
const existReference = this.existReference(reference);
|
||||||
|
|
||||||
|
if (existReference) {
|
||||||
|
if (reference.onDelete && existReference.onDelete !== reference.onDelete) {
|
||||||
|
throw new Error(
|
||||||
|
`On Delete Conflict, exist reference ${JSON.stringify(existReference)}, new reference ${JSON.stringify(
|
||||||
|
reference,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reference.onDelete) {
|
||||||
|
reference.onDelete = 'SET NULL';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map.set(reference.targetCollectionName, [...(this.map.get(reference.targetCollectionName) || []), reference]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getReferences(collectionName) {
|
||||||
|
return this.map.get(collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
existReference(reference: Reference) {
|
||||||
|
const references = this.map.get(reference.targetCollectionName);
|
||||||
|
|
||||||
|
if (!references) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(reference).filter((k) => k !== 'onDelete');
|
||||||
|
|
||||||
|
return references.find((ref) => keys.every((key) => ref[key] === reference[key]));
|
||||||
|
}
|
||||||
|
|
||||||
|
removeReference(reference: Reference) {
|
||||||
|
const references = this.map.get(reference.targetCollectionName);
|
||||||
|
if (!references) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const keys = Object.keys(reference);
|
||||||
|
|
||||||
|
this.map.set(
|
||||||
|
reference.targetCollectionName,
|
||||||
|
references.filter((ref) => !keys.every((key) => ref[key] === reference[key])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReferencesMap;
|
@ -0,0 +1,61 @@
|
|||||||
|
import Database from '../database';
|
||||||
|
import { Model, Transactionable } from 'sequelize';
|
||||||
|
|
||||||
|
interface ReferentialIntegrityCheckOptions extends Transactionable {
|
||||||
|
db: Database;
|
||||||
|
referencedInstance: Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function referentialIntegrityCheck(options: ReferentialIntegrityCheckOptions) {
|
||||||
|
const { referencedInstance, db, transaction } = options;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const collection = db.modelCollection.get(referencedInstance.constructor);
|
||||||
|
|
||||||
|
const collectionName = collection.name;
|
||||||
|
const references = db.referenceMap.getReferences(collectionName);
|
||||||
|
|
||||||
|
if (!references) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const reference of references) {
|
||||||
|
const { sourceCollectionName, sourceField, targetField, onDelete } = reference;
|
||||||
|
const sourceCollection = db.collections.get(sourceCollectionName);
|
||||||
|
const sourceRepository = sourceCollection.repository;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
[sourceField]: referencedInstance[targetField],
|
||||||
|
};
|
||||||
|
const referencingExists = await sourceRepository.count({
|
||||||
|
filter,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!referencingExists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDelete === 'RESTRICT') {
|
||||||
|
throw new Error('RESTRICT');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDelete === 'CASCADE') {
|
||||||
|
await sourceRepository.destroy({
|
||||||
|
filter: filter,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDelete === 'SET NULL') {
|
||||||
|
await sourceRepository.update({
|
||||||
|
filter,
|
||||||
|
values: {
|
||||||
|
[sourceField]: null,
|
||||||
|
},
|
||||||
|
hooks: false,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { omit } from 'lodash';
|
|||||||
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
|
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
import { Reference } from '../features/ReferencesMap';
|
||||||
|
|
||||||
export class BelongsToField extends RelationField {
|
export class BelongsToField extends RelationField {
|
||||||
static type = 'belongsTo';
|
static type = 'belongsTo';
|
||||||
@ -11,6 +12,18 @@ export class BelongsToField extends RelationField {
|
|||||||
return target || Utils.pluralize(name);
|
return target || Utils.pluralize(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reference(association): Reference {
|
||||||
|
const targetKey = association.targetKey;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceCollectionName: this.database.modelCollection.get(association.source).name,
|
||||||
|
sourceField: association.foreignKey,
|
||||||
|
targetField: targetKey,
|
||||||
|
targetCollectionName: this.database.modelCollection.get(association.target).name,
|
||||||
|
onDelete: this.options.onDelete,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
bind() {
|
bind() {
|
||||||
const { database, collection } = this.context;
|
const { database, collection } = this.context;
|
||||||
const Target = this.TargetModel;
|
const Target = this.TargetModel;
|
||||||
@ -30,7 +43,7 @@ export class BelongsToField extends RelationField {
|
|||||||
const association = collection.model.belongsTo(Target, {
|
const association = collection.model.belongsTo(Target, {
|
||||||
as: this.name,
|
as: this.name,
|
||||||
constraints: false,
|
constraints: false,
|
||||||
...omit(this.options, ['name', 'type', 'target']),
|
...omit(this.options, ['name', 'type', 'target', 'onDelete']),
|
||||||
});
|
});
|
||||||
|
|
||||||
// inverse relation
|
// inverse relation
|
||||||
@ -56,6 +69,9 @@ export class BelongsToField extends RelationField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.collection.addIndex([this.options.foreignKey]);
|
this.collection.addIndex([this.options.foreignKey]);
|
||||||
|
|
||||||
|
this.database.referenceMap.addReference(this.reference(association));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +89,10 @@ export class BelongsToField extends RelationField {
|
|||||||
if (!field1 && !field2) {
|
if (!field1 && !field2) {
|
||||||
collection.model.removeAttribute(foreignKey);
|
collection.model.removeAttribute(foreignKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const association = collection.model.associations[this.name];
|
||||||
|
this.database.referenceMap.removeReference(this.reference(association));
|
||||||
|
|
||||||
// 删掉 model 的关联字段
|
// 删掉 model 的关联字段
|
||||||
delete collection.model.associations[this.name];
|
delete collection.model.associations[this.name];
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -5,11 +5,12 @@ import {
|
|||||||
ForeignKeyOptions,
|
ForeignKeyOptions,
|
||||||
HasManyOptions,
|
HasManyOptions,
|
||||||
HasManyOptions as SequelizeHasManyOptions,
|
HasManyOptions as SequelizeHasManyOptions,
|
||||||
Utils
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
import { Reference } from '../features/ReferencesMap';
|
||||||
|
|
||||||
export interface HasManyFieldOptions extends HasManyOptions {
|
export interface HasManyFieldOptions extends HasManyOptions {
|
||||||
/**
|
/**
|
||||||
@ -81,6 +82,18 @@ export class HasManyField extends RelationField {
|
|||||||
return Utils.camelize([model.options.name.singular, this.sourceKey || model.primaryKeyAttribute].join('_'));
|
return Utils.camelize([model.options.name.singular, this.sourceKey || model.primaryKeyAttribute].join('_'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reference(association): Reference {
|
||||||
|
const sourceKey = association.sourceKey;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceCollectionName: this.database.modelCollection.get(association.target).name,
|
||||||
|
sourceField: association.foreignKey,
|
||||||
|
targetField: sourceKey,
|
||||||
|
targetCollectionName: this.database.modelCollection.get(association.source).name,
|
||||||
|
onDelete: this.options.onDelete,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
bind() {
|
bind() {
|
||||||
const { database, collection } = this.context;
|
const { database, collection } = this.context;
|
||||||
const Target = this.TargetModel;
|
const Target = this.TargetModel;
|
||||||
@ -95,7 +108,7 @@ export class HasManyField extends RelationField {
|
|||||||
|
|
||||||
const association = collection.model.hasMany(Target, {
|
const association = collection.model.hasMany(Target, {
|
||||||
constraints: false,
|
constraints: false,
|
||||||
...omit(this.options, ['name', 'type', 'target']),
|
...omit(this.options, ['name', 'type', 'target', 'onDelete']),
|
||||||
as: this.name,
|
as: this.name,
|
||||||
foreignKey: this.foreignKey,
|
foreignKey: this.foreignKey,
|
||||||
});
|
});
|
||||||
@ -130,6 +143,9 @@ export class HasManyField extends RelationField {
|
|||||||
if (tcoll) {
|
if (tcoll) {
|
||||||
tcoll.addIndex([this.options.foreignKey]);
|
tcoll.addIndex([this.options.foreignKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.database.referenceMap.addReference(this.reference(association));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +165,13 @@ export class HasManyField extends RelationField {
|
|||||||
if (!field) {
|
if (!field) {
|
||||||
tcoll.model.removeAttribute(foreignKey);
|
tcoll.model.removeAttribute(foreignKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const association = collection.model.associations[this.name];
|
||||||
|
|
||||||
|
if (association) {
|
||||||
|
this.database.referenceMap.removeReference(this.reference(association));
|
||||||
|
}
|
||||||
|
|
||||||
// 删掉 model 的关联字段
|
// 删掉 model 的关联字段
|
||||||
delete collection.model.associations[this.name];
|
delete collection.model.associations[this.name];
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -5,11 +5,12 @@ import {
|
|||||||
ForeignKeyOptions,
|
ForeignKeyOptions,
|
||||||
HasOneOptions,
|
HasOneOptions,
|
||||||
HasOneOptions as SequelizeHasOneOptions,
|
HasOneOptions as SequelizeHasOneOptions,
|
||||||
Utils
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
import { Reference } from '../features/ReferencesMap';
|
||||||
|
|
||||||
export interface HasOneFieldOptions extends HasOneOptions {
|
export interface HasOneFieldOptions extends HasOneOptions {
|
||||||
/**
|
/**
|
||||||
@ -86,9 +87,22 @@ export class HasOneField extends RelationField {
|
|||||||
return Utils.camelize([model.options.name.singular, model.primaryKeyAttribute].join('_'));
|
return Utils.camelize([model.options.name.singular, model.primaryKeyAttribute].join('_'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reference(association): Reference {
|
||||||
|
const sourceKey = association.sourceKey;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceCollectionName: this.database.modelCollection.get(association.target).name,
|
||||||
|
sourceField: association.foreignKey,
|
||||||
|
targetField: sourceKey,
|
||||||
|
targetCollectionName: this.database.modelCollection.get(association.source).name,
|
||||||
|
onDelete: this.options.onDelete,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
bind() {
|
bind() {
|
||||||
const { database, collection } = this.context;
|
const { database, collection } = this.context;
|
||||||
const Target = this.TargetModel;
|
const Target = this.TargetModel;
|
||||||
|
|
||||||
if (!Target) {
|
if (!Target) {
|
||||||
database.addPendingField(this);
|
database.addPendingField(this);
|
||||||
return false;
|
return false;
|
||||||
@ -96,7 +110,7 @@ export class HasOneField extends RelationField {
|
|||||||
|
|
||||||
const association = collection.model.hasOne(Target, {
|
const association = collection.model.hasOne(Target, {
|
||||||
constraints: false,
|
constraints: false,
|
||||||
...omit(this.options, ['name', 'type', 'target']),
|
...omit(this.options, ['name', 'type', 'target', 'onDelete']),
|
||||||
as: this.name,
|
as: this.name,
|
||||||
foreignKey: this.foreignKey,
|
foreignKey: this.foreignKey,
|
||||||
});
|
});
|
||||||
@ -129,6 +143,8 @@ export class HasOneField extends RelationField {
|
|||||||
if (tcoll) {
|
if (tcoll) {
|
||||||
tcoll.addIndex([this.options.foreignKey]);
|
tcoll.addIndex([this.options.foreignKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.database.referenceMap.addReference(this.reference(association));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +164,10 @@ export class HasOneField extends RelationField {
|
|||||||
if (!field) {
|
if (!field) {
|
||||||
tcoll.model.removeAttribute(foreignKey);
|
tcoll.model.removeAttribute(foreignKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const association = collection.model.associations[this.name];
|
||||||
|
this.database.referenceMap.removeReference(this.reference(association));
|
||||||
|
|
||||||
// 删掉 model 的关联字段
|
// 删掉 model 的关联字段
|
||||||
delete collection.model.associations[this.name];
|
delete collection.model.associations[this.name];
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -338,6 +338,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
const guard = UpdateGuard.fromOptions(this.model, { ...options, action: 'create' });
|
const guard = UpdateGuard.fromOptions(this.model, { ...options, action: 'create' });
|
||||||
const values = guard.sanitize(options.values || {});
|
const values = guard.sanitize(options.values || {});
|
||||||
|
|
||||||
|
|
||||||
const instance = await this.model.create<any>(values, {
|
const instance = await this.model.create<any>(values, {
|
||||||
...options,
|
...options,
|
||||||
transaction,
|
transaction,
|
||||||
|
@ -210,11 +210,14 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
this.middleware = new Toposort<any>();
|
this.middleware = new Toposort<any>();
|
||||||
this.plugins = new Map<string, Plugin>();
|
this.plugins = new Map<string, Plugin>();
|
||||||
this._acl = createACL();
|
this._acl = createACL();
|
||||||
|
|
||||||
if (this._db) {
|
if (this._db) {
|
||||||
// MaxListenersExceededWarning
|
// MaxListenersExceededWarning
|
||||||
this._db.removeAllListeners();
|
this._db.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._db = this.createDatabase(options);
|
this._db = this.createDatabase(options);
|
||||||
|
|
||||||
this._resourcer = createResourcer(options);
|
this._resourcer = createResourcer(options);
|
||||||
this._cli = new Command('nocobase').usage('[command] [options]');
|
this._cli = new Command('nocobase').usage('[command] [options]');
|
||||||
this._i18n = createI18n(options);
|
this._i18n = createI18n(options);
|
||||||
@ -250,16 +253,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createDatabase(options: ApplicationOptions) {
|
private createDatabase(options: ApplicationOptions) {
|
||||||
if (options.database instanceof Database) {
|
return new Database({
|
||||||
return options.database;
|
...(options.database instanceof Database ? options.database.options : options.database),
|
||||||
} else {
|
migrator: {
|
||||||
return new Database({
|
context: { app: this },
|
||||||
...options.database,
|
},
|
||||||
migrator: {
|
});
|
||||||
context: { app: this },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
@ -315,8 +314,11 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
|
|
||||||
async load(options?: any) {
|
async load(options?: any) {
|
||||||
if (options?.reload) {
|
if (options?.reload) {
|
||||||
|
const oldDb = this._db;
|
||||||
this.init();
|
this.init();
|
||||||
|
await oldDb.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.emitAsync('beforeLoad', this, options);
|
await this.emitAsync('beforeLoad', this, options);
|
||||||
await this.pm.load(options);
|
await this.pm.load(options);
|
||||||
await this.emitAsync('afterLoad', this, options);
|
await this.emitAsync('afterLoad', this, options);
|
||||||
|
@ -11,6 +11,7 @@ describe('collections repository', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await createApp();
|
app = await createApp();
|
||||||
db = app.db;
|
db = app.db;
|
||||||
|
|
||||||
Collection = db.getCollection('collections');
|
Collection = db.getCollection('collections');
|
||||||
Field = db.getCollection('fields');
|
Field = db.getCollection('fields');
|
||||||
});
|
});
|
||||||
|
@ -57,12 +57,14 @@ export class CollectionModel extends MagicAttributeModel {
|
|||||||
if (!collection) {
|
if (!collection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = await this.db.getRepository('fields').find({
|
const fields = await this.db.getRepository('fields').find({
|
||||||
filter: {
|
filter: {
|
||||||
'type.$in': ['belongsToMany', 'belongsTo', 'hasMany', 'hasOne'],
|
'type.$in': ['belongsToMany', 'belongsTo', 'hasMany', 'hasOne'],
|
||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (field.get('target') && field.get('target') === name) {
|
if (field.get('target') && field.get('target') === name) {
|
||||||
await field.destroy({ transaction });
|
await field.destroy({ transaction });
|
||||||
|
@ -13,12 +13,12 @@ import {
|
|||||||
beforeCreateForChildrenCollection,
|
beforeCreateForChildrenCollection,
|
||||||
beforeCreateForReverseField,
|
beforeCreateForReverseField,
|
||||||
beforeDestroyForeignKey,
|
beforeDestroyForeignKey,
|
||||||
beforeInitOptions
|
beforeInitOptions,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
|
|
||||||
import { CollectionModel, FieldModel } from './models';
|
import { CollectionModel, FieldModel } from './models';
|
||||||
|
|
||||||
export class CollectionManagerPlugin extends Plugin {
|
export class CollectionManagerPlugin extends Plugin {
|
||||||
|
|
||||||
async beforeLoad() {
|
async beforeLoad() {
|
||||||
this.app.db.registerModels({
|
this.app.db.registerModels({
|
||||||
CollectionModel,
|
CollectionModel,
|
||||||
@ -145,42 +145,6 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// this.app.resourcer.use(async (ctx, next) => {
|
|
||||||
// const { resourceName, actionName } = ctx.action;
|
|
||||||
// if (actionName === 'update') {
|
|
||||||
// const { updateAssociationValues = [] } = ctx.action.params;
|
|
||||||
// const [collectionName, associationName] = resourceName.split('.');
|
|
||||||
// if (!associationName) {
|
|
||||||
// const collection: Collection = ctx.db.getCollection(collectionName);
|
|
||||||
// if (collection) {
|
|
||||||
// for (const [, field] of collection.fields) {
|
|
||||||
// if (['subTable', 'o2m'].includes(field.options.interface)) {
|
|
||||||
// updateAssociationValues.push(field.name);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// const association = ctx.db.getCollection(collectionName)?.getField?.(associationName);
|
|
||||||
// if (association?.target) {
|
|
||||||
// const collection: Collection = ctx.db.getCollection(association?.target);
|
|
||||||
// if (collection) {
|
|
||||||
// for (const [, field] of collection.fields) {
|
|
||||||
// if (['subTable', 'o2m'].includes(field.options.interface)) {
|
|
||||||
// updateAssociationValues.push(field.name);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (updateAssociationValues.length) {
|
|
||||||
// ctx.action.mergeParams({
|
|
||||||
// updateAssociationValues,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// await next();
|
|
||||||
// });
|
|
||||||
|
|
||||||
this.app.acl.allow('collections', 'list', 'loggedIn');
|
this.app.acl.allow('collections', 'list', 'loggedIn');
|
||||||
this.app.acl.allow('collections', ['create', 'update', 'destroy'], 'allowConfigure');
|
this.app.acl.allow('collections', ['create', 'update', 'destroy'], 'allowConfigure');
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import enUS from './locale/en_US';
|
|||||||
import zhCN from './locale/zh_CN';
|
import zhCN from './locale/zh_CN';
|
||||||
|
|
||||||
export class PluginErrorHandler extends Plugin {
|
export class PluginErrorHandler extends Plugin {
|
||||||
|
|
||||||
errorHandler: ErrorHandler = new ErrorHandler();
|
errorHandler: ErrorHandler = new ErrorHandler();
|
||||||
i18nNs: string = 'error-handler';
|
i18nNs: string = 'error-handler';
|
||||||
|
|
||||||
@ -46,6 +45,7 @@ export class PluginErrorHandler extends Plugin {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.app.i18n.addResources('zh-CN', this.i18nNs, zhCN);
|
this.app.i18n.addResources('zh-CN', this.i18nNs, zhCN);
|
||||||
this.app.i18n.addResources('en-US', this.i18nNs, enUS);
|
this.app.i18n.addResources('en-US', this.i18nNs, enUS);
|
||||||
|
@ -3,7 +3,6 @@ import path from 'path';
|
|||||||
import { getApp } from '.';
|
import { getApp } from '.';
|
||||||
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../constants';
|
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../constants';
|
||||||
|
|
||||||
|
|
||||||
const { LOCAL_STORAGE_BASE_URL, APP_PORT = '13000' } = process.env;
|
const { LOCAL_STORAGE_BASE_URL, APP_PORT = '13000' } = process.env;
|
||||||
|
|
||||||
const DEFAULT_LOCAL_BASE_URL = LOCAL_STORAGE_BASE_URL || `http://localhost:${APP_PORT}/uploads`;
|
const DEFAULT_LOCAL_BASE_URL = LOCAL_STORAGE_BASE_URL || `http://localhost:${APP_PORT}/uploads`;
|
||||||
@ -73,7 +72,7 @@ describe('action', () => {
|
|||||||
|
|
||||||
const { documentRoot = 'uploads' } = storage.options || {};
|
const { documentRoot = 'uploads' } = storage.options || {};
|
||||||
const destPath = path.resolve(
|
const destPath = path.resolve(
|
||||||
path.isAbsolute(documentRoot) ? documentRoot : path.join(process.env.PWD, documentRoot),
|
path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot),
|
||||||
storage.path,
|
storage.path,
|
||||||
);
|
);
|
||||||
const file = await fs.readFile(`${destPath}/${attachment.filename}`);
|
const file = await fs.readFile(`${destPath}/${attachment.filename}`);
|
||||||
|
@ -89,8 +89,11 @@ describe('test with start', () => {
|
|||||||
|
|
||||||
let app = mockServer();
|
let app = mockServer();
|
||||||
await app.cleanDb();
|
await app.cleanDb();
|
||||||
|
|
||||||
|
|
||||||
app.plugin(PluginMultiAppManager);
|
app.plugin(PluginMultiAppManager);
|
||||||
|
|
||||||
|
|
||||||
await app.loadAndInstall();
|
await app.loadAndInstall();
|
||||||
await app.start();
|
await app.start();
|
||||||
|
|
||||||
@ -139,6 +142,7 @@ describe('test with start', () => {
|
|||||||
|
|
||||||
await newApp.appManager.applications.get(name).destroy();
|
await newApp.appManager.applications.get(name).destroy();
|
||||||
|
|
||||||
|
await newApp.destroy();
|
||||||
await app.destroy();
|
await app.destroy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,11 +9,12 @@ export interface registerAppOptions extends Transactionable {
|
|||||||
|
|
||||||
export class ApplicationModel extends Model {
|
export class ApplicationModel extends Model {
|
||||||
static getDatabaseConfig(app: Application): IDatabaseOptions {
|
static getDatabaseConfig(app: Application): IDatabaseOptions {
|
||||||
return lodash.cloneDeep(
|
const oldConfig =
|
||||||
lodash.isPlainObject(app.options.database)
|
app.options.database instanceof Database
|
||||||
? (app.options.database as IDatabaseOptions)
|
? (app.options.database as Database).options
|
||||||
: (app.options.database as Database).options,
|
: (app.options.database as IDatabaseOptions);
|
||||||
);
|
|
||||||
|
return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator']));
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleAppStart(app: Application, options: registerAppOptions) {
|
static async handleAppStart(app: Application, options: registerAppOptions) {
|
||||||
|
Loading…
Reference in New Issue
Block a user