@prismicio/types-internal
Version:
Prismic types for Custom Types and Prismic Data
334 lines (307 loc) • 8.75 kB
text/typescript
import { array, either } from "fp-ts"
import { isLeft } from "fp-ts/lib/Either"
import { pipe } from "fp-ts/lib/function"
import * as t from "io-ts"
import type {
ContentPath,
TraverseWidgetContentFn,
} from "../../_internal/utils"
import {
type Group,
type NestableWidget,
GroupFieldType,
} from "../../customtypes"
import {
FieldOrSliceType,
getFieldCtx,
LegacyContentCtx,
WithTypes,
} from "../LegacyContentCtx"
import { hasContentType } from "../utils"
import {
isNestableContent,
isRepeatableContent,
isTableContent,
NestableContent,
NestableLegacy,
traverseRepeatableContent,
traverseTableContent,
} from "./nestable"
import { repeatableContentWithDefaultNestableContentValues } from "./withDefaultValues"
export const GroupItemContentType = "GroupItemContent" as const
export const GroupContentType = "GroupContentType" as const
export const GroupItemContent: t.Type<GroupItemContent> = t.recursion(
"GroupItemContent",
() =>
t.strict({
__TYPE__: t.literal(GroupItemContentType),
key: t.string,
value: t.array(
t.tuple([t.string, t.union([NestableContent, GroupContent])]),
),
}),
)
export type GroupItemContent = {
__TYPE__: typeof GroupItemContentType
key: string
value: [string, NestableContent | GroupContent][]
}
export const GroupContent: t.Type<GroupContent> = t.recursion(
"GroupContent",
() =>
t.strict({
__TYPE__: t.literal(GroupContentType),
value: t.array(GroupItemContent),
}),
)
export type GroupContent = {
__TYPE__: typeof GroupContentType
value: GroupItemContent[]
}
export const isGroupContent = (u: unknown): u is GroupContent =>
hasContentType(u) && u.__TYPE__ === GroupContentType
export const GroupContentDefaultValue: GroupContent = {
__TYPE__: GroupContentType,
value: [],
}
const itemLegacyReader = t.record(t.string, t.unknown)
export type GroupItemLegacy = t.TypeOf<typeof itemLegacyReader>
export const GroupItemLegacy = (ctx: LegacyContentCtx, index: number) => {
return new t.Type<GroupItemContent, WithTypes<GroupItemLegacy>>(
"GroupItemLegacy",
(u): u is GroupItemContent =>
hasContentType(u) && u.__TYPE__ === GroupItemContentType,
(u) => {
const parsed = pipe(
itemLegacyReader.decode(u),
either.map((items) => {
const groupItemCtx = ctx.withContentKey(`${index}`)
const parsedItems = Object.entries(items).reduce<
Array<[string, NestableContent | GroupContent]>
>((acc, [itemKey, itemValue]) => {
const itemCtx = getFieldCtx({
fieldKey: itemKey,
contentKey: itemKey,
ctx: groupItemCtx,
})
const result =
itemCtx.fieldType === GroupFieldType
? GroupLegacy(itemCtx).decode(itemValue)
: NestableLegacy(itemCtx).decode(itemValue)
if (!result) return acc
if (isLeft(result)) return acc
return [...acc, [itemKey, result.right]]
}, [])
return {
value: parsedItems,
__TYPE__: GroupItemContentType,
key: groupItemCtx.fieldContentKey,
}
}),
)
return parsed
},
(item) => {
const groupItemCtx = ctx.withContentKey(`${index}`)
return item.value.reduce<WithTypes<GroupItemLegacy>>(
(acc, [key, value]) => {
const itemCtx = getFieldCtx({ fieldKey: key, ctx })
const encoded = isGroupContent(value)
? GroupLegacy(itemCtx).encode(value)
: NestableLegacy(itemCtx).encode(value)
if (!encoded) return acc
return {
content: { ...acc.content, [key]: encoded.content },
types: { ...acc.types, ...encoded.types },
keys: { ...acc.keys, ...encoded.keys },
}
},
{ content: {}, types: {}, keys: { [groupItemCtx.keyOfKey]: item.key } },
)
},
)
}
export function arrayWithIndexCodec<T, O>(
f: (index: number) => t.Type<T, O, unknown>,
): t.Type<Array<T>, O[], unknown> {
return new t.Type<Array<T>, O[], unknown>(
"ArrayWithIndexCodec",
(u): u is Array<T> => Array.isArray(u),
(items) =>
pipe(
t.array(t.unknown).decode(items),
either.chain((validItems) => {
const decodedItems = validItems.map((item, index) => {
return f(index).decode(item)
})
return array.sequence(either.Applicative)(decodedItems)
}),
),
(items) => items.map((item, index) => f(index).encode(item)),
)
}
type GroupLegacy = Array<GroupItemLegacy>
export const GroupLegacy = (
ctx: LegacyContentCtx,
): t.Type<GroupContent, WithTypes<GroupLegacy>, unknown> => {
const codecDecode = arrayWithIndexCodec<
GroupItemContent | null,
WithTypes<GroupItemLegacy> | null
>((index) => t.union([GroupItemLegacy(ctx, index), t.null]))
const codecEncode = (items: GroupItemContent[]) =>
items.map((item, index) => GroupItemLegacy(ctx, index).encode(item))
return new t.Type<GroupContent, WithTypes<GroupLegacy>, unknown>(
"GroupLegacy",
isGroupContent,
(items) => {
return pipe(
codecDecode.decode(items),
either.map((parsedItems) => {
const value = parsedItems.map((i, index) => {
if (i === null) {
const key = ctx.withContentKey(`${index}`).fieldContentKey
return { __TYPE__: GroupItemContentType, key, value: [] }
}
return i
})
return {
value,
__TYPE__: GroupContentType,
}
}),
)
},
(g: GroupContent) => {
const res = codecEncode(g.value)
return {
content: res.map((block) => block.content),
types: res.reduce<Record<string, FieldOrSliceType>>(
(acc, block) => {
return { ...acc, ...block.types }
},
{ [ctx.keyOfType]: GroupFieldType },
),
keys: res.reduce<Record<string, string>>((acc, block) => {
return { ...acc, ...block.keys }
}, {}),
}
},
)
}
export function groupContentWithDefaultValues(
customType: Group,
content: GroupContent,
): GroupContent {
const fields = customType.config?.fields
if (!fields) return content
return {
...content,
value: repeatableContentWithDefaultNestableContentValues(
fields,
content.value,
),
}
}
export function traverseGroupContent({
path,
key,
apiId,
model,
content,
}: {
path: ContentPath
key: string
apiId: string
content: GroupContent
model?: Group | undefined
}) {
return (transform: TraverseWidgetContentFn): GroupContent | undefined => {
const groupItems = traverseGroupItemsContent({
path,
model: model?.config?.fields,
content: content.value,
})(transform)
return transform({
path,
key,
apiId,
model,
content: {
__TYPE__: content.__TYPE__,
value: groupItems,
},
})
}
}
export function traverseGroupItemsContent({
path,
model,
content,
}: {
path: ContentPath
content: Array<GroupItemContent>
model?: Record<string, Group | NestableWidget> | undefined
}) {
return (transform: TraverseWidgetContentFn): Array<GroupItemContent> => {
return content.map((groupItem) => {
const groupItemPath = path.concat([
{ key: groupItem.key, type: "GroupItem" },
])
const groupItemFields = groupItem.value.reduce<GroupItemContent["value"]>(
(acc, [fieldKey, fieldContent]) => {
const fieldDef = model?.[fieldKey]
let transformedField
if (isGroupContent(fieldContent)) {
transformedField = traverseGroupContent({
path: groupItemPath.concat([{ key: fieldKey, type: "Widget" }]),
key: fieldKey,
apiId: fieldKey,
model: fieldDef?.type === "Group" ? fieldDef : undefined,
content: fieldContent,
})(transform)
} else if (isRepeatableContent(fieldContent)) {
transformedField = traverseRepeatableContent({
path: groupItemPath.concat([{ key: fieldKey, type: "Widget" }]),
key: fieldKey,
apiId: fieldKey,
model: fieldDef?.type === "Link" ? fieldDef : undefined,
content: fieldContent,
})(transform)
} else if (isTableContent(fieldContent)) {
transformedField = traverseTableContent({
path: groupItemPath.concat([{ key: fieldKey, type: "Widget" }]),
key: fieldKey,
apiId: fieldKey,
model: fieldDef?.type === "Table" ? fieldDef : undefined,
content: fieldContent,
})(transform)
} else {
transformedField = transform({
path: groupItemPath.concat([{ key: fieldKey, type: "Widget" }]),
key: fieldKey,
apiId: fieldKey,
model: fieldDef,
content: fieldContent,
})
}
// Can happen if the transform function returns undefined to filter out a field
if (
!transformedField ||
!(
isNestableContent(transformedField) ||
isGroupContent(transformedField)
)
)
return acc
return acc.concat([[fieldKey, transformedField]])
},
[],
)
return {
__TYPE__: groupItem.__TYPE__,
key: groupItem.key,
value: groupItemFields,
}
})
}
}