@prismicio/types-internal
Version:
Prismic types for Custom Types and Prismic Data
281 lines (248 loc) • 7.23 kB
text/typescript
import { Either, left, right } from "fp-ts/lib/Either"
import * as t from "io-ts"
import { withFallback } from "io-ts-types/lib/withFallback"
import type { OnFieldFn } from "../_internal/utils"
import { StringOrNull } from "../validators"
import {
DynamicSection,
Sections,
StaticSection,
traverseSection,
} from "./Section"
import type { Group, NestableWidget, NestedGroup, UID } from "./widgets"
import type { SharedSlice } from "./widgets/slices/SharedSlice"
import type { DynamicSlice } from "./widgets/slices/Slice"
import type { DynamicSlices } from "./widgets/slices/Slices"
import type { DynamicWidget } from "./widgets/Widget"
import type { StaticWidget } from "./widgets/Widget"
export const CustomTypeFormat = {
page: "page",
custom: "custom",
}
class CustomTypeSlicesError extends Error {
slices: Array<string>
override message: string
constructor(slices: Array<string>) {
super()
this.slices = slices
this.message = this._formatError(slices)
}
_formatError(slicesRefs: Array<string>) {
const slicesMsg = slicesRefs.map((ref) => `\t - ${ref}`)
return `The following slices doesn't exists in your Prismic repository:
${slicesMsg.join("\n")}
`
}
}
function customTypeReader<T extends StaticSection | DynamicSection>(
codec: t.Type<T, unknown>,
) {
return t.exact(
t.intersection([
t.type({
id: t.string,
label: StringOrNull,
repeatable: withFallback(t.boolean, true),
json: t.record(t.string, codec), //tab name => tab data
status: withFallback(t.boolean, true),
}),
t.partial({
format: withFallback(t.keyof(CustomTypeFormat), "custom"),
}),
]),
)
}
export const StaticCustomType = customTypeReader(StaticSection)
export type StaticCustomType = t.TypeOf<typeof StaticCustomType>
export const CustomType = customTypeReader(DynamicSection)
export type CustomType = t.TypeOf<typeof CustomType>
export function flattenWidgets(
customType: CustomType,
): Array<[string, DynamicWidget]> {
return Object.entries(customType.json).reduce(
(
acc: Array<[string, DynamicWidget]>,
[, section]: [string, DynamicSection],
) => {
const sectionWidgets: Array<[string, DynamicWidget]> =
Object.entries(section)
return acc.concat(sectionWidgets)
},
[],
)
}
export function flattenSections(
customType: StaticCustomType,
): Array<[string, StaticWidget]> {
return Object.entries(customType.json).reduce(
(
acc: Array<[string, StaticWidget]>,
[, section]: [string, StaticSection],
) => {
const sectionWidgets: Array<[string, StaticWidget]> =
Object.entries(section)
return acc.concat(sectionWidgets)
},
[],
)
}
function _retrieveSharedSlicesRef(customType: CustomType): Array<string> {
const slicezones = flattenWidgets(customType).filter(
([, widget]: [string, DynamicWidget]) => widget.type === "Slices",
) as Array<[string, DynamicSlices]>
const allSharedRefs = slicezones.reduce(
(acc: Array<string>, [, slicezone]) => {
const sharedRefs = Object.entries(
slicezone.config && slicezone.config.choices
? slicezone.config.choices
: {},
)
.filter(
([, slice]: [string, DynamicSlice]) => slice.type === "SharedSlice",
)
.map(([key]) => key)
return acc.concat(sharedRefs)
},
[],
)
return allSharedRefs
}
function _mapSharedSlicesRefs<A>(
customType: CustomType,
): (mapFn: (ref: string) => A) => Array<A> {
const refs = _retrieveSharedSlicesRef(customType)
return function (mapFn: (ref: string) => A): Array<A> {
return refs.map(mapFn)
}
}
export function toStatic(
customType: CustomType,
sharedSlices: Map<string, SharedSlice>,
): StaticCustomType {
const json = Object.entries(customType.json).reduce(
(
acc: { [key: string]: StaticSection },
[sectionKey, dynSection]: [string, DynamicSection],
) => {
return {
...acc,
[sectionKey]: Sections.toStatic(dynSection, sharedSlices),
}
},
{},
)
return { ...customType, json } as StaticCustomType
}
export function validateSlices(
customType: CustomType,
sharedSlices: Map<string, SharedSlice>,
): Either<CustomTypeSlicesError, CustomType> {
const missingSlices = _mapSharedSlicesRefs<string | undefined>(customType)(
(ref: string) => {
const slice: SharedSlice | undefined = sharedSlices.get(ref)
const isMissing = !slice
if (isMissing) return ref
return
},
).filter(Boolean) as Array<string>
if (missingSlices.length > 0)
return left(new CustomTypeSlicesError(missingSlices))
else return right(customType)
}
export function collectWidgets(
customType: CustomType,
f: (ref: string, widget: DynamicWidget) => DynamicWidget | undefined,
): CustomType {
const json = Object.entries(customType.json).reduce(
(acc, [sectionId, section]: [string, DynamicSection]) => {
const updatedSection = Object.entries(section).reduce(
(acc, [ref, widget]) => {
const updatedWidget = f(ref, widget)
if (updatedWidget) {
return { ...acc, [ref]: updatedWidget }
}
return acc
},
{},
)
return { ...acc, [sectionId]: updatedSection }
},
{},
)
return { ...customType, json }
}
export function filterMissingSharedSlices(
customType: CustomType,
sharedSlices: Map<string, SharedSlice>,
): CustomType {
return collectWidgets(customType, (_widgetId, widget) => {
if (widget.type === "Slices") {
if (!widget.config || !widget.config.choices) return widget
const choices = Object.entries(widget.config.choices).reduce(
(acc, [sliceId, sliceValue]: [string, DynamicSlice]) => {
if (sliceValue.type === "SharedSlice" && !sharedSlices.get(sliceId))
return acc
return { ...acc, [sliceId]: sliceValue }
},
{},
)
const config = { ...widget.config, choices }
return { ...widget, config }
}
return widget
})
}
export function flattenCustomTypeFields(
customType: StaticCustomType,
): Record<string, StaticWidget> {
return Object.values(customType.json).reduce(
(acc, tab) => ({
...acc,
...tab,
}),
{},
)
}
export function collectSharedSlices(customType: {
customTypeId: string
fields: Record<string, StaticWidget>
}): Record<string, SharedSlice> {
return Object.entries(customType.fields).reduce((acc, [, w]) => {
if (w.type === "Slices" || w.type === "Choice") {
return {
...acc,
...Object.entries(w.config?.choices || {}).reduce(
(sliceZoneAcc, [sliceKey, sliceModel]) => {
return sliceModel.type === "SharedSlice"
? { ...sliceZoneAcc, [sliceKey]: sliceModel }
: sliceZoneAcc
},
{},
),
}
}
return acc
}, {})
}
export function traverseCustomType<
T extends CustomType | StaticCustomType,
>(args: {
customType: T
onField: OnFieldFn<UID | NestableWidget | Group | NestedGroup>
}): T {
const { customType, onField } = args
let json: Record<string, typeof customType.json[string]> | undefined
for (const [key, section] of Object.entries(customType.json)) {
const newSection = traverseSection({
path: [key],
section,
onField,
})
if (newSection !== section) {
if (!json) json = { ...customType.json }
json[key] = newSection
}
}
// returns the traversed model instead of a new one if it didn't change
return json ? { ...customType, json } : customType
}