UNPKG

@prismicio/types-internal

Version:
410 lines (381 loc) 10.4 kB
import { either } from "fp-ts" import { isLeft, isRight } from "fp-ts/lib/Either" import { pipe } from "fp-ts/lib/function" import * as t from "io-ts" import { WidgetKey } from "../common" import { UUID } from "../common/UUID" import { type StaticWidget, collectSharedSlices, flattenSections, NestableWidget, StaticCustomType, } from "../customtypes" import { groupContentWithDefaultValues, isGroupContent, isSlicesContent, migrateSliceItem, NestableContentDefaultValue, slicesContentWithDefaultValues, traverseGroupContent, traverseRepeatableContent, traverseSlices, traverseTableContent, WidgetContent, WidgetLegacy, } from "./fields" import { defaultCtx, FieldOrSliceType, LegacyContentCtx, WithTypes, } from "./LegacyContentCtx" import { ContentPath, TraverseSliceContentFn, TraverseWidgetContentFn, } from "./utils" export const Document = t.record(WidgetKey, WidgetContent) export type Document = t.TypeOf<typeof Document> const legacyDocReader = t.record(WidgetKey, t.unknown) type DocumentLegacy = t.TypeOf<typeof legacyDocReader> /** * `DocumentLegacyCodec` handles decoding and encoding documents to the legacy * format used by Prismic DB, therefore, this function itself is not "legacy". */ const DocumentLegacyCodec = ( allTypes?: LegacyContentCtx["allTypes"], allKeys?: LegacyContentCtx["allKeys"], ) => { return new t.Type<Document, WithTypes<DocumentLegacy>, unknown>( "Document", (u): u is Document => !!u && typeof u === "object", (doc) => { return pipe( legacyDocReader.decode(doc), either.map((parsedDoc) => { return Object.entries(parsedDoc).reduce( (acc, [widgetKey, widgetValue]) => { const widgetCtx = defaultCtx(widgetKey, allTypes, allKeys) const parsedW = WidgetLegacy(widgetCtx).decode(widgetValue) if (!parsedW || isLeft(parsedW)) return acc return { ...acc, [widgetKey]: parsedW.right } }, {}, ) }), ) }, (g: Document) => { return Object.entries(g).reduce( (acc, [key, value]) => { const widgetCtx = defaultCtx(key, allTypes) const result = WidgetLegacy(widgetCtx).encode(value) if (!result) return acc return { content: { ...acc.content, [key]: result.content }, types: { ...acc.types, ...result.types }, keys: { ...acc.keys, ...result.keys }, } }, { content: {}, types: {}, keys: {} }, ) }, ) } function extractMetadata(data: { [p: string]: unknown }): { types: Map<string, FieldOrSliceType> widgets: Partial<Record<WidgetKey, unknown>> keys: Map<string, string> slugs: ReadonlyArray<string> uid: string | undefined } { const fields: [string, unknown][] = Object.entries(data) const { types, widgets, keys } = fields.reduce( (acc, [k, v]) => { if (k.endsWith("_TYPE")) { const decodedValue = FieldOrSliceType.decode(v) if (isRight(decodedValue)) { return { ...acc, types: acc.types.set( k.substring(0, k.length - 5), decodedValue.right, ), } } } if (k.endsWith("_KEY")) { const decodedValue = UUID.decode(v) if (isRight(decodedValue)) { return { ...acc, keys: acc.keys.set( k.substring(0, k.length - 4), decodedValue.right, ), } } } if ( !k.endsWith("_POSITION") && !k.endsWith("_TYPE") && !k.endsWith("_KEY") ) { return { ...acc, widgets: { ...acc.widgets, [k]: v, }, } } return acc }, { types: new Map<string, FieldOrSliceType>(), widgets: {}, keys: new Map<string, string>(), }, ) const slugs = (data["slugs_INTERNAL"] as string[]) || [] const uid = data["uid"] as string | undefined return { widgets, types, keys, uid, slugs, } } function parseLegacyDocument( legacyDoc: unknown, customType: | StaticCustomType | { customTypeId: string fields: Record<string, StaticWidget> }, ): Document | undefined { const result = pipe( // ensure it's the right document format first t.record(WidgetKey, t.unknown).decode(legacyDoc), either.chain((doc) => { // extract all metadata, meaning all _TYPES keys from legacy format + the widgets as unknown const { types, widgets, keys } = extractMetadata(doc) // parse the actual widgets return DocumentLegacyCodec(types, keys).decode(widgets) }), ) return isLeft(result) ? undefined : migrateDocument(result.right, customType) } function encodeToLegacyDocument(document: Document): DocumentLegacy { const encoded = DocumentLegacyCodec().encode(document) return { ...encoded.content, ...encoded.types, ...encoded.keys } } export const DocumentLegacy = { _codec: DocumentLegacyCodec, extractMetadata, parse: parseLegacyDocument, encode: encodeToLegacyDocument, } function simplifyCustomType(customType: StaticCustomType): { customTypeId: string fields: Record<string, StaticWidget> } { return { customTypeId: customType?.id, fields: Object.fromEntries(flattenSections(customType)), } } export function fillDocumentWithDefaultValues( customType: | StaticCustomType | { customTypeId: string fields: Record<string, StaticWidget> }, document: Document, ): Document { const { fields } = customType && StaticCustomType.is(customType) ? simplifyCustomType(customType) : customType return Object.entries(fields).reduce<Document>( (updatedDocument, [fieldKey, fieldDef]) => { const fieldContent = document[fieldKey] const updatedField = (() => { switch (fieldDef.type) { case "Group": return isGroupContent(fieldContent) ? groupContentWithDefaultValues(fieldDef, fieldContent) : fieldContent case "Choice": case "Slices": return isSlicesContent(fieldContent) ? slicesContentWithDefaultValues(fieldDef, fieldContent) : fieldContent default: return fieldContent === undefined && NestableWidget.is(fieldDef) ? NestableContentDefaultValue(fieldDef) : fieldContent } })() return updatedField ? { ...updatedDocument, [fieldKey]: updatedField, } : updatedDocument }, document, ) } /** * @param model: Can be optional if we simply want to loop through the content * without any consideration for the attached model * @param document: The content we actually want to iterate on in an immutable fashion * @param transform: A user function that provides a way to transform any kind * of content wherever it is in a structured Prismic object content. * @returns A transformed document with the user's transformation applied with * the transform function */ export function traverseDocument({ document, customType, }: { document: Document customType?: | StaticCustomType | { customTypeId: string fields: Record<string, StaticWidget> } | undefined }) { const model = customType && StaticCustomType.is(customType) ? simplifyCustomType(customType) : customType return ({ transformWidget = ({ content }) => content, transformSlice = ({ content }) => content, }: { transformWidget?: TraverseWidgetContentFn transformSlice?: TraverseSliceContentFn }): Document => { const fieldModels = model && Object.entries(model.fields).reduce<Record<string, StaticWidget>>( (acc, [key, def]) => ({ ...acc, [key]: def }), {}, ) return Object.entries(document).reduce((acc, [key, content]) => { const fieldModel = fieldModels && fieldModels[key] const path = ContentPath.make([ { key: model?.customTypeId, type: "CustomType" }, { key, type: "Widget" }, ]) const transformedWidget = (() => { switch (content.__TYPE__) { case "SliceContentType": return traverseSlices({ path, key, model: fieldModel?.type === "Slices" || fieldModel?.type === "Choice" ? fieldModel : undefined, content, })({ transformWidget, transformSlice }) case "GroupContentType": return traverseGroupContent({ path, key, apiId: key, model: fieldModel?.type === "Group" ? fieldModel : undefined, content, })(transformWidget) case "RepeatableContent": return traverseRepeatableContent({ path, key, apiId: key, model: fieldModel?.type === "Link" ? fieldModel : undefined, content, })(transformWidget) case "TableContent": return traverseTableContent({ path, key, apiId: key, model: fieldModel?.type === "Table" ? fieldModel : undefined, content, })(transformWidget) default: return transformWidget({ path, key, apiId: key, model: fieldModel?.type !== "Group" && fieldModel?.type !== "Slices" && fieldModel?.type !== "Choice" ? fieldModel : undefined, content, }) } })() return { ...acc, ...(transformedWidget ? { [key]: transformedWidget } : {}), } }, {}) } } // /** // * The goal is to be able to collect all widgets or slices of a given type at any level of nesting inside a prismic content // * // * @param document parsed prismic content // * @param is typeguard to match specifically the type of widget we want to collect // * @returns a record containing the path of the widget as key and the typed collected content as value // */ export function collectWidgets<W extends WidgetContent>( document: Document, is: (content: WidgetContent, path: ContentPath) => content is W, ): Record<string, W> { const collected: Record<string, W> = {} traverseDocument({ document })({ transformWidget: ({ content, path }) => { const key = ContentPath.serialize(path) if (is(content, path)) collected[key] = content return content }, }) return collected } export function migrateDocument( document: Document, customType: | StaticCustomType | { customTypeId: string fields: Record<string, StaticWidget> }, ) { const model = StaticCustomType.is(customType) ? simplifyCustomType(customType) : customType const needsMigration = Object.values(collectSharedSlices(model)).some( (slice) => Boolean(slice.legacyPaths), ) if (!needsMigration) return document return traverseDocument({ document, customType, })({ transformSlice: migrateSliceItem, }) }