Delete all entries in a locale when locale is deleted

This commit is contained in:
Pierre Noël 2021-04-01 11:58:17 +02:00
parent 6b6e3eba55
commit 3c85f38fb4
6 changed files with 173 additions and 17 deletions

View File

@ -11,6 +11,8 @@ const { contentTypes: contentTypesUtils } = require('strapi-utils');
const { singular } = require('pluralize');
const { handleDatabaseError } = require('./utils/errors');
const BATCH_SIZE = 1000;
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
const pickCountFilters = omit(['sort', 'limit', 'start']);
@ -173,7 +175,10 @@ module.exports = function createQueryBuilder({ model, strapi }) {
return wrapTransaction(runDelete, { transacting });
}
async function deleteMany(params, { transacting } = {}) {
async function deleteMany(
params,
{ transacting, returning = true, batchSize = BATCH_SIZE } = {}
) {
if (params[model.primaryKey]) {
const entries = await find({ ...params, _limit: 1 }, null, { transacting });
if (entries.length > 0) {
@ -182,12 +187,30 @@ module.exports = function createQueryBuilder({ model, strapi }) {
return null;
}
const paramsWithDefaults = _.defaults(params, { _limit: -1 });
const entries = await find(paramsWithDefaults, null, { transacting });
return pmap(entries, entry => deleteOne(entry.id, { transacting }), {
concurrency: 100,
stopOnError: true,
});
if (returning) {
const paramsWithDefaults = _.defaults(params, { _limit: -1 });
const entries = await find(paramsWithDefaults, null, { transacting });
return pmap(entries, entry => deleteOne(entry.id, { transacting }), {
concurrency: 100,
stopOnError: true,
});
}
// returning false, we can optimize the function
const batchParams = _.assign({}, params, { _limit: batchSize, _sort: 'id:ASC' });
// eslint-disable-next-line no-constant-condition
while (true) {
const batch = await find(batchParams, null, { transacting });
await pmap(batch, entry => deleteOne(entry.id, { transacting }), {
concurrency: 100,
stopOnError: true,
});
if (batch.length < BATCH_SIZE) {
break;
}
}
}
function search(params, populate) {

View File

@ -5,12 +5,15 @@
const _ = require('lodash');
const { prop, omit } = require('lodash/fp');
const pmap = require('p-map');
const { convertRestQueryParams, buildQuery } = require('strapi-utils');
const { contentTypes: contentTypesUtils } = require('strapi-utils');
const mongoose = require('mongoose');
const populateQueries = require('./utils/populate-queries');
const BATCH_SIZE = 1000;
const { PUBLISHED_AT_ATTRIBUTE, DP_PUB_STATES } = contentTypesUtils.constants;
const { findComponentByGlobalId } = require('./utils/helpers');
const { handleDatabaseError } = require('./utils/errors');
@ -505,7 +508,10 @@ module.exports = ({ model, strapi }) => {
return model.updateRelations(Object.assign(params, { values: relations }), { session });
}
async function deleteMany(params, { session = null } = {}) {
async function deleteMany(
params,
{ session = null, returning = true, batchSize = BATCH_SIZE } = {}
) {
if (params[model.primaryKey]) {
const entries = await find({ ...params, _limit: 1 }, null, { session });
if (entries.length > 0) {
@ -514,8 +520,28 @@ module.exports = ({ model, strapi }) => {
return null;
}
const entries = await find(params, null, { session });
return Promise.all(entries.map(entry => deleteOne(entry[model.primaryKey], { session })));
if (returning) {
const entries = await find(params, null, { session });
return pmap(entries, entry => deleteOne(entry[model.primaryKey], { session }), {
concurrency: 100,
stopOnError: true,
});
}
// returning false, we can optimize the function
const batchParams = _.assign({}, params, { _limit: batchSize, _sort: 'id:ASC' });
// eslint-disable-next-line no-constant-condition
while (true) {
const batch = await find(batchParams, null, { session });
await pmap(batch, entry => deleteOne(entry[model.primaryKey], { session }), {
concurrency: 100,
stopOnError: true,
});
if (batch.length < BATCH_SIZE) {
break;
}
}
}
async function deleteOne(id, { session = null } = {}) {

View File

@ -19,6 +19,7 @@
"mongoose": "5.10.8",
"mongoose-float": "^1.0.4",
"mongoose-long": "^0.3.2",
"p-map": "4.0.0",
"pluralize": "^8.0.0",
"semver": "^7.3.4",
"strapi-utils": "3.5.4"

View File

@ -122,14 +122,35 @@ describe('Locales', () => {
test('delete', async () => {
const locale = { name: 'French', code: 'fr' };
const deleteFn = jest.fn(() => locale);
const query = jest.fn(() => ({ delete: deleteFn }));
global.strapi = { query };
const findOne = jest.fn(() => locale);
const isLocalized = jest.fn(() => true);
const query = jest.fn(() => ({ delete: deleteFn, findOne }));
global.strapi = {
query,
plugins: { i18n: { services: { 'content-types': { isLocalized } } } },
contentTypes: { 'application::country.country': {} },
};
const deletedLocale = await localesService.delete({ id: 1 });
expect(query).toHaveBeenCalledWith('locale', 'i18n');
expect(deleteFn).toHaveBeenCalledWith({ id: 1 });
expect(deletedLocale).toMatchObject(locale);
});
test('delete - not found', async () => {
const locale = { name: 'French', code: 'fr' };
const deleteFn = jest.fn(() => locale);
const findOne = jest.fn(() => undefined);
const query = jest.fn(() => ({ delete: deleteFn, findOne }));
global.strapi = {
query,
};
const deletedLocale = await localesService.delete({ id: 1 });
expect(query).toHaveBeenCalledWith('locale', 'i18n');
expect(deleteFn).not.toHaveBeenCalled();
expect(deletedLocale).toBeUndefined();
});
});
describe('initDefaultLocale', () => {

View File

@ -2,6 +2,7 @@
const { isNil } = require('lodash/fp');
const { DEFAULT_LOCALE } = require('../constants');
const { getService } = require('../utils');
const { getCoreStore } = require('../utils');
@ -15,7 +16,15 @@ const create = locale => strapi.query('locale', 'i18n').create(locale);
const update = (params, updates) => strapi.query('locale', 'i18n').update(params, updates);
const deleteFn = ({ id }) => strapi.query('locale', 'i18n').delete({ id });
const deleteFn = async ({ id }) => {
let localeToDelete = await strapi.query('locale', 'i18n').findOne({ id });
if (localeToDelete) {
await deleteAllLocalizedEntriesFor({ locale: localeToDelete.code });
localeToDelete = await strapi.query('locale', 'i18n').delete({ id });
}
return localeToDelete;
};
const setDefaultLocale = ({ code }) => getCoreStore().set({ key: 'default_locale', value: code });
@ -44,6 +53,16 @@ const initDefaultLocale = async () => {
}
};
const deleteAllLocalizedEntriesFor = async ({ locale }) => {
const { isLocalized } = getService('content-types');
const localizedModels = Object.values(strapi.contentTypes).filter(isLocalized);
for (const model of localizedModels) {
await strapi.query(model.uid).delete({ locale }, { returning: false });
}
};
module.exports = {
find,
findById,

View File

@ -2,6 +2,7 @@
const { omit } = require('lodash/fp');
const { createTestBuilder } = require('../../../test/helpers/builder');
const { createStrapiInstance } = require('../../../test/helpers/strapi');
const { createAuthRequest } = require('../../../test/helpers/request');
@ -13,11 +14,31 @@ const data = {
const omitTimestamps = omit(['updatedAt', 'createdAt', 'updated_at', 'created_at']);
const compareLocales = (a, b) => (a.code < b.code ? -1 : 1);
const productModel = {
pluginOptions: {
i18n: {
localized: true,
},
},
attributes: {
name: {
type: 'string',
},
},
connection: 'default',
name: 'product',
description: '',
collectionName: '',
};
describe('CRUD locales', () => {
let rq;
let strapi;
const builder = createTestBuilder();
beforeAll(async () => {
await builder.addContentType(productModel).build();
strapi = await createStrapiInstance();
rq = await createAuthRequest({ strapi });
});
@ -167,7 +188,9 @@ describe('CRUD locales', () => {
expect(res.statusCode).toBe(200);
expect(res.body).toHaveLength(data.locales.length);
expect(res.body.sort(compareLocales)).toMatchObject(data.locales.sort(compareLocales));
expect(res.body.sort(compareLocales)).toMatchObject(
data.locales.slice().sort(compareLocales)
);
});
});
@ -179,17 +202,17 @@ describe('CRUD locales', () => {
};
let res = await rq({
url: `/i18n/locales/${data.locales[0].id}`,
url: `/i18n/locales/${data.locales[1].id}`,
method: 'PUT',
body: localeUpdate,
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
...omitTimestamps(data.locales[0]),
...omitTimestamps(data.locales[1]),
...localeUpdate,
});
data.locales[0] = res.body;
data.locales[1] = res.body;
});
test('Cannot update the code of a locale (without name)', async () => {
@ -306,11 +329,54 @@ describe('CRUD locales', () => {
});
test('Can delete a locale', async () => {
const { body: frenchProduct } = await rq({
url: '/content-manager/collection-types/application::product.product',
method: 'POST',
qs: { plugins: { i18n: { locale: 'fr' } } },
body: { name: 'product name' },
});
await rq({
url: '/content-manager/collection-types/application::product.product',
method: 'POST',
qs: { plugins: { i18n: { locale: 'en', relatedEntityId: frenchProduct.id } } },
body: { name: 'product name' },
});
const {
body: { results: createdProducts },
} = await rq({
url: '/content-manager/collection-types/application::product.product',
method: 'GET',
qs: { _locale: 'fr' },
});
expect(createdProducts).toHaveLength(1);
expect(createdProducts[0].localizations[0].locale).toBe('en');
const res = await rq({
url: `/i18n/locales/${data.locales[1].id}`,
method: 'DELETE',
});
const {
body: { results: frenchProducts },
} = await rq({
url: '/content-manager/collection-types/application::product.product',
method: 'GET',
qs: { _locale: 'fr' },
});
expect(frenchProducts).toHaveLength(0);
const {
body: { results: englishProducts },
} = await rq({
url: '/content-manager/collection-types/application::product.product',
method: 'GET',
qs: { _locale: 'en' },
});
expect(englishProducts).toHaveLength(1);
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject(omitTimestamps(data.locales[1]));
data.deletedLocales.push(res.body);