-
Notifications
You must be signed in to change notification settings - Fork 618
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api-headless-cms-tasks): delete model and entries (#4429)
- Loading branch information
1 parent
abc82dd
commit 7009bc7
Showing
36 changed files
with
1,521 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
packages/api-headless-cms-tasks/src/tasks/deleteModel/DeleteModelRunner.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import { ITaskResponse, ITaskResponseResult, ITaskRunParams } from "@webiny/tasks"; | ||
import { HcmsTasksContext } from "~/types"; | ||
import { IDeleteModelTaskInput, IDeleteModelTaskOutput, IStoreValue } from "./types"; | ||
import { CmsEntryListWhere, CmsModel } from "@webiny/api-headless-cms/types"; | ||
import { createStoreKey, createStoreValue } from "~/tasks/deleteModel/helpers/store"; | ||
|
||
export interface IDeleteModelRunnerParams< | ||
C extends HcmsTasksContext, | ||
I extends IDeleteModelTaskInput, | ||
O extends IDeleteModelTaskOutput | ||
> { | ||
taskId: string; | ||
context: C; | ||
response: ITaskResponse<I, O>; | ||
} | ||
|
||
export type IExecuteParams< | ||
C extends HcmsTasksContext, | ||
I extends IDeleteModelTaskInput, | ||
O extends IDeleteModelTaskOutput | ||
> = Omit<ITaskRunParams<C, I, O>, "context" | "response" | "store" | "timer" | "trigger">; | ||
|
||
export class DeleteModelRunner< | ||
C extends HcmsTasksContext, | ||
I extends IDeleteModelTaskInput, | ||
O extends IDeleteModelTaskOutput | ||
> { | ||
private readonly taskId: string; | ||
private readonly context: C; | ||
private readonly response: ITaskResponse<I, O>; | ||
|
||
public constructor(params: IDeleteModelRunnerParams<C, I, O>) { | ||
this.taskId = params.taskId; | ||
this.context = params.context; | ||
this.response = params.response; | ||
} | ||
|
||
public async execute(params: IExecuteParams<C, I, O>): Promise<ITaskResponseResult<I, O>> { | ||
const { input, isCloseToTimeout, isAborted } = params; | ||
|
||
const model = await this.getModel(input.modelId); | ||
/** | ||
* We need to mark model as getting deleted, so that we can prevent any further operations on it. | ||
*/ | ||
const gettingDeleted = await this.getDeletingTag(model); | ||
|
||
if (!gettingDeleted) { | ||
await this.addDeletingTag(model); | ||
} | ||
|
||
let hasMoreItems = false; | ||
let lastDeletedId: string | undefined = input.lastDeletedId; | ||
do { | ||
if (isAborted()) { | ||
/** | ||
* If the task was aborted, we need to remove the task tag from the model. | ||
*/ | ||
await this.removeDeletingTag(model); | ||
return this.response.aborted(); | ||
} else if (isCloseToTimeout()) { | ||
return this.response.continue({ | ||
...input, | ||
lastDeletedId | ||
}); | ||
} | ||
let where: CmsEntryListWhere | undefined = undefined; | ||
if (lastDeletedId) { | ||
where = { | ||
entryId_gte: lastDeletedId | ||
}; | ||
} | ||
const [items, meta] = await this.context.cms.listLatestEntries(model, { | ||
limit: 1000, | ||
where, | ||
sort: ["id_ASC"] | ||
}); | ||
for (const item of items) { | ||
try { | ||
await this.context.cms.deleteEntry(model, item.id, { | ||
permanently: true, | ||
force: true | ||
}); | ||
} catch (ex) { | ||
console.error("Failed to delete entry.", { | ||
model: model.modelId, | ||
id: item.id | ||
}); | ||
return this.response.error( | ||
new Error(`Failed to delete entry "${item.id}". Cannot continue.`) | ||
); | ||
} | ||
lastDeletedId = item.entryId; | ||
} | ||
|
||
hasMoreItems = meta.hasMoreItems; | ||
} while (hasMoreItems); | ||
/** | ||
* Let's do one more check. If there are items, continue the task with 5 seconds delay. | ||
*/ | ||
const [items] = await this.context.cms.listLatestEntries(model, { | ||
limit: 1 | ||
}); | ||
if (items.length > 0) { | ||
console.log("There are still items to be deleted. Continuing the task."); | ||
return this.response.continue( | ||
{ | ||
...input | ||
}, | ||
{ | ||
seconds: 5 | ||
} | ||
); | ||
} | ||
|
||
let hasMoreFolders = false; | ||
do { | ||
const [items, meta] = await this.context.aco.folder.list({ | ||
where: { | ||
type: `cms:${model.modelId}` | ||
}, | ||
limit: 1000 | ||
}); | ||
for (const item of items) { | ||
try { | ||
await this.context.aco.folder.delete(item.id); | ||
} catch (ex) { | ||
console.error(`Failed to delete folder "${item.id}".`, ex); | ||
return this.response.error(ex); | ||
} | ||
} | ||
|
||
hasMoreFolders = meta.hasMoreItems; | ||
} while (hasMoreFolders); | ||
|
||
/** | ||
* When there is no more records to be deleted, let's delete the model, if it's not a plugin. | ||
*/ | ||
if (model.isPlugin) { | ||
return this.response.done(); | ||
} | ||
try { | ||
await this.context.cms.deleteModel(model.modelId); | ||
} catch (ex) { | ||
await this.removeDeletingTag(model); | ||
const message = `Failed to delete model "${model.modelId}".`; | ||
console.error(message); | ||
return this.response.error(ex); | ||
} | ||
|
||
return this.response.done(); | ||
} | ||
|
||
private async getModel(modelId: string): Promise<CmsModel> { | ||
const model = await this.context.cms.getModel(modelId); | ||
if (!model) { | ||
throw new Error(`Model "${modelId}" not found.`); | ||
} | ||
return model; | ||
} | ||
|
||
private async getDeletingTag(model: Pick<CmsModel, "modelId">): Promise<IStoreValue | null> { | ||
const key = createStoreKey(model); | ||
const value = await this.context.db.store.getValue<IStoreValue>(key); | ||
|
||
return value.data || null; | ||
} | ||
|
||
private async addDeletingTag(model: Pick<CmsModel, "modelId">): Promise<void> { | ||
const key = createStoreKey(model); | ||
const value = createStoreValue({ | ||
model, | ||
identity: this.context.security.getIdentity(), | ||
task: this.taskId | ||
}); | ||
await this.context.db.store.storeValue(key, value); | ||
} | ||
|
||
private async removeDeletingTag(model: Pick<CmsModel, "modelId">): Promise<void> { | ||
const key = createStoreKey(model); | ||
await this.context.db.store.removeValue(key); | ||
} | ||
} | ||
|
||
export const createDeleteModelRunner = < | ||
C extends HcmsTasksContext, | ||
I extends IDeleteModelTaskInput, | ||
O extends IDeleteModelTaskOutput | ||
>( | ||
params: IDeleteModelRunnerParams<C, I, O> | ||
) => { | ||
return new DeleteModelRunner<C, I, O>(params); | ||
}; |
1 change: 1 addition & 0 deletions
1
packages/api-headless-cms-tasks/src/tasks/deleteModel/constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const DELETE_MODEL_TASK = "deleteModelAndEntries"; |
72 changes: 72 additions & 0 deletions
72
packages/api-headless-cms-tasks/src/tasks/deleteModel/graphql/abortDeleteModel.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { HcmsTasksContext } from "~/types"; | ||
import { | ||
IDeleteCmsModelTask, | ||
IDeleteModelTaskInput, | ||
IDeleteModelTaskOutput, | ||
IStoreValue | ||
} from "~/tasks/deleteModel/types"; | ||
import { DELETE_MODEL_TASK } from "~/tasks/deleteModel/constants"; | ||
import { WebinyError } from "@webiny/error"; | ||
import { getStatus } from "~/tasks/deleteModel/graphql/status"; | ||
import { createStoreKey } from "~/tasks/deleteModel/helpers/store"; | ||
|
||
export interface IAbortDeleteModelParams { | ||
readonly context: Pick<HcmsTasksContext, "cms" | "tasks" | "db">; | ||
readonly modelId: string; | ||
} | ||
|
||
export const abortDeleteModel = async ( | ||
params: IAbortDeleteModelParams | ||
): Promise<IDeleteCmsModelTask> => { | ||
const { context, modelId } = params; | ||
|
||
const model = await context.cms.getModel(modelId); | ||
|
||
await context.cms.accessControl.ensureCanAccessModel({ | ||
model, | ||
rwd: "d" | ||
}); | ||
|
||
await context.cms.accessControl.ensureCanAccessEntry({ | ||
model, | ||
rwd: "w" | ||
}); | ||
|
||
const storeKey = createStoreKey(model); | ||
|
||
const result = await context.db.store.getValue<IStoreValue>(storeKey); | ||
|
||
const taskId = result.data?.task; | ||
if (!taskId) { | ||
if (result.error) { | ||
throw result.error; | ||
} | ||
throw new Error(`Model "${modelId}" is not being deleted.`); | ||
} | ||
|
||
await context.db.store.removeValue(storeKey); | ||
|
||
const task = await context.tasks.getTask<IDeleteModelTaskInput, IDeleteModelTaskOutput>(taskId); | ||
if (task?.definitionId !== DELETE_MODEL_TASK) { | ||
throw new WebinyError({ | ||
message: `The task which is deleting a model cannot be found. Please check Step Functions for more info. Task id: ${taskId}`, | ||
code: "DELETE_MODEL_TASK_NOT_FOUND", | ||
data: { | ||
model: model.modelId, | ||
task: taskId | ||
} | ||
}); | ||
} | ||
|
||
const abortedTask = await context.tasks.abort<IDeleteModelTaskInput, IDeleteModelTaskOutput>({ | ||
id: task.id, | ||
message: "User aborted the task." | ||
}); | ||
|
||
return { | ||
id: abortedTask.id, | ||
status: getStatus(abortedTask.taskStatus), | ||
total: abortedTask.output?.total || 0, | ||
deleted: abortedTask.output?.deleted || 0 | ||
}; | ||
}; |
66 changes: 66 additions & 0 deletions
66
packages/api-headless-cms-tasks/src/tasks/deleteModel/graphql/fullyDeleteModel.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { HcmsTasksContext } from "~/types"; | ||
import { DELETE_MODEL_TASK } from "~/tasks/deleteModel/constants"; | ||
import { IDeleteCmsModelTask, IDeleteModelTaskInput, IStoreValue } from "~/tasks/deleteModel/types"; | ||
import { getStatus } from "~/tasks/deleteModel/graphql/status"; | ||
import { createStoreKey, createStoreValue } from "~/tasks/deleteModel/helpers/store"; | ||
|
||
export interface IFullyDeleteModelParams { | ||
readonly context: Pick<HcmsTasksContext, "cms" | "tasks" | "db" | "security">; | ||
readonly modelId: string; | ||
} | ||
|
||
export const fullyDeleteModel = async ( | ||
params: IFullyDeleteModelParams | ||
): Promise<IDeleteCmsModelTask> => { | ||
const { context, modelId } = params; | ||
|
||
const model = await context.cms.getModel(modelId); | ||
|
||
if (model.isPrivate) { | ||
throw new Error(`Cannot delete private model.`); | ||
} | ||
|
||
await context.cms.accessControl.ensureCanAccessModel({ | ||
model, | ||
rwd: "d" | ||
}); | ||
|
||
await context.cms.accessControl.ensureCanAccessEntry({ | ||
model, | ||
rwd: "w" | ||
}); | ||
|
||
if (!model) { | ||
throw new Error(`Model "${modelId}" not found.`); | ||
} | ||
const storeKey = createStoreKey(model); | ||
const result = await context.db.store.getValue<IStoreValue>(storeKey); | ||
const taskId = result.data?.task; | ||
if (taskId) { | ||
throw new Error(`Model "${modelId}" is already getting deleted. Task id: ${taskId}.`); | ||
} | ||
|
||
const task = await context.tasks.trigger<IDeleteModelTaskInput>({ | ||
input: { | ||
modelId | ||
}, | ||
definition: DELETE_MODEL_TASK, | ||
name: `Fully delete model: ${modelId}` | ||
}); | ||
|
||
await context.db.store.storeValue( | ||
storeKey, | ||
createStoreValue({ | ||
model, | ||
identity: context.security.getIdentity(), | ||
task: task.id | ||
}) | ||
); | ||
|
||
return { | ||
id: task.id, | ||
status: getStatus(task.taskStatus), | ||
total: 0, | ||
deleted: 0 | ||
}; | ||
}; |
Oops, something went wrong.