Skip to content

Commit 480c909

Browse files
authored
fix: finish scheduler implementation (#4884)
1 parent bf6459e commit 480c909

34 files changed

+299
-246
lines changed

packages/api-headless-cms-scheduler/__tests__/actionHandlers.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe("Action Handlers", () => {
4545
modelId: MOCK_TARGET_MODEL_ID,
4646
targetId: entryResult.value.id,
4747
actionType: "Publish",
48-
scheduleOn: new Date(Date.now() + 100000).toISOString()
48+
scheduleFor: new Date(Date.now() + 100000).toISOString()
4949
});
5050

5151
// Assert scheduled actions
@@ -73,7 +73,7 @@ describe("Action Handlers", () => {
7373
modelId: MOCK_TARGET_MODEL_ID,
7474
targetId: entryResult.value.id,
7575
actionType: "Unpublish",
76-
scheduleOn: new Date(Date.now() + 1000000).toISOString()
76+
scheduleFor: new Date(Date.now() + 1000000).toISOString()
7777
});
7878

7979
// Execute action handler

packages/api-headless-cms-scheduler/src/features/CancelScheduledActionOnEntryChange/CancelScheduledActionOnDeleteHandler.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { EntryAfterDeleteHandler } from "@webiny/api-headless-cms/features/contentEntry/DeleteEntry/events";
2+
import { ListScheduledActionsUseCase, CancelScheduledActionUseCase } from "@webiny/api-scheduler";
3+
4+
/**
5+
* Cancels scheduled actions when an entry is deleted
6+
*
7+
* When a user deletes an entry, any scheduled publish/unpublish
8+
* actions for all of its revisions should be canceled since the entry
9+
* no longer exists.
10+
*/
11+
class CancelScheduledActionOnEntryDeleteHandlerImpl implements EntryAfterDeleteHandler.Interface {
12+
constructor(
13+
private listScheduledActions: ListScheduledActionsUseCase.Interface,
14+
private cancelScheduledEntryAction: CancelScheduledActionUseCase.Interface
15+
) {}
16+
17+
async handle(event: EntryAfterDeleteHandler.Event): Promise<void> {
18+
const { entry, model } = event.payload;
19+
20+
// Skip private models
21+
if (model.isPrivate) {
22+
return;
23+
}
24+
25+
const schedules = await this.listSchedules(model.modelId, entry.entryId);
26+
for (const action of schedules) {
27+
const cancelRes = await this.cancelScheduledEntryAction.execute(action.id);
28+
if (cancelRes.isFail()) {
29+
// Silently ignore errors - this is non-critical cleanup.
30+
// The entry was deleted successfully, cancelling scheduled actions is best-effort.
31+
}
32+
}
33+
}
34+
35+
private async listSchedules(modelId: string, entryId: string) {
36+
const actionsResult = await this.listScheduledActions.execute({
37+
limit: 10000,
38+
where: {
39+
namespace: `Cms/Entry/${modelId}`,
40+
targetId_startsWith: `${entryId}#`
41+
}
42+
});
43+
44+
return actionsResult.value.items;
45+
}
46+
}
47+
48+
export const CancelScheduledActionOnEntryDeleteHandler =
49+
EntryAfterDeleteHandler.createImplementation({
50+
implementation: CancelScheduledActionOnEntryDeleteHandlerImpl,
51+
dependencies: [ListScheduledActionsUseCase, CancelScheduledActionUseCase]
52+
});
Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { EntryAfterPublishHandler } from "@webiny/api-headless-cms/features/contentEntry/PublishEntry/events";
2-
import { CancelScheduledEntryActionUseCase } from "../CancelScheduledEntryAction/index.js";
2+
import { ListScheduledActionsUseCase, CancelScheduledActionUseCase } from "@webiny/api-scheduler";
33

44
/**
5-
* Cancels scheduled actions when an entry is manually published
5+
* Cancels scheduled "publish" when an entry is manually published
66
*
7-
* When a user manually publishes an entry, any scheduled publish/unpublish
8-
* actions for that entry should be cancelled since the manual action
7+
* When a user manually publishes an entry, any scheduled publish
8+
* action for that entry should be canceled since the manual action
99
* takes precedence.
1010
*/
1111
class CancelScheduledActionOnPublishHandlerImpl implements EntryAfterPublishHandler.Interface {
12-
constructor(private cancelScheduledEntryAction: CancelScheduledEntryActionUseCase.Interface) {}
12+
constructor(
13+
private listScheduledActions: ListScheduledActionsUseCase.Interface,
14+
private cancelScheduledEntryAction: CancelScheduledActionUseCase.Interface
15+
) {}
1316

1417
async handle(event: EntryAfterPublishHandler.Event): Promise<void> {
1518
const { entry, model } = event.payload;
@@ -19,19 +22,27 @@ class CancelScheduledActionOnPublishHandlerImpl implements EntryAfterPublishHand
1922
return;
2023
}
2124

22-
try {
23-
await this.cancelScheduledEntryAction.execute({
24-
modelId: model.modelId,
25+
const actionsResult = await this.listScheduledActions.execute({
26+
where: {
27+
namespace: `Cms/Entry/${model.modelId}`,
28+
actionType: "Publish",
2529
targetId: entry.id
26-
});
27-
} catch {
28-
// Silently ignore errors - this is non-critical cleanup
29-
// The entry was published successfully, cancelling scheduled actions is best-effort
30+
}
31+
});
32+
33+
const actions = actionsResult.value.items;
34+
35+
for (const action of actions) {
36+
const cancelRes = await this.cancelScheduledEntryAction.execute(action.id);
37+
if (cancelRes.isFail()) {
38+
// Silently ignore errors - this is non-critical cleanup.
39+
// Even if a schedule runs on an already published action, nothing bad will happen.
40+
}
3041
}
3142
}
3243
}
3344

3445
export const CancelScheduledActionOnPublishHandler = EntryAfterPublishHandler.createImplementation({
3546
implementation: CancelScheduledActionOnPublishHandlerImpl,
36-
dependencies: [CancelScheduledEntryActionUseCase]
47+
dependencies: [ListScheduledActionsUseCase, CancelScheduledActionUseCase]
3748
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { EntryAfterDeleteHandler } from "@webiny/api-headless-cms/features/contentEntry/DeleteEntry/events";
2+
import { CancelScheduledActionUseCase, ListScheduledActionsUseCase } from "@webiny/api-scheduler";
3+
4+
/**
5+
* Cancels scheduled actions when an entry revision is deleted
6+
*
7+
* When a user deletes an entry revision, any scheduled publish/unpublish
8+
* action for that revision should be canceled since the revision
9+
* no longer exists.
10+
*/
11+
class CancelScheduledActionOnDeleteHandlerImpl implements EntryAfterDeleteHandler.Interface {
12+
constructor(
13+
private listScheduledActions: ListScheduledActionsUseCase.Interface,
14+
private cancelScheduledEntryAction: CancelScheduledActionUseCase.Interface
15+
) {}
16+
17+
async handle(event: EntryAfterDeleteHandler.Event): Promise<void> {
18+
const { entry, model } = event.payload;
19+
20+
// Skip private models
21+
if (model.isPrivate) {
22+
return;
23+
}
24+
25+
const actionsResult = await this.listScheduledActions.execute({
26+
where: {
27+
namespace: `Cms/Entry/${model.modelId}`,
28+
targetId: entry.id
29+
}
30+
});
31+
32+
const actions = actionsResult.value.items;
33+
34+
for (const action of actions) {
35+
const cancelRes = await this.cancelScheduledEntryAction.execute(action.id);
36+
if (cancelRes.isFail()) {
37+
// Silently ignore errors - this is non-critical cleanup.
38+
// Entry was deleted successfully, cancelling scheduled actions is best-effort.
39+
}
40+
}
41+
}
42+
}
43+
44+
export const CancelScheduledActionOnRevisionDeleteHandler =
45+
EntryAfterDeleteHandler.createImplementation({
46+
implementation: CancelScheduledActionOnDeleteHandlerImpl,
47+
dependencies: [ListScheduledActionsUseCase, CancelScheduledActionUseCase]
48+
});
Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { EntryAfterUnpublishHandler } from "@webiny/api-headless-cms/features/contentEntry/UnpublishEntry/events";
2-
import { CancelScheduledEntryActionUseCase } from "../CancelScheduledEntryAction/index.js";
2+
import { CancelScheduledActionUseCase, ListScheduledActionsUseCase } from "@webiny/api-scheduler";
33

44
/**
5-
* Cancels scheduled actions when an entry is manually unpublished
5+
* Cancels scheduled action when an entry is manually unpublished
66
*
7-
* When a user manually unpublishes an entry, any scheduled publish/unpublish
8-
* actions for that entry should be cancelled since the manual action
7+
* When a user manually unpublishes an entry revision, any scheduled unpublish
8+
* action for that revision should be canceled since the manual action
99
* takes precedence.
1010
*/
1111
class CancelScheduledActionOnUnpublishHandlerImpl implements EntryAfterUnpublishHandler.Interface {
12-
constructor(private cancelScheduledEntryAction: CancelScheduledEntryActionUseCase.Interface) {}
12+
constructor(
13+
private listScheduledActions: ListScheduledActionsUseCase.Interface,
14+
private cancelScheduledEntryAction: CancelScheduledActionUseCase.Interface
15+
) {}
1316

1417
async handle(event: EntryAfterUnpublishHandler.Event): Promise<void> {
1518
const { entry, model } = event.payload;
@@ -19,20 +22,28 @@ class CancelScheduledActionOnUnpublishHandlerImpl implements EntryAfterUnpublish
1922
return;
2023
}
2124

22-
try {
23-
await this.cancelScheduledEntryAction.execute({
24-
modelId: model.modelId,
25+
const actionsResult = await this.listScheduledActions.execute({
26+
where: {
27+
namespace: `Cms/Entry/${model.modelId}`,
28+
actionType: "Unpublish",
2529
targetId: entry.id
26-
});
27-
} catch {
28-
// Silently ignore errors - this is non-critical cleanup
29-
// The entry was unpublished successfully, cancelling scheduled actions is best-effort
30+
}
31+
});
32+
33+
const actions = actionsResult.value.items;
34+
35+
for (const action of actions) {
36+
const cancelRes = await this.cancelScheduledEntryAction.execute(action.id);
37+
if (cancelRes.isFail()) {
38+
// Silently ignore errors - this is non-critical cleanup.
39+
// Entry was unpublished successfully, cancelling scheduled actions is best-effort.
40+
}
3041
}
3142
}
3243
}
3344

3445
export const CancelScheduledActionOnUnpublishHandler =
3546
EntryAfterUnpublishHandler.createImplementation({
3647
implementation: CancelScheduledActionOnUnpublishHandlerImpl,
37-
dependencies: [CancelScheduledEntryActionUseCase]
48+
dependencies: [ListScheduledActionsUseCase, CancelScheduledActionUseCase]
3849
});

packages/api-headless-cms-scheduler/src/features/CancelScheduledActionOnEntryChange/feature.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createFeature } from "@webiny/feature/api";
22
import { CancelScheduledActionOnPublishHandler } from "./CancelScheduledActionOnPublishHandler.js";
33
import { CancelScheduledActionOnUnpublishHandler } from "./CancelScheduledActionOnUnpublishHandler.js";
4-
import { CancelScheduledActionOnDeleteHandler } from "./CancelScheduledActionOnDeleteHandler.js";
4+
import { CancelScheduledActionOnEntryDeleteHandler } from "./CancelScheduledActionOnEntryDeleteHandler.js";
5+
import { CancelScheduledActionOnRevisionDeleteHandler } from "./CancelScheduledActionOnRevisionDeleteHandler.js";
56

67
/**
78
* CancelScheduledActionOnEntryChange Feature
@@ -15,6 +16,7 @@ export const CancelScheduledActionOnEntryChangeFeature = createFeature({
1516
register(container) {
1617
container.register(CancelScheduledActionOnPublishHandler);
1718
container.register(CancelScheduledActionOnUnpublishHandler);
18-
container.register(CancelScheduledActionOnDeleteHandler);
19+
container.register(CancelScheduledActionOnEntryDeleteHandler);
20+
container.register(CancelScheduledActionOnRevisionDeleteHandler);
1921
}
2022
});
Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,23 @@
11
import { Result } from "@webiny/feature/api";
2-
import { ListScheduledActionsUseCase } from "@webiny/api-scheduler/features/ListScheduledActions";
32
import { CancelScheduledActionUseCase } from "@webiny/api-scheduler/features/CancelScheduledAction";
43
import { CancelScheduledEntryActionUseCase as UseCaseAbstraction } from "./abstractions.js";
54

65
/**
76
* Cancels a scheduled CMS entry action
87
*/
98
class CancelScheduledEntryActionUseCaseImpl implements UseCaseAbstraction.Interface {
10-
constructor(
11-
private listScheduledActions: ListScheduledActionsUseCase.Interface,
12-
private cancelScheduledAction: CancelScheduledActionUseCase.Interface
13-
) {}
9+
constructor(private cancelScheduledAction: CancelScheduledActionUseCase.Interface) {}
1410

15-
async execute(
16-
input: UseCaseAbstraction.Input
17-
): Promise<Result<void, UseCaseAbstraction.Error>> {
18-
const namespace = `Cms/Entry/${input.modelId}`;
19-
20-
const actionsResult = await this.listScheduledActions.execute({
21-
where: {
22-
namespace,
23-
targetId: input.targetId
24-
}
25-
});
26-
27-
const actions = actionsResult.value.items;
28-
29-
for (const action of actions) {
30-
const cancelRes = await this.cancelScheduledAction.execute(action.id);
31-
if (cancelRes.isFail()) {
32-
return Result.fail(cancelRes.error);
33-
}
11+
async execute(scheduleId: string): Promise<Result<void, UseCaseAbstraction.Error>> {
12+
const cancelRes = await this.cancelScheduledAction.execute(scheduleId);
13+
if (cancelRes.isFail()) {
14+
return Result.fail(cancelRes.error);
3415
}
35-
3616
return Result.ok();
3717
}
3818
}
3919

4020
export const CancelScheduledEntryActionUseCase = UseCaseAbstraction.createImplementation({
4121
implementation: CancelScheduledEntryActionUseCaseImpl,
42-
dependencies: [ListScheduledActionsUseCase, CancelScheduledActionUseCase]
22+
dependencies: [CancelScheduledActionUseCase]
4323
});

packages/api-headless-cms-scheduler/src/features/CancelScheduledEntryAction/abstractions.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ import {
1212
* This is a convenience use case for canceling scheduled CMS entry actions.
1313
*/
1414

15-
export interface ICancelScheduledEntryActionInput {
16-
modelId: string;
17-
targetId: string;
18-
}
19-
2015
export interface ICancelScheduledEntryActionErrors {
2116
notFound: ScheduledActionNotFoundError;
2217
persistence: ScheduledActionPersistenceError;
@@ -27,16 +22,13 @@ type CancelScheduledEntryActionError =
2722
ICancelScheduledEntryActionErrors[keyof ICancelScheduledEntryActionErrors];
2823

2924
export interface ICancelScheduledEntryActionUseCase {
30-
execute(
31-
input: ICancelScheduledEntryActionInput
32-
): Promise<Result<void, CancelScheduledEntryActionError>>;
25+
execute(scheduleId: string): Promise<Result<void, CancelScheduledEntryActionError>>;
3326
}
3427

3528
export const CancelScheduledEntryActionUseCase =
3629
createAbstraction<ICancelScheduledEntryActionUseCase>("CancelScheduledEntryActionUseCase");
3730

3831
export namespace CancelScheduledEntryActionUseCase {
3932
export type Interface = ICancelScheduledEntryActionUseCase;
40-
export type Input = ICancelScheduledEntryActionInput;
4133
export type Error = CancelScheduledEntryActionError;
4234
}

0 commit comments

Comments
 (0)