Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,17 @@ This command will:

`flotiq purge [flotiqApiKey]`

This command will remove all data from your account. Great for testing imports. Command requires additional confirmation.
This command purges a selected target in your account. Command requires additional confirmation.

**Parameters**

* `flotiqApiKey` - read and write API key to your Flotiq account

**Flags**

* `--withInternal` or `--internal` - purge will also remove internal type objects like (`_media`)
* `--force` or `--f` - purge will remove data even if Content Types relations loop to each other.
* `--spaceId=[spaceId]` or `--space=[spaceId]` - purge all data in selected space
* `--ctdName=[ctdName]` or `--ctd=[ctdName]` - purge data for selected Content Type Definition
* `--deleteSchema` or `--deleteCtd` - when used with `--ctdName`, also removes the CTD schema

### Install Flotiq SDK

Expand Down
41 changes: 2 additions & 39 deletions src/command/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import importerCommand from "../../commands/importer.js";
import exporterCommand from "../../commands/exporter.js";
import questionsText from "./questions.js";
import projectSetup from "../start/projectSetup.js";
import purgeContentObjects from "../purifier/purifier.js";
import purgeCommand from "../purifier/command.js";
import sdk from "../sdk/sdk.js";
import stats from "../stats/stats.js";
import { getFlotiqApi } from "@flotiq/api";
Expand All @@ -22,6 +22,7 @@ yargs
.alias("help", "h")
.command(exporterCommand)
.command(importerCommand)
.command(purgeCommand)
.string("framework")
.alias("framework", ["fw"])
.describe("framework", " Determines which framework should be used (gatsby, nextjs)")
Expand Down Expand Up @@ -104,44 +105,6 @@ yargs
process.exit(1);
}
})
.command(
"purge [flotiqApiKey]",
"Purge Flotiq account, removes all objects to which the key has access",
(yargs) => {
optionalParamFlotiqApiKey(yargs);
yargs
.boolean("force")
.alias("force", ["f"])
.describe("force", "force removing content objects when function gets stuck")
.boolean("withInternal")
.alias("withInternal", ["internal"])
.describe("withInternal", "remove objects from internal CTD like _media");
}, (argv) => {
const purge = async (apiKey, withInternal, force) => {
const answers = await askQuestions(questionsText.PURGE_QUESTION);
const { confirmation } = answers;
if (confirmation.toUpperCase() === "Y") {
await purgeContentObjects(getFlotiqApi(`${config.apiUrl}/api/v1`, apiKey), withInternal, force);
} else {
console.log("I'm finishing, no data has been deleted");
process.exit(1);
}
};

if (yargs.argv._.length < 2 && !apiKeyDefinedInDotEnv()) {
console.log("Api key not found");
} else if (yargs.argv._.length === 1 && apiKeyDefinedInDotEnv()) {
purge(process.env.FLOTIQ_API_KEY);
} else if ((yargs.argv._.length <= 3 && apiKeyDefinedInDotEnv()) || yargs.argv._.length <= 4) {
if (!argv.flotiqApiKey && apiKeyDefinedInDotEnv()) {
argv.flotiqApiKey = process.env.FLOTIQ_API_KEY;
}
purge(argv.flotiqApiKey, yargs.argv["withInternal"], yargs.argv["force"]);
} else {
yargs.showHelp();
process.exit(1);
}
})
.command("sdk install [language] [directory] [flotiqApiKey]", "Install Flotiq SDK", (yargs) => {
yargs.positional("language", {
describe: "SDK language, choices: csharp, go, java, javascript, php, python, typescript",
Expand Down
24 changes: 19 additions & 5 deletions src/command/questions.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,20 @@ const CF_API_KEY = {
type: "input",
message: "Contentful api key"
}
const PURGE_QUESTION_CONFIRMATION = {
const PURGE_SPACE_QUESTION_CONFIRMATION = {
name: "confirmation",
type: "input",
message: "Are you sure you want to delete all data available for this API KEY? [y/N]",
defaultAnswer: 'n'
}

const PURGE_CTD_QUESTION_CONFIRMATION = {
name: "confirmation",
type: "input",
message: "Are you sure you want to delete all data belonging to this content type? [y/N]",
defaultAnswer: 'n'
}

const LANGUAGE = {
name: "language",
type: "input",
Expand Down Expand Up @@ -81,8 +89,12 @@ const CONTENTFUL_IMPORT = [
CF_API_KEY,
]

const PURGE_QUESTION = [
PURGE_QUESTION_CONFIRMATION
const PURGE_SPACE_QUESTION = [
PURGE_SPACE_QUESTION_CONFIRMATION
]

const PURGE_CTD_QUESTION = [
PURGE_CTD_QUESTION_CONFIRMATION
]

const EXPORT_QUESTIONS = [
Expand Down Expand Up @@ -115,7 +127,8 @@ export {
IMPORT_QUESTIONS,
WORDPRESS_IMPORT_QUESTIONS,
CONTENTFUL_IMPORT,
PURGE_QUESTION,
PURGE_SPACE_QUESTION,
PURGE_CTD_QUESTION,
EXPORT_QUESTIONS,
INSTALL_SDK,
EXCEL_MIGRATION,
Expand All @@ -131,7 +144,8 @@ export default {
IMPORT_QUESTIONS,
WORDPRESS_IMPORT_QUESTIONS,
CONTENTFUL_IMPORT,
PURGE_QUESTION,
PURGE_SPACE_QUESTION,
PURGE_CTD_QUESTION,
EXPORT_QUESTIONS,
INSTALL_SDK,
EXCEL_MIGRATION,
Expand Down
103 changes: 103 additions & 0 deletions src/purifier/command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import inquirer from "inquirer";
import { getFlotiqApi } from "@flotiq/api";
import questionsText from "../command/questions.js";
import config from "../configuration/config.js";
import purgeContentObjects from "./purifier.js";

async function confirmPurge(type) {
let answers = '';
if (type === 'space') {
answers = await inquirer.prompt(questionsText.PURGE_SPACE_QUESTION);
} else {
answers = await inquirer.prompt(questionsText.PURGE_CTD_QUESTION);
}
return answers.confirmation.toUpperCase() === "Y";
}

function resolvePurgeOptions(argv) {
const deleteSchema = Boolean(argv.deleteSchema);

if (!argv.spaceId && !argv.ctdName) {
throw new Error("Choose purge target with --spaceId <spaceId> or --ctdName <ctdName>");
}

if (argv.spaceId && argv.ctdName) {
throw new Error("Use only one purge target at a time: --spaceId or --ctdName");
}

if (argv.spaceId && deleteSchema) {
throw new Error("--deleteSchema can be used only with --ctdName");
}

if (argv.spaceId) {
return {
type: "space",
spaceId: argv.spaceId,
ctdName: undefined,
deleteSchema,
};
}

return {
type: "ctd",
spaceId: undefined,
ctdName: argv.ctdName,
deleteSchema,
};
}

function resolveApiKey(argv) {
const envApiKey = process.env.FLOTIQ_API_KEY;
return argv.flotiqApiKey || envApiKey || null;
}

async function handler(argv) {
const apiKey = resolveApiKey(argv);

if (!apiKey) {
throw new Error("Api key not found");
}
const purgeOptions = resolvePurgeOptions(argv);
const confirmed = await confirmPurge(purgeOptions.type);
if (!confirmed) {
console.log("I'm finishing, no data has been deleted");
process.exit(1);
}

await purgeContentObjects(getFlotiqApi(`${config.apiUrl}/api/v1`, apiKey), purgeOptions);
}

const commandModule = {
command: "purge [flotiqApiKey]",
describe: "Purge Flotiq space or selected CTD",
builder: (yargs) => yargs
.option("flotiqApiKey", {
description: "Flotiq Read and write API KEY.",
type: "string",
demandOption: false,
})
.option("spaceId", {
description: "Flotiq space id to purge",
alias: ["space"],
type: "string",
demandOption: false,
})
.option("ctdName", {
description: "API name of Content Type Definition to purge",
alias: ["ctd"],
type: "string",
demandOption: false,
})
.option("deleteSchema", {
description: "remove CTD schema during CTD purge",
alias: ["deleteCtd"],
type: "boolean",
default: false,
demandOption: false,
}),
handler,
};

export { confirmPurge, handler, resolveApiKey, resolvePurgeOptions };

export default commandModule;
125 changes: 14 additions & 111 deletions src/purifier/purifier.js
Original file line number Diff line number Diff line change
@@ -1,120 +1,23 @@
import ora from "ora";
const purgeContentObjects = async (flotiqApi, options = {}) => {
const { type, spaceId, ctdName, deleteSchema = false } = options;

const purgeContentObjects = async (flotiqApi, internal = false, force = false) => {

let ctdsClearedOfRelations = 0;

let contentTypeDefinitions = await flotiqApi.fetchContentType(internal);

let i = 0;
let ctdArrFormerLength = contentTypeDefinitions.length;
let spinner;
while (contentTypeDefinitions.length) {
if (contentTypeDefinitions[i]) {
let objectsNotPurged = await removeContentObjects(contentTypeDefinitions[i], flotiqApi);
if (objectsNotPurged) {
i++;
} else {
contentTypeDefinitions.splice(i, 1);
}
} else {
i = 0;
if (contentTypeDefinitions.length !== ctdArrFormerLength) {
ctdArrFormerLength = contentTypeDefinitions.length
} else {
if (!force) {
console.log("Purge command stumbled upon relation loop in CTDs:");
while (i < contentTypeDefinitions.length) {
console.log(contentTypeDefinitions[i].name)
i++;
}
console.log("Use `flotiq purge [apiKey] --force` or remove conflicting relations manually");
return;
} else {
spinner = ora(`Cleaning data of relation loops, please do not stop the command or close the terminal... Content Types cleared of looped relations: ${ctdsClearedOfRelations}\n`).start();
await dropRelations(contentTypeDefinitions.slice(ctdsClearedOfRelations), flotiqApi);
ctdsClearedOfRelations++;
spinner.stop();
}
}
}
}
}

export default purgeContentObjects;

const dropRelations = (contentTypeDefinitions, flotiqApi) => {
const removeProperty = (ctd, property) => {
delete ctd.metaDefinition.propertiesConfig[property];
delete ctd.schemaDefinition.allOf[1].properties[property];
ctd.metaDefinition.order.splice(ctd.metaDefinition.order.indexOf(property), 1);
if (ctd.schemaDefinition.required.includes(property)) {
ctd.schemaDefinition.required.splice(ctd.schemaDefinition.required.indexOf(property), 1);
if (type === "space") {
if (!spaceId) {
throw new Error("Missing required option: spaceId");
}
return ctd;
}

const cloneObject = (obj) => {
return JSON.parse(JSON.stringify(obj));
return flotiqApi.purgeSpace(spaceId);
}

const clearContentType = async (ctd) => {
let ctdWithDroppedRelations = cloneObject(ctd);
for (let property in ctd.metaDefinition.propertiesConfig) {
const isRelationField = ctd.metaDefinition.propertiesConfig[property]?.validation?.hasOwnProperty("relationContenttype");
if (isRelationField) {
ctdWithDroppedRelations = removeProperty(ctdWithDroppedRelations, property);
}
}
await flotiqApi.updateContentTypeDefinition(ctdWithDroppedRelations.name, ctdWithDroppedRelations);
await flotiqApi.updateContentTypeDefinition(ctd.name, ctd);
}

for (let ctd in contentTypeDefinitions) {
for (let property in contentTypeDefinitions[ctd].metaDefinition.propertiesConfig) {
const isCtdWithRelations = contentTypeDefinitions[ctd].metaDefinition.propertiesConfig[property]?.validation?.hasOwnProperty("relationContenttype");
if (isCtdWithRelations) {
return clearContentType(contentTypeDefinitions[ctd]);
}
if (type === "ctd") {
if (!ctdName) {
throw new Error("Missing required option: ctdName");
}
}
}

const removeContentObjects = async (contentTypeDefinition, flotiqApi) => {

let limit = 100;
let page = 1;
let totalPages = 0;

while (totalPages !== page) {

const ctdName = contentTypeDefinition.name;
let contentObjects = (await flotiqApi.middleware.get(
`/content/${ctdName}?page=${page}&limit=${limit}`
)).data;

totalPages = contentObjects.total_pages;

if (contentObjects.count === 0) {
break;
}

let deleteQuery = [];
contentObjects.data.map(contentObject => {
deleteQuery.push(contentObject.id);
});

const response = await flotiqApi.middleware.post(
`/content/${ctdName}/batch-delete`,
deleteQuery,
{ validateStatus: (s) => s < 500 }
);
return flotiqApi.purgeCtd(ctdName, deleteSchema);
}

if (response.status === 400) {
return contentTypeDefinition;
}
throw new Error("Unsupported purge type. Use 'space' or 'ctd'.");
};

console.log(`${ctdName} - Page: ${page}/${totalPages}`, response.data);
page++;
}
}
export default purgeContentObjects;
Loading
Loading