Skip to content

Commit 877c650

Browse files
authored
[DT-2684] feat: rollback custom type update on failed CR sync (#1624)
1 parent 5bdfb14 commit 877c650

2 files changed

Lines changed: 204 additions & 111 deletions

File tree

packages/manager/src/managers/customTypes/CustomTypesManager.ts

Lines changed: 140 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,14 @@ type CustomTypesMachineManagerDeleteCustomTypeReturnType = {
8888
errors: (DecodeError | HookError)[];
8989
};
9090

91-
type CustomTypeFieldIdChangedMeta = NonNullable<
92-
NonNullable<CustomTypeUpdateHookData["updateMeta"]>["fieldIdChanged"]
93-
>;
91+
type CustomTypesMachineManagerUpdateCustomTypeReturnType = {
92+
errors: (DecodeError | HookError)[];
93+
};
94+
95+
type CustomTypeFieldIdChangedMeta = {
96+
previousPath: string[];
97+
newPath: string[];
98+
};
9499

95100
type CrCustomType =
96101
| string
@@ -193,72 +198,95 @@ export class CustomTypesManager extends BaseManager {
193198
* property.
194199
*/
195200
private async updateContentRelationships(
196-
args: CustomTypeUpdateHookData,
197-
): Promise<OnlyHookErrors<CallHookReturnType<CustomTypeUpdateHook>>> {
201+
args: { model: CustomType } & CustomTypeFieldIdChangedMeta,
202+
): Promise<
203+
OnlyHookErrors<CallHookReturnType<CustomTypeUpdateHook>> & {
204+
rollback?: () => Promise<void>;
205+
}
206+
> {
198207
assertPluginsInitialized(this.sliceMachinePluginRunner);
199208

200-
const { model, updateMeta } = args;
201-
202-
if (updateMeta?.fieldIdChanged) {
203-
let { previousPath, newPath } = updateMeta.fieldIdChanged;
204-
205-
if (previousPath.join(".") !== newPath.join(".")) {
206-
previousPath = [model.id, ...previousPath];
207-
newPath = [model.id, ...newPath];
209+
const { model } = args;
210+
let { newPath, previousPath } = args;
211+
212+
if (previousPath.join(".") !== newPath.join(".")) {
213+
previousPath = [model.id, ...previousPath];
214+
newPath = [model.id, ...newPath];
215+
216+
const crUpdates: {
217+
updatePromise: Promise<{ errors: HookError[] }>;
218+
rollback: () => void;
219+
}[] = [];
220+
221+
// Find existing content relationships that link to the renamed field id in
222+
// any custom type and update them to use the new one.
223+
const customTypes = await this.readAllCustomTypes();
224+
225+
updateCustomTypeContentRelationships({
226+
models: customTypes.models,
227+
onUpdate: ({ previousModel, model: updatedModel }) => {
228+
assertPluginsInitialized(this.sliceMachinePluginRunner);
229+
230+
crUpdates.push({
231+
updatePromise: this.sliceMachinePluginRunner?.callHook(
232+
"custom-type:update",
233+
{ model: updatedModel },
234+
),
235+
rollback: () => {
236+
this.sliceMachinePluginRunner?.callHook("custom-type:update", {
237+
model: previousModel,
238+
});
239+
},
240+
});
241+
},
242+
previousPath,
243+
newPath,
244+
});
208245

209-
const crUpdates: Promise<{ errors: HookError[] }>[] = [];
246+
// Find existing slice with content relationships that link to the renamed
247+
// field id in all libraries and update them to use the new one.
248+
const { libraries } = await this.slices.readAllSliceLibraries();
210249

211-
// Find existing content relationships that link to the renamed field id in
212-
// any custom type and update them to use the new one.
213-
const customTypes = await this.readAllCustomTypes();
250+
for (const library of libraries) {
251+
const slices = await this.slices.readAllSlicesForLibrary({
252+
libraryID: library.libraryID,
253+
});
214254

215-
updateCustomTypeContentRelationships({
216-
models: customTypes.models,
217-
onUpdate: (model) => {
218-
pushIfDefined(
219-
crUpdates,
220-
this.sliceMachinePluginRunner?.callHook("custom-type:update", {
221-
model,
222-
}),
223-
);
255+
updateSharedSliceContentRelationships({
256+
models: slices.models,
257+
onUpdate: ({ previousModel, model: updatedModel }) => {
258+
assertPluginsInitialized(this.sliceMachinePluginRunner);
259+
260+
crUpdates.push({
261+
updatePromise: this.sliceMachinePluginRunner?.callHook(
262+
"slice:update",
263+
{ libraryID: library.libraryID, model: updatedModel },
264+
),
265+
rollback: () => {
266+
this.sliceMachinePluginRunner?.callHook("slice:update", {
267+
libraryID: library.libraryID,
268+
model: previousModel,
269+
});
270+
},
271+
});
224272
},
225273
previousPath,
226274
newPath,
227275
});
276+
}
228277

229-
// Find existing slice with content relationships that link to the renamed
230-
// field id in all libraries and update them to use the new one.
231-
const { libraries } = await this.slices.readAllSliceLibraries();
232-
233-
for (const library of libraries) {
234-
const slices = await this.slices.readAllSlicesForLibrary({
235-
libraryID: library.libraryID,
236-
});
237-
238-
updateSharedSliceContentRelationships({
239-
models: slices.models,
240-
onUpdate: (model) => {
241-
pushIfDefined(
242-
crUpdates,
243-
this.sliceMachinePluginRunner?.callHook("slice:update", {
244-
libraryID: library.libraryID,
245-
model,
246-
}),
247-
);
248-
},
249-
previousPath,
250-
newPath,
251-
});
252-
}
253-
254-
// Process all the Content Relationship updates at once.
255-
const crUpdatesResult = await Promise.all(crUpdates);
278+
// Process all the Content Relationship updates at once.
279+
const crUpdatesResult = await Promise.all(
280+
crUpdates.map((update) => update.updatePromise),
281+
);
256282

257-
if (crUpdatesResult.some((result) => result.errors.length > 0)) {
258-
return {
259-
errors: crUpdatesResult.flatMap((result) => result.errors),
260-
};
261-
}
283+
if (crUpdatesResult.some((result) => result.errors.length > 0)) {
284+
return {
285+
errors: crUpdatesResult.flatMap((result) => result.errors),
286+
rollback: async () => {
287+
await Promise.all(crUpdates.map((update) => update.rollback()));
288+
},
289+
};
262290
}
263291
}
264292

@@ -267,19 +295,56 @@ export class CustomTypesManager extends BaseManager {
267295

268296
async updateCustomType(
269297
args: CustomTypeUpdateHookData,
270-
): Promise<OnlyHookErrors<CallHookReturnType<CustomTypeUpdateHook>>> {
298+
): Promise<CustomTypesMachineManagerUpdateCustomTypeReturnType> {
271299
assertPluginsInitialized(this.sliceMachinePluginRunner);
300+
const { model } = args;
301+
const { fieldIdChanged } = args.updateMeta ?? {};
272302

273-
const hookResult = await this.sliceMachinePluginRunner.callHook(
303+
let previousCustomType: CustomType | undefined;
304+
305+
if (fieldIdChanged) {
306+
const customTypeRead = await this.readCustomType({ id: model.id });
307+
308+
if (customTypeRead.errors.length > 0) {
309+
return { errors: customTypeRead.errors };
310+
}
311+
if (!customTypeRead.model) {
312+
throw new Error(
313+
`readCustomType succeeded reading custom type ${model.id} but model is undefined.`,
314+
);
315+
}
316+
317+
previousCustomType = customTypeRead.model;
318+
}
319+
320+
const customTypeUpdateResult = await this.sliceMachinePluginRunner.callHook(
274321
"custom-type:update",
275-
args,
322+
{ model },
276323
);
277324

278-
if (args.updateMeta?.fieldIdChanged) {
279-
await this.updateContentRelationships(args);
325+
if (customTypeUpdateResult.errors.length > 0) {
326+
return { errors: customTypeUpdateResult.errors };
280327
}
281328

282-
return { errors: hookResult.errors };
329+
if (previousCustomType && fieldIdChanged) {
330+
const crUpdateResult = await this.updateContentRelationships({
331+
...fieldIdChanged,
332+
model: previousCustomType,
333+
});
334+
335+
if (crUpdateResult.errors.length > 0) {
336+
// put the previous custom type back
337+
await this.sliceMachinePluginRunner?.callHook("custom-type:update", {
338+
model: previousCustomType,
339+
});
340+
// revert the content relationships updates
341+
await crUpdateResult.rollback?.();
342+
343+
return { errors: crUpdateResult.errors };
344+
}
345+
}
346+
347+
return { errors: [] };
283348
}
284349

285350
async renameCustomType(
@@ -620,13 +685,13 @@ function updateFieldContentRelationships<
620685
export function updateCustomTypeContentRelationships(
621686
args: {
622687
models: { model: CustomType }[];
623-
onUpdate: (model: CustomType) => void;
688+
onUpdate: (model: { previousModel: CustomType; model: CustomType }) => void;
624689
} & CustomTypeFieldIdChangedMeta,
625690
): void {
626691
const { models, previousPath, newPath, onUpdate } = args;
627692

628693
for (const { model: customType } of models) {
629-
const updatedCustomTypeModel = traverseCustomType({
694+
const updatedCustomType = traverseCustomType({
630695
customType,
631696
onField: ({ field }) => {
632697
return updateFieldContentRelationships({
@@ -637,22 +702,25 @@ export function updateCustomTypeContentRelationships(
637702
},
638703
});
639704

640-
if (!isEqualModel(customType, updatedCustomTypeModel)) {
641-
onUpdate(updatedCustomTypeModel);
705+
if (!isEqualModel(customType, updatedCustomType)) {
706+
onUpdate({ model: updatedCustomType, previousModel: customType });
642707
}
643708
}
644709
}
645710

646711
export function updateSharedSliceContentRelationships(
647712
args: {
648713
models: { model: SharedSlice }[];
649-
onUpdate: (model: SharedSlice) => void;
714+
onUpdate: (model: {
715+
previousModel: SharedSlice;
716+
model: SharedSlice;
717+
}) => void;
650718
} & CustomTypeFieldIdChangedMeta,
651719
): void {
652720
const { models, previousPath, newPath, onUpdate } = args;
653721

654722
for (const { model: slice } of models) {
655-
const updatedSliceModel = traverseSharedSlice({
723+
const updateSlice = traverseSharedSlice({
656724
path: ["."],
657725
slice,
658726
onField: ({ field }) => {
@@ -664,8 +732,8 @@ export function updateSharedSliceContentRelationships(
664732
},
665733
});
666734

667-
if (!isEqualModel(slice, updatedSliceModel)) {
668-
onUpdate(updatedSliceModel);
735+
if (!isEqualModel(slice, updateSlice)) {
736+
onUpdate({ model: updateSlice, previousModel: slice });
669737
}
670738
}
671739
}
@@ -684,9 +752,3 @@ function shallowCloneIfObject<T>(value: T): T {
684752

685753
return value;
686754
}
687-
688-
function pushIfDefined<T>(array: T[], value: T | undefined) {
689-
if (value) {
690-
array.push(value);
691-
}
692-
}

0 commit comments

Comments
 (0)