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();
|
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 () => {
|
it('should throw error when associated with item that null with target key', async () => {
|
||||||
const User = db.collection({
|
const User = db.collection({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
@ -27,7 +27,7 @@ import { CollectionFactory } from './collection-factory';
|
|||||||
import { CollectionGroupManager } from './collection-group-manager';
|
import { CollectionGroupManager } from './collection-group-manager';
|
||||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||||
import DatabaseUtils from './database-utils';
|
import DatabaseUtils from './database-utils';
|
||||||
import ReferencesMap from './features/ReferencesMap';
|
import ReferencesMap from './features/references-map';
|
||||||
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
||||||
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
||||||
import * as FieldTypes from './fields';
|
import * as FieldTypes from './fields';
|
||||||
|
@ -1,30 +1,57 @@
|
|||||||
|
export type ReferencePriority = 'default' | 'user';
|
||||||
|
|
||||||
export interface Reference {
|
export interface Reference {
|
||||||
sourceCollectionName: string;
|
sourceCollectionName: string;
|
||||||
sourceField: string;
|
sourceField: string;
|
||||||
targetField: string;
|
targetField: string;
|
||||||
targetCollectionName: string;
|
targetCollectionName: string;
|
||||||
onDelete: 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 {
|
class ReferencesMap {
|
||||||
protected map: Map<string, Reference[]> = new Map();
|
protected map: Map<string, Reference[]> = new Map();
|
||||||
|
|
||||||
addReference(reference: Reference) {
|
addReference(reference: Reference) {
|
||||||
if (!reference.onDelete) {
|
|
||||||
reference.onDelete = 'SET NULL';
|
|
||||||
}
|
|
||||||
|
|
||||||
reference.onDelete = reference.onDelete.toUpperCase();
|
|
||||||
|
|
||||||
const existReference = this.existReference(reference);
|
const existReference = this.existReference(reference);
|
||||||
|
|
||||||
if (existReference && existReference.onDelete !== reference.onDelete) {
|
if (existReference && existReference.onDelete !== reference.onDelete) {
|
||||||
if (reference.onDelete === 'SET NULL') {
|
// check two references onDelete priority, using the higher priority, if both are the same, throw error
|
||||||
// using existing reference
|
const existPriority = PRIORITY_MAP[existReference.priority];
|
||||||
return;
|
const newPriority = PRIORITY_MAP[reference.priority];
|
||||||
} else if (existReference.onDelete === 'SET NULL') {
|
|
||||||
|
if (newPriority > existPriority) {
|
||||||
existReference.onDelete = reference.onDelete;
|
existReference.onDelete = reference.onDelete;
|
||||||
} else {
|
existReference.priority = reference.priority;
|
||||||
|
} else if (newPriority === existPriority && newPriority === PRIORITY_MAP['user']) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`On Delete Conflict, exist reference ${JSON.stringify(existReference)}, new reference ${JSON.stringify(
|
`On Delete Conflict, exist reference ${JSON.stringify(existReference)}, new reference ${JSON.stringify(
|
||||||
reference,
|
reference,
|
||||||
@ -52,7 +79,7 @@ class ReferencesMap {
|
|||||||
return null;
|
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]));
|
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) {
|
for (const reference of references) {
|
||||||
const { sourceCollectionName, sourceField, targetField, onDelete } = reference;
|
const { sourceCollectionName, sourceField, targetField, onDelete } = reference;
|
||||||
|
|
||||||
|
if (onDelete === 'NO ACTION') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const sourceCollection = db.collections.get(sourceCollectionName);
|
const sourceCollection = db.collections.get(sourceCollectionName);
|
||||||
const sourceRepository = sourceCollection.repository;
|
const sourceRepository = sourceCollection.repository;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import lodash, { omit } from 'lodash';
|
import lodash, { omit } from 'lodash';
|
||||||
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
|
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 { checkIdentifier } from '../utils';
|
||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
@ -16,20 +16,26 @@ export class BelongsToField extends RelationField {
|
|||||||
return target || Utils.pluralize(name);
|
return target || Utils.pluralize(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static toReference(db, association, onDelete) {
|
static toReference(db, association, onDelete, priority: ReferencePriority = 'default'): Reference {
|
||||||
const targetKey = association.targetKey;
|
const targetKey = association.targetKey;
|
||||||
|
|
||||||
return {
|
return buildReference({
|
||||||
sourceCollectionName: db.modelCollection.get(association.source).name,
|
sourceCollectionName: db.modelCollection.get(association.source).name,
|
||||||
sourceField: association.foreignKey,
|
sourceField: association.foreignKey,
|
||||||
targetField: targetKey,
|
targetField: targetKey,
|
||||||
targetCollectionName: db.modelCollection.get(association.target).name,
|
targetCollectionName: db.modelCollection.get(association.target).name,
|
||||||
onDelete: onDelete,
|
onDelete: onDelete,
|
||||||
};
|
priority: priority,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reference(association): Reference {
|
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() {
|
checkAssociationKeys() {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { AssociationScope, BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
|
import { AssociationScope, BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { Reference } from '../features/ReferencesMap';
|
import { Reference } from '../features/references-map';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
import { BelongsToField } from './belongs-to-field';
|
import { BelongsToField } from './belongs-to-field';
|
||||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||||
@ -32,6 +32,8 @@ export class BelongsToManyField extends RelationField {
|
|||||||
|
|
||||||
const onDelete = this.options.onDelete || 'CASCADE';
|
const onDelete = this.options.onDelete || 'CASCADE';
|
||||||
|
|
||||||
|
const priority = this.options.onDelete ? 'user' : 'default';
|
||||||
|
|
||||||
const targetAssociation = association.toTarget;
|
const targetAssociation = association.toTarget;
|
||||||
|
|
||||||
if (association.targetKey) {
|
if (association.targetKey) {
|
||||||
@ -45,8 +47,8 @@ export class BelongsToManyField extends RelationField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
BelongsToField.toReference(db, targetAssociation, onDelete),
|
BelongsToField.toReference(db, targetAssociation, onDelete, priority),
|
||||||
BelongsToField.toReference(db, sourceAssociation, onDelete),
|
BelongsToField.toReference(db, sourceAssociation, onDelete, priority),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,10 +151,6 @@ export class BelongsToManyField extends RelationField {
|
|||||||
Object.defineProperty(Through.model, 'isThrough', { value: true });
|
Object.defineProperty(Through.model, 'isThrough', { value: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.options.onDelete) {
|
|
||||||
this.options.onDelete = 'CASCADE';
|
|
||||||
}
|
|
||||||
|
|
||||||
const belongsToManyOptions = {
|
const belongsToManyOptions = {
|
||||||
constraints: false,
|
constraints: false,
|
||||||
...omit(this.options, ['name', 'type', 'target']),
|
...omit(this.options, ['name', 'type', 'target']),
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
Utils,
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { Reference } from '../features/ReferencesMap';
|
import { buildReference, Reference } from '../features/references-map';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
@ -89,13 +89,13 @@ export class HasManyField extends RelationField {
|
|||||||
reference(association): Reference {
|
reference(association): Reference {
|
||||||
const sourceKey = association.sourceKey;
|
const sourceKey = association.sourceKey;
|
||||||
|
|
||||||
return {
|
return buildReference({
|
||||||
sourceCollectionName: this.database.modelCollection.get(association.target).name,
|
sourceCollectionName: this.database.modelCollection.get(association.target).name,
|
||||||
sourceField: association.foreignKey,
|
sourceField: association.foreignKey,
|
||||||
targetField: sourceKey,
|
targetField: sourceKey,
|
||||||
targetCollectionName: this.database.modelCollection.get(association.source).name,
|
targetCollectionName: this.database.modelCollection.get(association.source).name,
|
||||||
onDelete: this.options.onDelete,
|
onDelete: this.options.onDelete,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAssociationKeys() {
|
checkAssociationKeys() {
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
Utils,
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { Reference } from '../features/ReferencesMap';
|
import { buildReference, Reference } from '../features/references-map';
|
||||||
import { checkIdentifier } from '../utils';
|
import { checkIdentifier } from '../utils';
|
||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
@ -98,13 +98,13 @@ export class HasOneField extends RelationField {
|
|||||||
reference(association): Reference {
|
reference(association): Reference {
|
||||||
const sourceKey = association.sourceKey;
|
const sourceKey = association.sourceKey;
|
||||||
|
|
||||||
return {
|
return buildReference({
|
||||||
sourceCollectionName: this.database.modelCollection.get(association.target).name,
|
sourceCollectionName: this.database.modelCollection.get(association.target).name,
|
||||||
sourceField: association.foreignKey,
|
sourceField: association.foreignKey,
|
||||||
targetField: sourceKey,
|
targetField: sourceKey,
|
||||||
targetCollectionName: this.database.modelCollection.get(association.source).name,
|
targetCollectionName: this.database.modelCollection.get(association.source).name,
|
||||||
onDelete: this.options.onDelete,
|
onDelete: this.options.onDelete,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAssociationKeys() {
|
checkAssociationKeys() {
|
||||||
|
Loading…
Reference in New Issue
Block a user