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:
ian
2022-07-22 02:44:05 +08:00
committed by GitHub
parent 8d9991abe3
commit 9f9ef45846
8 changed files with 316 additions and 3 deletions

View 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');
}
}

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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);

View 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);
}
})
);
};

View 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);
});
});
});
});
});

View File

@@ -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' },