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 supertest, { SuperAgentTest } from 'supertest';
 | 
			
		||||
import db2resource from '../../../server/src/middlewares/db2resource';
 | 
			
		||||
import { uid } from '@nocobase/utils';
 | 
			
		||||
 | 
			
		||||
export function generatePrefixByPath() {
 | 
			
		||||
  const { id } = require.main;
 | 
			
		||||
  const key = id
 | 
			
		||||
    .replace(`${process.env.PWD}/packages`, '')
 | 
			
		||||
    .replace(/src\/__tests__/g, '')
 | 
			
		||||
    .replace('.test.ts', '')
 | 
			
		||||
    .replace(/[^\w]/g, '_')
 | 
			
		||||
    .replace(/_+/g, '_');
 | 
			
		||||
  return key;
 | 
			
		||||
  return `mock_${uid(6)}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getConfig(config = {}, options?: any): DatabaseOptions {
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,5 @@ export async function create(ctx: Context, next) {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  ctx.body = instance;
 | 
			
		||||
 | 
			
		||||
  await next();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ISchema } from '@formily/react';
 | 
			
		||||
import { cloneDeep } from 'lodash';
 | 
			
		||||
import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
 | 
			
		||||
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
 | 
			
		||||
import { IField } from './types';
 | 
			
		||||
 | 
			
		||||
export const m2o: IField = {
 | 
			
		||||
@ -203,6 +203,7 @@ export const m2o: IField = {
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    ...constraintsProps,
 | 
			
		||||
    ...reverseFieldProperties,
 | 
			
		||||
  },
 | 
			
		||||
  filterable: {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ISchema } from '@formily/react';
 | 
			
		||||
import { cloneDeep } from 'lodash';
 | 
			
		||||
import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
 | 
			
		||||
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
 | 
			
		||||
import { IField } from './types';
 | 
			
		||||
 | 
			
		||||
export const o2m: IField = {
 | 
			
		||||
@ -240,6 +240,7 @@ export const o2m: IField = {
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    ...constraintsProps,
 | 
			
		||||
    ...reverseFieldProperties,
 | 
			
		||||
  },
 | 
			
		||||
  filterable: {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ISchema } from '@formily/react';
 | 
			
		||||
import { cloneDeep } from 'lodash';
 | 
			
		||||
import { recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
 | 
			
		||||
import { constraintsProps, recordPickerSelector, recordPickerViewer, relationshipType, reverseFieldProperties } from './properties';
 | 
			
		||||
import { IField } from './types';
 | 
			
		||||
 | 
			
		||||
const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => {
 | 
			
		||||
@ -382,6 +382,7 @@ export const oho: IField = {
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    ...constraintsProps,
 | 
			
		||||
    ...reverseFieldProperties,
 | 
			
		||||
  },
 | 
			
		||||
  filterable: {
 | 
			
		||||
@ -548,6 +549,7 @@ export const obo: IField = {
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    ...constraintsProps,
 | 
			
		||||
    ...reverseFieldProperties,
 | 
			
		||||
  },
 | 
			
		||||
  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> = {
 | 
			
		||||
  reverse: {
 | 
			
		||||
    type: 'void',
 | 
			
		||||
 | 
			
		||||
@ -64,10 +64,13 @@ describe('belongs to field', () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('custom targetKey and foreignKey', async () => {
 | 
			
		||||
    const Post = db.collection({
 | 
			
		||||
      name: 'posts',
 | 
			
		||||
      fields: [{ type: 'string', name: 'key', unique: true }],
 | 
			
		||||
    db.collection({
 | 
			
		||||
      name: "posts",
 | 
			
		||||
      fields: [
 | 
			
		||||
        { type: "string", name: "key" },
 | 
			
		||||
      ]
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const Comment = db.collection({
 | 
			
		||||
      name: 'comments',
 | 
			
		||||
      fields: [
 | 
			
		||||
@ -79,6 +82,7 @@ describe('belongs to field', () => {
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const association = Comment.model.associations.post;
 | 
			
		||||
    expect(association).toBeDefined();
 | 
			
		||||
    expect(association.foreignKey).toBe('postKey');
 | 
			
		||||
@ -99,7 +103,7 @@ describe('belongs to field', () => {
 | 
			
		||||
    let error;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const Comment = db.collection({
 | 
			
		||||
      db.collection({
 | 
			
		||||
        name: 'comments1',
 | 
			
		||||
        fields: [
 | 
			
		||||
          {
 | 
			
		||||
@ -114,6 +118,7 @@ describe('belongs to field', () => {
 | 
			
		||||
      error = e;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    expect(error).toBeInstanceOf(IdentifierError);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -194,4 +199,107 @@ describe('belongs to field', () => {
 | 
			
		||||
    const association = Post.model.associations;
 | 
			
		||||
    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);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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.modelInit();
 | 
			
		||||
    this.db.modelCollection.set(this.model, this);
 | 
			
		||||
 | 
			
		||||
    this.setFields(options.fields);
 | 
			
		||||
    this.setRepository(options.repository);
 | 
			
		||||
    this.setSortable(options.sortable);
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ import {
 | 
			
		||||
  Sequelize,
 | 
			
		||||
  SyncOptions,
 | 
			
		||||
  Transactionable,
 | 
			
		||||
  Utils
 | 
			
		||||
  Utils,
 | 
			
		||||
} from 'sequelize';
 | 
			
		||||
import { SequelizeStorage, Umzug } from 'umzug';
 | 
			
		||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
 | 
			
		||||
@ -52,8 +52,10 @@ import {
 | 
			
		||||
  SyncListener,
 | 
			
		||||
  UpdateListener,
 | 
			
		||||
  UpdateWithAssociationsListener,
 | 
			
		||||
  ValidateListener
 | 
			
		||||
  ValidateListener,
 | 
			
		||||
} from './types';
 | 
			
		||||
import { referentialIntegrityCheck } from './features/referential-integrity-check';
 | 
			
		||||
import ReferencesMap from './features/ReferencesMap';
 | 
			
		||||
 | 
			
		||||
export interface MergeOptions extends merge.Options {}
 | 
			
		||||
 | 
			
		||||
@ -146,6 +148,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
  collections = new Map<string, Collection>();
 | 
			
		||||
  pendingFields = new Map<string, RelationField[]>();
 | 
			
		||||
  modelCollection = new Map<ModelCtor<any>, Collection>();
 | 
			
		||||
  referenceMap = new ReferencesMap();
 | 
			
		||||
 | 
			
		||||
  modelHook: ModelHook;
 | 
			
		||||
  version: DatabaseVersion;
 | 
			
		||||
@ -235,6 +238,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.initListener();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initListener() {
 | 
			
		||||
    this.on('afterCreate', async (instance) => {
 | 
			
		||||
      instance?.toChangedWithAssociations?.();
 | 
			
		||||
    });
 | 
			
		||||
@ -242,6 +249,14 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
    this.on('afterUpdate', async (instance) => {
 | 
			
		||||
      instance?.toChangedWithAssociations?.();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.on('beforeDestroy', async (instance, options) => {
 | 
			
		||||
      await referentialIntegrityCheck({
 | 
			
		||||
        db: this,
 | 
			
		||||
        referencedInstance: instance,
 | 
			
		||||
        transaction: options.transaction,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addMigration(item: MigrationItem) {
 | 
			
		||||
@ -283,7 +298,6 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.collections.set(collection.name, collection);
 | 
			
		||||
    this.modelCollection.set(collection.model, collection);
 | 
			
		||||
 | 
			
		||||
    this.emit('afterDefineCollection', collection);
 | 
			
		||||
 | 
			
		||||
@ -497,7 +511,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
 | 
			
		||||
  on(event: ModelSaveWithAssociationsEventTypes, listener: SaveWithAssociationsListener): this;
 | 
			
		||||
  on(event: DatabaseBeforeDefineCollectionEventType, listener: BeforeDefineCollectionListener): 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 {
 | 
			
		||||
    // NOTE: to match if event is a sequelize or model type
 | 
			
		||||
    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 { checkIdentifier } from '../utils';
 | 
			
		||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
 | 
			
		||||
import { Reference } from '../features/ReferencesMap';
 | 
			
		||||
 | 
			
		||||
export class BelongsToField extends RelationField {
 | 
			
		||||
  static type = 'belongsTo';
 | 
			
		||||
@ -11,6 +12,18 @@ export class BelongsToField extends RelationField {
 | 
			
		||||
    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() {
 | 
			
		||||
    const { database, collection } = this.context;
 | 
			
		||||
    const Target = this.TargetModel;
 | 
			
		||||
@ -30,7 +43,7 @@ export class BelongsToField extends RelationField {
 | 
			
		||||
    const association = collection.model.belongsTo(Target, {
 | 
			
		||||
      as: this.name,
 | 
			
		||||
      constraints: false,
 | 
			
		||||
      ...omit(this.options, ['name', 'type', 'target']),
 | 
			
		||||
      ...omit(this.options, ['name', 'type', 'target', 'onDelete']),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // inverse relation
 | 
			
		||||
@ -56,6 +69,9 @@ export class BelongsToField extends RelationField {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.collection.addIndex([this.options.foreignKey]);
 | 
			
		||||
 | 
			
		||||
    this.database.referenceMap.addReference(this.reference(association));
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -73,6 +89,10 @@ export class BelongsToField extends RelationField {
 | 
			
		||||
    if (!field1 && !field2) {
 | 
			
		||||
      collection.model.removeAttribute(foreignKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const association = collection.model.associations[this.name];
 | 
			
		||||
    this.database.referenceMap.removeReference(this.reference(association));
 | 
			
		||||
 | 
			
		||||
    // 删掉 model 的关联字段
 | 
			
		||||
    delete collection.model.associations[this.name];
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
 | 
			
		||||
@ -5,11 +5,12 @@ import {
 | 
			
		||||
  ForeignKeyOptions,
 | 
			
		||||
  HasManyOptions,
 | 
			
		||||
  HasManyOptions as SequelizeHasManyOptions,
 | 
			
		||||
  Utils
 | 
			
		||||
  Utils,
 | 
			
		||||
} from 'sequelize';
 | 
			
		||||
import { Collection } from '../collection';
 | 
			
		||||
import { checkIdentifier } from '../utils';
 | 
			
		||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
 | 
			
		||||
import { Reference } from '../features/ReferencesMap';
 | 
			
		||||
 | 
			
		||||
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('_'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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() {
 | 
			
		||||
    const { database, collection } = this.context;
 | 
			
		||||
    const Target = this.TargetModel;
 | 
			
		||||
@ -95,7 +108,7 @@ export class HasManyField extends RelationField {
 | 
			
		||||
 | 
			
		||||
    const association = collection.model.hasMany(Target, {
 | 
			
		||||
      constraints: false,
 | 
			
		||||
      ...omit(this.options, ['name', 'type', 'target']),
 | 
			
		||||
      ...omit(this.options, ['name', 'type', 'target', 'onDelete']),
 | 
			
		||||
      as: this.name,
 | 
			
		||||
      foreignKey: this.foreignKey,
 | 
			
		||||
    });
 | 
			
		||||
@ -130,6 +143,9 @@ export class HasManyField extends RelationField {
 | 
			
		||||
    if (tcoll) {
 | 
			
		||||
      tcoll.addIndex([this.options.foreignKey]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.database.referenceMap.addReference(this.reference(association));
 | 
			
		||||
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -149,6 +165,13 @@ export class HasManyField extends RelationField {
 | 
			
		||||
    if (!field) {
 | 
			
		||||
      tcoll.model.removeAttribute(foreignKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const association = collection.model.associations[this.name];
 | 
			
		||||
 | 
			
		||||
    if (association) {
 | 
			
		||||
      this.database.referenceMap.removeReference(this.reference(association));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 删掉 model 的关联字段
 | 
			
		||||
    delete collection.model.associations[this.name];
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
 | 
			
		||||
@ -5,11 +5,12 @@ import {
 | 
			
		||||
  ForeignKeyOptions,
 | 
			
		||||
  HasOneOptions,
 | 
			
		||||
  HasOneOptions as SequelizeHasOneOptions,
 | 
			
		||||
  Utils
 | 
			
		||||
  Utils,
 | 
			
		||||
} from 'sequelize';
 | 
			
		||||
import { Collection } from '../collection';
 | 
			
		||||
import { checkIdentifier } from '../utils';
 | 
			
		||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
 | 
			
		||||
import { Reference } from '../features/ReferencesMap';
 | 
			
		||||
 | 
			
		||||
export interface HasOneFieldOptions extends HasOneOptions {
 | 
			
		||||
  /**
 | 
			
		||||
@ -86,9 +87,22 @@ export class HasOneField extends RelationField {
 | 
			
		||||
    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() {
 | 
			
		||||
    const { database, collection } = this.context;
 | 
			
		||||
    const Target = this.TargetModel;
 | 
			
		||||
 | 
			
		||||
    if (!Target) {
 | 
			
		||||
      database.addPendingField(this);
 | 
			
		||||
      return false;
 | 
			
		||||
@ -96,7 +110,7 @@ export class HasOneField extends RelationField {
 | 
			
		||||
 | 
			
		||||
    const association = collection.model.hasOne(Target, {
 | 
			
		||||
      constraints: false,
 | 
			
		||||
      ...omit(this.options, ['name', 'type', 'target']),
 | 
			
		||||
      ...omit(this.options, ['name', 'type', 'target', 'onDelete']),
 | 
			
		||||
      as: this.name,
 | 
			
		||||
      foreignKey: this.foreignKey,
 | 
			
		||||
    });
 | 
			
		||||
@ -129,6 +143,8 @@ export class HasOneField extends RelationField {
 | 
			
		||||
    if (tcoll) {
 | 
			
		||||
      tcoll.addIndex([this.options.foreignKey]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.database.referenceMap.addReference(this.reference(association));
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -148,6 +164,10 @@ export class HasOneField extends RelationField {
 | 
			
		||||
    if (!field) {
 | 
			
		||||
      tcoll.model.removeAttribute(foreignKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const association = collection.model.associations[this.name];
 | 
			
		||||
    this.database.referenceMap.removeReference(this.reference(association));
 | 
			
		||||
 | 
			
		||||
    // 删掉 model 的关联字段
 | 
			
		||||
    delete collection.model.associations[this.name];
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
 | 
			
		||||
@ -338,6 +338,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
 | 
			
		||||
    const guard = UpdateGuard.fromOptions(this.model, { ...options, action: 'create' });
 | 
			
		||||
    const values = guard.sanitize(options.values || {});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    const instance = await this.model.create<any>(values, {
 | 
			
		||||
      ...options,
 | 
			
		||||
      transaction,
 | 
			
		||||
 | 
			
		||||
@ -210,11 +210,14 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
 | 
			
		||||
    this.middleware = new Toposort<any>();
 | 
			
		||||
    this.plugins = new Map<string, Plugin>();
 | 
			
		||||
    this._acl = createACL();
 | 
			
		||||
 | 
			
		||||
    if (this._db) {
 | 
			
		||||
      // MaxListenersExceededWarning
 | 
			
		||||
      this._db.removeAllListeners();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._db = this.createDatabase(options);
 | 
			
		||||
 | 
			
		||||
    this._resourcer = createResourcer(options);
 | 
			
		||||
    this._cli = new Command('nocobase').usage('[command] [options]');
 | 
			
		||||
    this._i18n = createI18n(options);
 | 
			
		||||
@ -250,16 +253,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private createDatabase(options: ApplicationOptions) {
 | 
			
		||||
    if (options.database instanceof Database) {
 | 
			
		||||
      return options.database;
 | 
			
		||||
    } else {
 | 
			
		||||
      return new Database({
 | 
			
		||||
        ...options.database,
 | 
			
		||||
        migrator: {
 | 
			
		||||
          context: { app: this },
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return new Database({
 | 
			
		||||
      ...(options.database instanceof Database ? options.database.options : options.database),
 | 
			
		||||
      migrator: {
 | 
			
		||||
        context: { app: this },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getVersion() {
 | 
			
		||||
@ -315,8 +314,11 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
 | 
			
		||||
 | 
			
		||||
  async load(options?: any) {
 | 
			
		||||
    if (options?.reload) {
 | 
			
		||||
      const oldDb = this._db;
 | 
			
		||||
      this.init();
 | 
			
		||||
      await oldDb.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await this.emitAsync('beforeLoad', this, options);
 | 
			
		||||
    await this.pm.load(options);
 | 
			
		||||
    await this.emitAsync('afterLoad', this, options);
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ describe('collections repository', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    app = await createApp();
 | 
			
		||||
    db = app.db;
 | 
			
		||||
 | 
			
		||||
    Collection = db.getCollection('collections');
 | 
			
		||||
    Field = db.getCollection('fields');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -57,12 +57,14 @@ export class CollectionModel extends MagicAttributeModel {
 | 
			
		||||
    if (!collection) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fields = await this.db.getRepository('fields').find({
 | 
			
		||||
      filter: {
 | 
			
		||||
        'type.$in': ['belongsToMany', 'belongsTo', 'hasMany', 'hasOne'],
 | 
			
		||||
      },
 | 
			
		||||
      transaction,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (const field of fields) {
 | 
			
		||||
      if (field.get('target') && field.get('target') === name) {
 | 
			
		||||
        await field.destroy({ transaction });
 | 
			
		||||
 | 
			
		||||
@ -13,12 +13,12 @@ import {
 | 
			
		||||
  beforeCreateForChildrenCollection,
 | 
			
		||||
  beforeCreateForReverseField,
 | 
			
		||||
  beforeDestroyForeignKey,
 | 
			
		||||
  beforeInitOptions
 | 
			
		||||
  beforeInitOptions,
 | 
			
		||||
} from './hooks';
 | 
			
		||||
 | 
			
		||||
import { CollectionModel, FieldModel } from './models';
 | 
			
		||||
 | 
			
		||||
export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
 | 
			
		||||
  async beforeLoad() {
 | 
			
		||||
    this.app.db.registerModels({
 | 
			
		||||
      CollectionModel,
 | 
			
		||||
@ -145,42 +145,6 @@ export class CollectionManagerPlugin extends Plugin {
 | 
			
		||||
      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', ['create', 'update', 'destroy'], 'allowConfigure');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ import enUS from './locale/en_US';
 | 
			
		||||
import zhCN from './locale/zh_CN';
 | 
			
		||||
 | 
			
		||||
export class PluginErrorHandler extends Plugin {
 | 
			
		||||
 | 
			
		||||
  errorHandler: ErrorHandler = new ErrorHandler();
 | 
			
		||||
  i18nNs: string = 'error-handler';
 | 
			
		||||
 | 
			
		||||
@ -46,6 +45,7 @@ export class PluginErrorHandler extends Plugin {
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async load() {
 | 
			
		||||
    this.app.i18n.addResources('zh-CN', this.i18nNs, zhCN);
 | 
			
		||||
    this.app.i18n.addResources('en-US', this.i18nNs, enUS);
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import path from 'path';
 | 
			
		||||
import { getApp } from '.';
 | 
			
		||||
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../constants';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const { LOCAL_STORAGE_BASE_URL, APP_PORT = '13000' } = process.env;
 | 
			
		||||
 | 
			
		||||
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 destPath = path.resolve(
 | 
			
		||||
        path.isAbsolute(documentRoot) ? documentRoot : path.join(process.env.PWD, documentRoot),
 | 
			
		||||
        path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot),
 | 
			
		||||
        storage.path,
 | 
			
		||||
      );
 | 
			
		||||
      const file = await fs.readFile(`${destPath}/${attachment.filename}`);
 | 
			
		||||
 | 
			
		||||
@ -89,8 +89,11 @@ describe('test with start', () => {
 | 
			
		||||
 | 
			
		||||
    let app = mockServer();
 | 
			
		||||
    await app.cleanDb();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    app.plugin(PluginMultiAppManager);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    await app.loadAndInstall();
 | 
			
		||||
    await app.start();
 | 
			
		||||
 | 
			
		||||
@ -139,6 +142,7 @@ describe('test with start', () => {
 | 
			
		||||
 | 
			
		||||
    await newApp.appManager.applications.get(name).destroy();
 | 
			
		||||
 | 
			
		||||
    await newApp.destroy();
 | 
			
		||||
    await app.destroy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -9,11 +9,12 @@ export interface registerAppOptions extends Transactionable {
 | 
			
		||||
 | 
			
		||||
export class ApplicationModel extends Model {
 | 
			
		||||
  static getDatabaseConfig(app: Application): IDatabaseOptions {
 | 
			
		||||
    return lodash.cloneDeep(
 | 
			
		||||
      lodash.isPlainObject(app.options.database)
 | 
			
		||||
        ? (app.options.database as IDatabaseOptions)
 | 
			
		||||
        : (app.options.database as Database).options,
 | 
			
		||||
    );
 | 
			
		||||
    const oldConfig =
 | 
			
		||||
      app.options.database instanceof Database
 | 
			
		||||
        ? (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) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user