mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Disable foreign check on SQLite when deleting fields (#14512)
* Disable foreign check on SQLite when deleting fields * Add default on_delete constraint * Add test * Rename methods * Fix test sequence
This commit is contained in:
17
api/src/database/helpers/schema/dialects/sqlite.ts
Normal file
17
api/src/database/helpers/schema/dialects/sqlite.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SchemaHelper } from '../types';
|
||||
|
||||
export class SchemaHelperSQLite extends SchemaHelper {
|
||||
async preColumnDelete(): Promise<boolean> {
|
||||
const foreignCheckStatus = (await this.knex.raw('PRAGMA foreign_keys'))[0].foreign_keys === 1;
|
||||
|
||||
if (foreignCheckStatus) {
|
||||
await this.knex.raw('PRAGMA foreign_keys = OFF');
|
||||
}
|
||||
|
||||
return foreignCheckStatus;
|
||||
}
|
||||
|
||||
async postColumnDelete(): Promise<void> {
|
||||
await this.knex.raw('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@ export { SchemaHelperDefault as postgres } from './dialects/default';
|
||||
export { SchemaHelperCockroachDb as cockroachdb } from './dialects/cockroachdb';
|
||||
export { SchemaHelperDefault as redshift } from './dialects/default';
|
||||
export { SchemaHelperOracle as oracle } from './dialects/oracle';
|
||||
export { SchemaHelperDefault as sqlite } from './dialects/default';
|
||||
export { SchemaHelperSQLite as sqlite } from './dialects/sqlite';
|
||||
export { SchemaHelperDefault as mysql } from './dialects/default';
|
||||
export { SchemaHelperDefault as mssql } from './dialects/default';
|
||||
|
||||
@@ -125,4 +125,12 @@ export abstract class SchemaHelper extends DatabaseHelper {
|
||||
await this.changeNullable(table, column, options.nullable);
|
||||
}
|
||||
}
|
||||
|
||||
async preColumnDelete(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async postColumnDelete(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,6 +420,8 @@ export class FieldsService {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const runPostColumnDelete = await this.helpers.schema.preColumnDelete();
|
||||
|
||||
try {
|
||||
await emitter.emitFilter(
|
||||
'fields.delete',
|
||||
@@ -535,6 +537,10 @@ export class FieldsService {
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
if (runPostColumnDelete) {
|
||||
await this.helpers.schema.postColumnDelete();
|
||||
}
|
||||
|
||||
if (this.cache && env.CACHE_AUTO_PURGE) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
@@ -263,7 +263,9 @@ export async function CreateFieldM2O(vendor: string, options: OptionsCreateField
|
||||
fieldSchema: {},
|
||||
primaryKeyType: 'integer',
|
||||
relationMeta: {},
|
||||
relationSchema: {},
|
||||
relationSchema: {
|
||||
on_delete: 'SET NULL',
|
||||
},
|
||||
};
|
||||
|
||||
options = Object.assign({}, defaultOptions, options);
|
||||
@@ -319,7 +321,9 @@ export async function CreateFieldO2M(vendor: string, options: OptionsCreateField
|
||||
otherMeta: {},
|
||||
otherSchema: {},
|
||||
relationMeta: {},
|
||||
relationSchema: {},
|
||||
relationSchema: {
|
||||
on_delete: 'SET NULL',
|
||||
},
|
||||
};
|
||||
|
||||
options = Object.assign({}, defaultOptions, options);
|
||||
|
||||
206
tests-blackbox/routes/fields/delete-field.seed.ts
Normal file
206
tests-blackbox/routes/fields/delete-field.seed.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import vendors from '@common/get-dbs-to-test';
|
||||
import {
|
||||
CreateCollection,
|
||||
CreateField,
|
||||
CreateFieldO2M,
|
||||
CreateItem,
|
||||
DeleteCollection,
|
||||
SeedFunctions,
|
||||
PrimaryKeyType,
|
||||
PRIMARY_KEY_TYPES,
|
||||
} from '@common/index';
|
||||
import { CachedTestsSchema, TestsSchema, TestsSchemaVendorValues } from '@query/filter';
|
||||
import { set } from 'lodash';
|
||||
|
||||
export const collectionCountries = 'test_fields_delete_field_countries';
|
||||
export const collectionStates = 'test_fields_delete_field_states';
|
||||
|
||||
export type Country = {
|
||||
id?: number | string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type State = {
|
||||
id?: number | string;
|
||||
name: string;
|
||||
country_id?: number | string | null;
|
||||
};
|
||||
|
||||
export type City = {
|
||||
id?: number | string;
|
||||
name: string;
|
||||
state_id?: number | string | null;
|
||||
};
|
||||
|
||||
export function getTestsSchema(pkType: PrimaryKeyType, seed?: string): TestsSchema {
|
||||
const schema: TestsSchema = {
|
||||
[`${collectionCountries}_${pkType}`]: {
|
||||
id: {
|
||||
field: 'id',
|
||||
type: pkType,
|
||||
isPrimaryKey: true,
|
||||
filters: true,
|
||||
possibleValues: SeedFunctions.generatePrimaryKeys(pkType, {
|
||||
quantity: 2,
|
||||
seed: `${collectionCountries}${seed}`,
|
||||
incremental: true,
|
||||
}),
|
||||
},
|
||||
name: {
|
||||
field: 'name',
|
||||
type: 'string',
|
||||
filters: true,
|
||||
possibleValues: ['United States', 'Malaysia'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
schema[`${collectionStates}_${pkType}`] = {
|
||||
id: {
|
||||
field: 'id',
|
||||
type: pkType,
|
||||
isPrimaryKey: true,
|
||||
filters: false,
|
||||
possibleValues: SeedFunctions.generatePrimaryKeys(pkType, {
|
||||
quantity: 4,
|
||||
seed: `${collectionStates}${seed}`,
|
||||
incremental: true,
|
||||
}),
|
||||
},
|
||||
name: {
|
||||
field: 'name',
|
||||
type: 'string',
|
||||
filters: false,
|
||||
possibleValues: ['Washington', 'California', 'Johor', 'Sarawak'],
|
||||
},
|
||||
};
|
||||
|
||||
schema[`${collectionCountries}_${pkType}`]['states'] = {
|
||||
field: 'states',
|
||||
type: 'alias',
|
||||
filters: false,
|
||||
possibleValues: schema[`${collectionStates}_${pkType}`].id.possibleValues,
|
||||
children: schema[`${collectionStates}_${pkType}`],
|
||||
relatedCollection: `${collectionStates}_${pkType}`,
|
||||
};
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
export const seedDBStructure = () => {
|
||||
it.each(vendors)(
|
||||
'%s',
|
||||
async (vendor) => {
|
||||
for (const pkType of PRIMARY_KEY_TYPES) {
|
||||
try {
|
||||
const localCollectionCountries = `${collectionCountries}_${pkType}`;
|
||||
const localCollectionStates = `${collectionStates}_${pkType}`;
|
||||
|
||||
// Delete existing collections
|
||||
await DeleteCollection(vendor, { collection: localCollectionStates });
|
||||
await DeleteCollection(vendor, { collection: localCollectionCountries });
|
||||
|
||||
// Create countries collection
|
||||
await CreateCollection(vendor, {
|
||||
collection: localCollectionCountries,
|
||||
primaryKeyType: pkType,
|
||||
});
|
||||
|
||||
await CreateField(vendor, {
|
||||
collection: localCollectionCountries,
|
||||
field: 'name',
|
||||
type: 'string',
|
||||
});
|
||||
|
||||
// Create states collection
|
||||
await CreateCollection(vendor, {
|
||||
collection: localCollectionStates,
|
||||
primaryKeyType: pkType,
|
||||
});
|
||||
|
||||
await CreateField(vendor, {
|
||||
collection: localCollectionStates,
|
||||
field: 'name',
|
||||
type: 'string',
|
||||
});
|
||||
|
||||
// Create O2M relationships
|
||||
await CreateFieldO2M(vendor, {
|
||||
collection: localCollectionCountries,
|
||||
field: 'states',
|
||||
otherCollection: localCollectionStates,
|
||||
otherField: 'country_id',
|
||||
primaryKeyType: pkType,
|
||||
});
|
||||
|
||||
expect(true).toBeTruthy();
|
||||
} catch (error) {
|
||||
expect(error).toBeFalsy();
|
||||
}
|
||||
}
|
||||
},
|
||||
300000
|
||||
);
|
||||
};
|
||||
|
||||
export const seedDBValues = async (cachedSchema: CachedTestsSchema, vendorSchemaValues: TestsSchemaVendorValues) => {
|
||||
await Promise.all(
|
||||
vendors.map(async (vendor) => {
|
||||
for (const pkType of PRIMARY_KEY_TYPES) {
|
||||
const schema = cachedSchema[pkType];
|
||||
|
||||
const localCollectionCountries = `${collectionCountries}_${pkType}`;
|
||||
const localCollectionStates = `${collectionStates}_${pkType}`;
|
||||
|
||||
// Create countries
|
||||
const itemCountries = [];
|
||||
|
||||
for (let i = 0; i < schema[localCollectionCountries].id.possibleValues.length; i++) {
|
||||
const country: Country = {
|
||||
name: schema[localCollectionCountries].name.possibleValues[i],
|
||||
};
|
||||
|
||||
if (pkType === 'string') {
|
||||
country.id = schema[localCollectionCountries].id.possibleValues[i];
|
||||
}
|
||||
|
||||
itemCountries.push(country);
|
||||
}
|
||||
|
||||
const countries = await CreateItem(vendor, {
|
||||
collection: localCollectionCountries,
|
||||
item: itemCountries,
|
||||
});
|
||||
|
||||
const countriesIDs = countries.map((country: Country) => country.id);
|
||||
|
||||
set(vendorSchemaValues, `${vendor}.${localCollectionCountries}.id`, countriesIDs);
|
||||
|
||||
// Create states
|
||||
const itemStates = [];
|
||||
|
||||
for (let i = 0; i < schema[localCollectionStates].id.possibleValues.length; i++) {
|
||||
const state: State = {
|
||||
name: schema[localCollectionStates].name.possibleValues[i],
|
||||
country_id: countriesIDs[i % countriesIDs.length],
|
||||
};
|
||||
|
||||
if (pkType === 'string') {
|
||||
state.id = schema[localCollectionStates].id.possibleValues[i];
|
||||
}
|
||||
|
||||
itemStates.push(state);
|
||||
}
|
||||
|
||||
const states = await CreateItem(vendor, {
|
||||
collection: localCollectionStates,
|
||||
item: itemStates,
|
||||
});
|
||||
|
||||
const statesIDs = states.map((state: State) => state.id);
|
||||
|
||||
set(vendorSchemaValues, `${vendor}.${localCollectionStates}.id`, statesIDs);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
71
tests-blackbox/routes/fields/delete-field.test.ts
Normal file
71
tests-blackbox/routes/fields/delete-field.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import request from 'supertest';
|
||||
import { getUrl } from '@common/config';
|
||||
import vendors from '@common/get-dbs-to-test';
|
||||
import { CreateField, DeleteField } from '@common/functions';
|
||||
import { CachedTestsSchema, TestsSchemaVendorValues } from '@query/filter';
|
||||
import * as common from '@common/index';
|
||||
import { collectionCountries, collectionStates, getTestsSchema, seedDBValues } from './delete-field.seed';
|
||||
|
||||
const cachedSchema = common.PRIMARY_KEY_TYPES.reduce((acc, pkType) => {
|
||||
acc[pkType] = getTestsSchema(pkType);
|
||||
return acc;
|
||||
}, {} as CachedTestsSchema);
|
||||
|
||||
const vendorSchemaValues: TestsSchemaVendorValues = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
await seedDBValues(cachedSchema, vendorSchemaValues);
|
||||
}, 300000);
|
||||
|
||||
describe('Seed Database Values', () => {
|
||||
it.each(vendors)('%s', async (vendor) => {
|
||||
// Assert
|
||||
expect(vendorSchemaValues[vendor]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(common.PRIMARY_KEY_TYPES)('/fields', (pkType) => {
|
||||
const localCollectionCountries = `${collectionCountries}_${pkType}`;
|
||||
const localCollectionStates = `${collectionStates}_${pkType}`;
|
||||
|
||||
describe(`pkType: ${pkType}`, () => {
|
||||
describe('DELETE /:collection/:field', () => {
|
||||
describe('with foreign key constraints does not clear existing data', () => {
|
||||
it.each(vendors)('%s', async (vendor) => {
|
||||
// Setup
|
||||
const newFieldName = 'to_be_deleted';
|
||||
|
||||
const response = await request(getUrl(vendor))
|
||||
.get(`/items/${localCollectionStates}`)
|
||||
.set('Authorization', `Bearer ${common.USER.ADMIN.TOKEN}`);
|
||||
|
||||
const existingData = response.body.data;
|
||||
|
||||
// Action
|
||||
await CreateField(vendor, {
|
||||
collection: localCollectionCountries,
|
||||
field: newFieldName,
|
||||
type: 'string',
|
||||
});
|
||||
|
||||
await DeleteField(vendor, {
|
||||
collection: localCollectionCountries,
|
||||
field: newFieldName,
|
||||
});
|
||||
|
||||
const response2 = await request(getUrl(vendor))
|
||||
.get(`/items/${localCollectionStates}`)
|
||||
.set('Authorization', `Bearer ${common.USER.ADMIN.TOKEN}`);
|
||||
|
||||
const updatedData = response2.body.data;
|
||||
|
||||
// Assert
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response2.statusCode).toEqual(200);
|
||||
expect(existingData).toHaveLength(4);
|
||||
expect(existingData).toStrictEqual(updatedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ exports.list = {
|
||||
{ testFilePath: '/common/seed-database.test.ts' },
|
||||
{ testFilePath: '/common/common.test.ts' },
|
||||
{ testFilePath: '/routes/collections/crud.test.ts' },
|
||||
{ testFilePath: '/routes/fields/delete-field.test.ts' },
|
||||
],
|
||||
after: [
|
||||
{ testFilePath: '/schema/timezone/timezone.test.ts' },
|
||||
|
||||
Reference in New Issue
Block a user