chore: set default association reference on delete action to no action (#3722)
* chore: tmp commit * chore: build association reference * fix: test
This commit is contained in:
parent
fbed0201aa
commit
5ee278557d
@ -0,0 +1,52 @@
|
||||
import { Database } from '../../database';
|
||||
import { mockDatabase } from '../index';
|
||||
describe('association references', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
|
||||
await db.clean({ drop: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should add reference with default priority', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [{ type: 'hasOne', name: 'profile' }],
|
||||
});
|
||||
|
||||
const Profile = db.collection({
|
||||
name: 'profiles',
|
||||
fields: [{ type: 'belongsTo', name: 'user' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const references = db.referenceMap.getReferences('users');
|
||||
|
||||
expect(references[0].priority).toBe('default');
|
||||
});
|
||||
|
||||
it('should add reference with user defined priority', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [{ type: 'hasOne', name: 'profile', onDelete: 'CASCADE' }],
|
||||
});
|
||||
|
||||
const Profile = db.collection({
|
||||
name: 'profiles',
|
||||
fields: [{ type: 'belongsTo', name: 'user' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const references = db.referenceMap.getReferences('users');
|
||||
|
||||
expect(references.length).toBe(1);
|
||||
expect(references[0].priority).toBe('user');
|
||||
});
|
||||
});
|
@ -14,6 +14,36 @@ describe('belongs to field', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should load with no action', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [{ type: 'string', name: 'name', unique: true }],
|
||||
});
|
||||
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'belongsTo', name: 'user', onDelete: 'NO ACTION' },
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const u1 = await User.repository.create({ values: { name: 'u1' } });
|
||||
const p1 = await Post.repository.create({ values: { title: 'p1', user: u1.id } });
|
||||
|
||||
// delete u1
|
||||
await User.repository.destroy({ filterByTk: u1.id });
|
||||
|
||||
// list posts with user
|
||||
const post = await Post.repository.findOne({
|
||||
appends: ['user'],
|
||||
});
|
||||
|
||||
expect(post.user).toBeNull();
|
||||
});
|
||||
|
||||
it('should throw error when associated with item that null with target key', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
|
@ -27,7 +27,7 @@ import { CollectionFactory } from './collection-factory';
|
||||
import { CollectionGroupManager } from './collection-group-manager';
|
||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||
import DatabaseUtils from './database-utils';
|
||||
import ReferencesMap from './features/ReferencesMap';
|
||||
import ReferencesMap from './features/references-map';
|
||||
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
||||
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
||||
import * as FieldTypes from './fields';
|
||||
|
@ -1,30 +1,57 @@
|
||||
export type ReferencePriority = 'default' | 'user';
|
||||
|
||||
export interface Reference {
|
||||
sourceCollectionName: string;
|
||||
sourceField: string;
|
||||
targetField: string;
|
||||
targetCollectionName: string;
|
||||
onDelete: string;
|
||||
priority: ReferencePriority;
|
||||
}
|
||||
|
||||
const DEFAULT_ON_DELETE = 'NO ACTION';
|
||||
|
||||
export function buildReference(options: Partial<Reference>): Reference {
|
||||
const { sourceCollectionName, sourceField, targetField, targetCollectionName, onDelete, priority } = options;
|
||||
|
||||
return {
|
||||
sourceCollectionName,
|
||||
sourceField,
|
||||
targetField,
|
||||
targetCollectionName,
|
||||
onDelete: (onDelete || DEFAULT_ON_DELETE).toUpperCase(),
|
||||
priority: assignPriority(priority, onDelete),
|
||||
};
|
||||
}
|
||||
|
||||
function assignPriority(priority: string | undefined, onDelete: string | undefined): ReferencePriority {
|
||||
if (priority) {
|
||||
return priority as ReferencePriority;
|
||||
}
|
||||
|
||||
return onDelete ? 'user' : 'default';
|
||||
}
|
||||
|
||||
const PRIORITY_MAP = {
|
||||
default: 1,
|
||||
user: 2,
|
||||
};
|
||||
|
||||
class ReferencesMap {
|
||||
protected map: Map<string, Reference[]> = new Map();
|
||||
|
||||
addReference(reference: Reference) {
|
||||
if (!reference.onDelete) {
|
||||
reference.onDelete = 'SET NULL';
|
||||
}
|
||||
|
||||
reference.onDelete = reference.onDelete.toUpperCase();
|
||||
|
||||
const existReference = this.existReference(reference);
|
||||
|
||||
if (existReference && existReference.onDelete !== reference.onDelete) {
|
||||
if (reference.onDelete === 'SET NULL') {
|
||||
// using existing reference
|
||||
return;
|
||||
} else if (existReference.onDelete === 'SET NULL') {
|
||||
// check two references onDelete priority, using the higher priority, if both are the same, throw error
|
||||
const existPriority = PRIORITY_MAP[existReference.priority];
|
||||
const newPriority = PRIORITY_MAP[reference.priority];
|
||||
|
||||
if (newPriority > existPriority) {
|
||||
existReference.onDelete = reference.onDelete;
|
||||
} else {
|
||||
existReference.priority = reference.priority;
|
||||
} else if (newPriority === existPriority && newPriority === PRIORITY_MAP['user']) {
|
||||
throw new Error(
|
||||
`On Delete Conflict, exist reference ${JSON.stringify(existReference)}, new reference ${JSON.stringify(
|
||||
reference,
|
||||
@ -52,7 +79,7 @@ class ReferencesMap {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keys = Object.keys(reference).filter((k) => k !== 'onDelete');
|
||||
const keys = Object.keys(reference).filter((k) => k !== 'onDelete' && k !== 'priority');
|
||||
|
||||
return references.find((ref) => keys.every((key) => ref[key] === reference[key]));
|
||||
}
|
@ -21,6 +21,11 @@ export async function referentialIntegrityCheck(options: ReferentialIntegrityChe
|
||||
|
||||
for (const reference of references) {
|
||||
const { sourceCollectionName, sourceField, targetField, onDelete } = reference;
|
||||
|
||||
if (onDelete === 'NO ACTION') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceCollection = db.collections.get(sourceCollectionName);
|
||||
const sourceRepository = sourceCollection.repository;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import lodash, { omit } from 'lodash';
|
||||
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { buildReference, Reference, ReferencePriority } from '../features/references-map';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
@ -16,20 +16,26 @@ export class BelongsToField extends RelationField {
|
||||
return target || Utils.pluralize(name);
|
||||
}
|
||||
|
||||
static toReference(db, association, onDelete) {
|
||||
static toReference(db, association, onDelete, priority: ReferencePriority = 'default'): Reference {
|
||||
const targetKey = association.targetKey;
|
||||
|
||||
return {
|
||||
return buildReference({
|
||||
sourceCollectionName: db.modelCollection.get(association.source).name,
|
||||
sourceField: association.foreignKey,
|
||||
targetField: targetKey,
|
||||
targetCollectionName: db.modelCollection.get(association.target).name,
|
||||
onDelete: onDelete,
|
||||
};
|
||||
priority: priority,
|
||||
});
|
||||
}
|
||||
|
||||
reference(association): Reference {
|
||||
return BelongsToField.toReference(this.database, association, this.options.onDelete);
|
||||
return BelongsToField.toReference(
|
||||
this.database,
|
||||
association,
|
||||
this.options.onDelete,
|
||||
this.options.onDelete ? 'user' : 'default',
|
||||
);
|
||||
}
|
||||
|
||||
checkAssociationKeys() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { omit } from 'lodash';
|
||||
import { AssociationScope, BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { Reference } from '../features/references-map';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { BelongsToField } from './belongs-to-field';
|
||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
@ -32,6 +32,8 @@ export class BelongsToManyField extends RelationField {
|
||||
|
||||
const onDelete = this.options.onDelete || 'CASCADE';
|
||||
|
||||
const priority = this.options.onDelete ? 'user' : 'default';
|
||||
|
||||
const targetAssociation = association.toTarget;
|
||||
|
||||
if (association.targetKey) {
|
||||
@ -45,8 +47,8 @@ export class BelongsToManyField extends RelationField {
|
||||
}
|
||||
|
||||
return [
|
||||
BelongsToField.toReference(db, targetAssociation, onDelete),
|
||||
BelongsToField.toReference(db, sourceAssociation, onDelete),
|
||||
BelongsToField.toReference(db, targetAssociation, onDelete, priority),
|
||||
BelongsToField.toReference(db, sourceAssociation, onDelete, priority),
|
||||
];
|
||||
}
|
||||
|
||||
@ -149,10 +151,6 @@ export class BelongsToManyField extends RelationField {
|
||||
Object.defineProperty(Through.model, 'isThrough', { value: true });
|
||||
}
|
||||
|
||||
if (!this.options.onDelete) {
|
||||
this.options.onDelete = 'CASCADE';
|
||||
}
|
||||
|
||||
const belongsToManyOptions = {
|
||||
constraints: false,
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { buildReference, Reference } from '../features/references-map';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
@ -89,13 +89,13 @@ export class HasManyField extends RelationField {
|
||||
reference(association): Reference {
|
||||
const sourceKey = association.sourceKey;
|
||||
|
||||
return {
|
||||
return buildReference({
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
checkAssociationKeys() {
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { buildReference, Reference } from '../features/references-map';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
@ -98,13 +98,13 @@ export class HasOneField extends RelationField {
|
||||
reference(association): Reference {
|
||||
const sourceKey = association.sourceKey;
|
||||
|
||||
return {
|
||||
return buildReference({
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
checkAssociationKeys() {
|
||||
|
Loading…
Reference in New Issue
Block a user