@slicemachine/manager 0.24.15-beta.5 → 0.24.15-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,13 @@ import * as t from "io-ts";
2
2
  import * as prismicCustomTypesClient from "@prismicio/custom-types-client";
3
3
  import {
4
4
  CustomType,
5
+ Group,
6
+ NestableWidget,
7
+ NestedGroup,
5
8
  SharedSlice,
9
+ UID,
10
+ traverseCustomType,
11
+ traverseSharedSlice,
6
12
  } from "@prismicio/types-internal/lib/customtypes";
7
13
  import {
8
14
  CallHookReturnType,
@@ -82,6 +88,25 @@ type CustomTypesMachineManagerDeleteCustomTypeReturnType = {
82
88
  errors: (DecodeError | HookError)[];
83
89
  };
84
90
 
91
+ type CustomTypesMachineManagerUpdateCustomTypeReturnType = {
92
+ errors: (DecodeError | HookError)[];
93
+ };
94
+
95
+ type CustomTypeFieldIdChangedMeta = {
96
+ previousPath: string[];
97
+ newPath: string[];
98
+ };
99
+
100
+ type CrCustomType =
101
+ | string
102
+ | { id: string; fields?: readonly CrCustomTypeNestedCr[] };
103
+ type CrCustomTypeNestedCr =
104
+ | string
105
+ | { id: string; customtypes: readonly CrCustomTypeFieldLeaf[] };
106
+ type CrCustomTypeFieldLeaf =
107
+ | string
108
+ | { id: string; fields?: readonly string[] };
109
+
85
110
  export class CustomTypesManager extends BaseManager {
86
111
  async readCustomTypeLibrary(): Promise<SliceMachineManagerReadCustomTypeLibraryReturnType> {
87
112
  assertPluginsInitialized(this.sliceMachinePluginRunner);
@@ -167,19 +192,159 @@ export class CustomTypesManager extends BaseManager {
167
192
  };
168
193
  }
169
194
 
195
+ /**
196
+ * Update the Content Relationship API IDs for all existing custom types and
197
+ * slices. The change is determined by properties inside the `updateMeta`
198
+ * property.
199
+ */
200
+ private async updateContentRelationships(
201
+ args: { model: CustomType } & CustomTypeFieldIdChangedMeta,
202
+ ): Promise<
203
+ OnlyHookErrors<CallHookReturnType<CustomTypeUpdateHook>> & {
204
+ rollback?: () => Promise<void>;
205
+ }
206
+ > {
207
+ assertPluginsInitialized(this.sliceMachinePluginRunner);
208
+
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
+ });
245
+
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();
249
+
250
+ for (const library of libraries) {
251
+ const slices = await this.slices.readAllSlicesForLibrary({
252
+ libraryID: library.libraryID,
253
+ });
254
+
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
+ });
272
+ },
273
+ previousPath,
274
+ newPath,
275
+ });
276
+ }
277
+
278
+ // Process all the Content Relationship updates at once.
279
+ const crUpdatesResult = await Promise.all(
280
+ crUpdates.map((update) => update.updatePromise),
281
+ );
282
+
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
+ };
290
+ }
291
+ }
292
+
293
+ return { errors: [] };
294
+ }
295
+
170
296
  async updateCustomType(
171
297
  args: CustomTypeUpdateHookData,
172
- ): Promise<OnlyHookErrors<CallHookReturnType<CustomTypeUpdateHook>>> {
298
+ ): Promise<CustomTypesMachineManagerUpdateCustomTypeReturnType> {
173
299
  assertPluginsInitialized(this.sliceMachinePluginRunner);
300
+ const { model } = args;
301
+ const { fieldIdChanged } = args.updateMeta ?? {};
174
302
 
175
- 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(
176
321
  "custom-type:update",
177
- args,
322
+ { model },
178
323
  );
179
324
 
180
- return {
181
- errors: hookResult.errors,
182
- };
325
+ if (customTypeUpdateResult.errors.length > 0) {
326
+ return { errors: customTypeUpdateResult.errors };
327
+ }
328
+
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: [] };
183
348
  }
184
349
 
185
350
  async renameCustomType(
@@ -393,3 +558,197 @@ const InferSliceResponse = z.object({
393
558
  }),
394
559
  langSmithUrl: z.string().url().optional(),
395
560
  });
561
+
562
+ function updateCRCustomType(
563
+ args: { customType: CrCustomType } & CustomTypeFieldIdChangedMeta,
564
+ ): CrCustomType {
565
+ const [previousCustomTypeId, previousFieldId] = args.previousPath;
566
+ const [newCustomTypeId, newFieldId] = args.newPath;
567
+
568
+ if (!previousCustomTypeId || !newCustomTypeId) {
569
+ throw new Error(
570
+ "Could not find a customtype id in previousPath and/or newPath, which should not be possible.",
571
+ );
572
+ }
573
+
574
+ if (!previousFieldId || !newFieldId) {
575
+ throw new Error(
576
+ "Could not find a field id in previousPath and/or newPath, which should not be possible.",
577
+ );
578
+ }
579
+
580
+ const customType = shallowCloneIfObject(args.customType);
581
+
582
+ if (typeof customType === "string" || !customType.fields) {
583
+ return customType;
584
+ }
585
+
586
+ const matchedCustomTypeId = customType.id === previousCustomTypeId;
587
+
588
+ const newFields = customType.fields.map((fieldArg) => {
589
+ const customTypeField = shallowCloneIfObject(fieldArg);
590
+
591
+ if (typeof customTypeField === "string") {
592
+ if (
593
+ matchedCustomTypeId &&
594
+ customTypeField === previousFieldId &&
595
+ customTypeField !== newFieldId
596
+ ) {
597
+ // We have reached a field id that matches the id that was renamed,
598
+ // so we update it new one. The field is a string, so return the new
599
+ // id.
600
+ return newFieldId;
601
+ }
602
+
603
+ return customTypeField;
604
+ }
605
+
606
+ if (
607
+ matchedCustomTypeId &&
608
+ customTypeField.id === previousFieldId &&
609
+ customTypeField.id !== newFieldId
610
+ ) {
611
+ // We have reached a field id that matches the id that was renamed,
612
+ // so we update it new one.
613
+ // Since field is not a string, we don't exit, as we might have
614
+ // something to update further down in customtypes.
615
+ customTypeField.id = newFieldId;
616
+ }
617
+
618
+ return {
619
+ ...customTypeField,
620
+ customtypes: customTypeField.customtypes.map((customTypeArg) => {
621
+ const nestedCustomType = shallowCloneIfObject(customTypeArg);
622
+
623
+ if (
624
+ typeof nestedCustomType === "string" ||
625
+ !nestedCustomType.fields ||
626
+ // Since we are on the last level, if we don't start matching right
627
+ // at the custom type id, we can return exit early because it's not
628
+ // a match.
629
+ nestedCustomType.id !== previousCustomTypeId
630
+ ) {
631
+ return nestedCustomType;
632
+ }
633
+
634
+ return {
635
+ ...nestedCustomType,
636
+ fields: nestedCustomType.fields.map((fieldArg) => {
637
+ const nestedCustomTypeField = shallowCloneIfObject(fieldArg);
638
+
639
+ if (
640
+ nestedCustomTypeField === previousFieldId &&
641
+ nestedCustomTypeField !== newFieldId
642
+ ) {
643
+ // Matches the previous id, so we update it and return because
644
+ // it's the last level.
645
+ return newFieldId;
646
+ }
647
+
648
+ return nestedCustomTypeField;
649
+ }),
650
+ };
651
+ }),
652
+ };
653
+ });
654
+
655
+ return { ...customType, fields: newFields };
656
+ }
657
+
658
+ /**
659
+ * Update the Content Relationship API IDs of a single field. The change is
660
+ * determined by the `previousPath` and `newPath` properties.
661
+ */
662
+ function updateFieldContentRelationships<
663
+ T extends UID | NestableWidget | Group | NestedGroup,
664
+ >(args: { field: T } & CustomTypeFieldIdChangedMeta): T {
665
+ const { field, ...updateMeta } = args;
666
+ if (
667
+ field.type !== "Link" ||
668
+ field.config?.select !== "document" ||
669
+ !field.config?.customtypes
670
+ ) {
671
+ // not a content relationship field
672
+ return field;
673
+ }
674
+
675
+ const newCustomTypes = field.config.customtypes.map((customType) => {
676
+ return updateCRCustomType({ customType, ...updateMeta });
677
+ });
678
+
679
+ return {
680
+ ...field,
681
+ config: { ...field.config, customtypes: newCustomTypes },
682
+ };
683
+ }
684
+
685
+ export function updateCustomTypeContentRelationships(
686
+ args: {
687
+ models: { model: CustomType }[];
688
+ onUpdate: (model: { previousModel: CustomType; model: CustomType }) => void;
689
+ } & CustomTypeFieldIdChangedMeta,
690
+ ): void {
691
+ const { models, previousPath, newPath, onUpdate } = args;
692
+
693
+ for (const { model: customType } of models) {
694
+ const updatedCustomType = traverseCustomType({
695
+ customType,
696
+ onField: ({ field }) => {
697
+ return updateFieldContentRelationships({
698
+ field,
699
+ previousPath,
700
+ newPath,
701
+ });
702
+ },
703
+ });
704
+
705
+ if (!isEqualModel(customType, updatedCustomType)) {
706
+ onUpdate({ model: updatedCustomType, previousModel: customType });
707
+ }
708
+ }
709
+ }
710
+
711
+ export function updateSharedSliceContentRelationships(
712
+ args: {
713
+ models: { model: SharedSlice }[];
714
+ onUpdate: (model: {
715
+ previousModel: SharedSlice;
716
+ model: SharedSlice;
717
+ }) => void;
718
+ } & CustomTypeFieldIdChangedMeta,
719
+ ): void {
720
+ const { models, previousPath, newPath, onUpdate } = args;
721
+
722
+ for (const { model: slice } of models) {
723
+ const updateSlice = traverseSharedSlice({
724
+ path: ["."],
725
+ slice,
726
+ onField: ({ field }) => {
727
+ return updateFieldContentRelationships({
728
+ field,
729
+ previousPath,
730
+ newPath,
731
+ });
732
+ },
733
+ });
734
+
735
+ if (!isEqualModel(slice, updateSlice)) {
736
+ onUpdate({ model: updateSlice, previousModel: slice });
737
+ }
738
+ }
739
+ }
740
+
741
+ function isEqualModel<T extends CustomType | SharedSlice>(
742
+ modelA: T,
743
+ modelB: T,
744
+ ): boolean {
745
+ return JSON.stringify(modelA) === JSON.stringify(modelB);
746
+ }
747
+
748
+ function shallowCloneIfObject<T>(value: T): T {
749
+ if (typeof value === "object") {
750
+ return { ...value };
751
+ }
752
+
753
+ return value;
754
+ }