import { getOptionalLinkProperties } from "./lib/getOptionalLinkProperties" import * as is from "./lib/isValue" import { validateAssetMetadata } from "./lib/validateAssetMetadata" import type { Asset } from "./types/api/asset/asset" import type { MigrationAssetConfig, MigrationImage, MigrationLinkToMedia, MigrationRTImageNode, } from "./types/migration/Asset" import { PrismicMigrationAsset } from "./types/migration/Asset" import type { MigrationContentRelationship } from "./types/migration/ContentRelationship" import { PrismicMigrationDocument } from "./types/migration/Document" import type { ExistingPrismicDocument, PendingPrismicDocument } from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" import type { FilledImageFieldImage } from "./types/value/image" import { type FilledLinkToWebField, LinkType } from "./types/value/link" import type { FilledLinkToMediaField } from "./types/value/linkToMedia" import { RichTextNodeType } from "./types/value/richText" /** * Extracts one or more Prismic document types that match a given Prismic document type. If no * matches are found, no extraction is performed and the union of all provided Prismic document * types are returned. * * @typeParam TDocuments - Prismic document types from which to extract. * @typeParam TDocumentType - Type(s) to match `TDocuments` against. */ type ExtractDocumentType< TDocuments extends { type: string }, TDocumentType extends TDocuments["type"], > = Extract extends never ? TDocuments : Extract /** * A helper that allows preparing your migration to Prismic. * * @typeParam TDocuments - Document types that are registered for the Prismic repository. Query * methods will automatically be typed based on this type. */ export class Migration { /** * Assets registered in the migration. * * @internal */ _assets: Map = new Map() /** * Documents registered in the migration. * * @internal */ _documents: PrismicMigrationDocument[] = [] /** * Registers an asset to be created in the migration from an asset object. * * @remarks * This method does not create the asset in Prismic media library right away. Instead, it * registers it in your migration. The asset will be created when the migration is executed * through the `writeClient.migrate()` method. * @param asset - An asset object from Prismic Asset API. * @returns A migration asset field instance. * @internal */ createAsset(asset: Asset): PrismicMigrationAsset /** * Registers an asset to be created in the migration from an image or link to media field. * * @remarks * This method does not create the asset in Prismic media library right away. Instead, it * registers it in your migration. The asset will be created when the migration is executed * through the `writeClient.migrate()` method. * @param imageOrLinkToMediaField - An image or link to media field from Prismic Document API. * @returns A migration asset field instance. * @internal */ createAsset( imageOrLinkToMediaField: FilledImageFieldImage | FilledLinkToMediaField, ): PrismicMigrationAsset /** * Registers an asset to be created in the migration from a file. * * @remarks * This method does not create the asset in Prismic media library right away. Instead, it * registers it in your migration. The asset will be created when the migration is executed * through the `writeClient.migrate()` method. * @param file - The URL or content of the file to be created. * @param filename - The filename of the asset. * @param params - Additional asset data. * @returns A migration asset field instance. */ createAsset( file: MigrationAssetConfig["file"], filename: MigrationAssetConfig["filename"], params?: { notes?: string credits?: string alt?: string tags?: string[] }, ): PrismicMigrationAsset /** * Registers an asset to be created in the migration from a file, an asset object, or an image or * link to media field. * * @remarks * This method does not create the asset in Prismic media library right away. Instead, it * registers it in your migration. The asset will be created when the migration is executed * through the `writeClient.migrate()` method. * @returns A migration asset field instance. */ createAsset( fileOrAssetOrField: | MigrationAssetConfig["file"] | Asset | FilledImageFieldImage | FilledLinkToMediaField, filename?: MigrationAssetConfig["filename"], { notes, credits, alt, tags, }: { notes?: string credits?: string alt?: string tags?: string[] } = {}, ): PrismicMigrationAsset { let config: MigrationAssetConfig let maybeInitialField: FilledImageFieldImage | undefined if (typeof fileOrAssetOrField === "object" && "url" in fileOrAssetOrField) { if ("dimensions" in fileOrAssetOrField || "link_type" in fileOrAssetOrField) { const url = fileOrAssetOrField.url.split("?")[0] const filename = "name" in fileOrAssetOrField ? fileOrAssetOrField.name : url.split("/").pop()!.split("_").pop()! const credits = "copyright" in fileOrAssetOrField && fileOrAssetOrField.copyright ? fileOrAssetOrField.copyright : undefined const alt = "alt" in fileOrAssetOrField && fileOrAssetOrField.alt ? fileOrAssetOrField.alt : undefined if ("dimensions" in fileOrAssetOrField) { maybeInitialField = fileOrAssetOrField } config = { id: fileOrAssetOrField.id, file: url, filename, notes: undefined, credits, alt, tags: undefined, } } else { config = { id: fileOrAssetOrField.id, file: fileOrAssetOrField.url, filename: fileOrAssetOrField.filename, notes: fileOrAssetOrField.notes, credits: fileOrAssetOrField.credits, alt: fileOrAssetOrField.alt, tags: fileOrAssetOrField.tags?.map(({ name }) => name), } } } else { config = { id: fileOrAssetOrField, file: fileOrAssetOrField, filename: filename!, notes, credits, alt, tags, } } validateAssetMetadata(config) // We create a detached instance of the asset each time to serialize it properly const migrationAsset = new PrismicMigrationAsset(config, maybeInitialField) const maybeAsset = this._assets.get(config.id) if (maybeAsset) { // Consolidate existing asset with new asset value if possible maybeAsset.config.notes = maybeAsset.config.notes || config.notes maybeAsset.config.credits = maybeAsset.config.credits || config.credits maybeAsset.config.alt = maybeAsset.config.alt || config.alt maybeAsset.config.tags = Array.from( new Set([...(maybeAsset.config.tags || []), ...(config.tags || [])]), ) } else { this._assets.set(config.id, migrationAsset) } return migrationAsset } /** * Registers a document to be created in the migration. * * @remarks * This method does not create the document in Prismic right away. Instead, it registers it in * your migration. The document will be created when the migration is executed through the * `writeClient.migrate()` method. * @typeParam TType - Type of the Prismic document to create. * @param document - The document to create. * @param title - The title of the document to create which will be displayed in the editor. * @param params - Document master language document ID. * @returns A migration document instance. */ createDocument( document: ExtractDocumentType, TType>, title: string, params?: { masterLanguageDocument?: MigrationContentRelationship }, ): PrismicMigrationDocument> { const doc = new PrismicMigrationDocument>( document, title, params, ) this._documents.push(doc) return doc } /** * Registers an existing document to be updated in the migration. * * @remarks * This method does not update the document in Prismic right away. Instead, it registers it in * your migration. The document will be updated when the migration is executed through the * `writeClient.migrate()` method. * @typeParam TType - Type of Prismic documents to update. * @param document - The document to update. * @param title - The title of the document to update which will be displayed in the editor. * @returns A migration document instance. */ updateDocument( document: ExtractDocumentType, TType>, // Title is optional for existing documents as we might not want to update it. title?: string, ): PrismicMigrationDocument> { const doc = new PrismicMigrationDocument>( document, title, ) this._documents.push(doc) return doc } /** * Registers a document from another Prismic repository to be created in the migration. * * @remarks * This method does not create the document in Prismic right away. Instead, it registers it in * your migration. The document will be created when the migration is executed through the * `writeClient.migrate()` method. * @param document - The document from Prismic to create. * @param title - The title of the document to create which will be displayed in the editor. * @returns A migration document instance. */ createDocumentFromPrismic( document: ExtractDocumentType, TType>, title: string, ): PrismicMigrationDocument> { const doc = new PrismicMigrationDocument( this.#migratePrismicDocumentData({ type: document.type, lang: document.lang, uid: document.uid, tags: document.tags, data: document.data, }) as PendingPrismicDocument>, title, { originalPrismicDocument: document }, ) this._documents.push(doc) return doc } /** * Queries a document from the migration instance with a specific UID and custom type. * * @example * ;```ts * const contentRelationship = migration.createContentRelationship(() => * migration.getByUID("blog_post", "my-first-post"), * ) * ``` * * @typeParam TType - Type of the Prismic document returned. * @param type - The API ID of the document's custom type. * @param uid - The UID of the document. * @returns The migration document instance with a UID matching the `uid` parameter, if a matching * document is found. */ getByUID( type: TType, uid: string, ): PrismicMigrationDocument> | undefined { return this._documents.find( (doc): doc is PrismicMigrationDocument> => doc.document.type === type && doc.document.uid === uid, ) } /** * Queries a singleton document from the migration instance for a specific custom type. * * @example * ;```ts * const contentRelationship = migration.createContentRelationship(() => * migration.getSingle("settings"), * ) * ``` * * @typeParam TType - Type of the Prismic document returned. * @param type - The API ID of the singleton custom type. * @returns The migration document instance for the custom type, if a matching * document is found. */ getSingle( type: TType, ): PrismicMigrationDocument> | undefined { return this._documents.find( (doc): doc is PrismicMigrationDocument> => doc.document.type === type, ) } /** * Migrates a Prismic document data from another repository so that it can be created through the * current repository's Migration API. * * @param input - The Prismic document data to migrate. * @returns The migrated Prismic document data. */ #migratePrismicDocumentData(input: unknown): unknown { if (is.filledContentRelationship(input)) { const optionalLinkProperties = getOptionalLinkProperties(input) if (input.isBroken) { return { ...optionalLinkProperties, link_type: LinkType.Document, // ID needs to be 16 characters long to be considered valid by the API id: "_____broken_____", isBroken: true, } } return { ...optionalLinkProperties, link_type: LinkType.Document, id: () => this._getByOriginalID(input.id), } } if (is.filledLinkToMedia(input)) { const optionalLinkProperties = getOptionalLinkProperties(input) return { ...optionalLinkProperties, link_type: LinkType.Media, id: this.createAsset(input), } } if (is.rtImageNode(input)) { // Rich text image nodes const rtImageNode: MigrationRTImageNode = { type: RichTextNodeType.image, id: this.createAsset(input), } if (input.linkTo) { rtImageNode.linkTo = this.#migratePrismicDocumentData(input.linkTo) as | MigrationContentRelationship | MigrationLinkToMedia | FilledLinkToWebField } return rtImageNode } if (is.filledImage(input)) { const image: MigrationImage = { id: this.createAsset(input), } const { id: _id, url: _url, dimensions: _dimensions, edit: _edit, alt: _alt, copyright: _copyright, ...thumbnails } = input for (const name in thumbnails) { if (is.filledImage(thumbnails[name])) { image[name] = this.createAsset(thumbnails[name]) } } return image } if (Array.isArray(input)) { return input.map((element) => this.#migratePrismicDocumentData(element)) } if (input && typeof input === "object") { const res: Record = {} for (const key in input) { res[key] = this.#migratePrismicDocumentData(input[key as keyof typeof input]) } return res } return input } /** * Queries a document from the migration instance for a specific original ID. * * @example * ;```ts * const contentRelationship = migration.createContentRelationship(() => * migration._getByOriginalID("YhdrDxIAACgAcp_b"), * ) * ``` * * @typeParam TType - Type of the Prismic document returned. * @param id - The original ID of the Prismic document. * @returns The migration document instance for the original ID, if a matching * document is found. * @internal */ _getByOriginalID( id: string, ): PrismicMigrationDocument> | undefined { return this._documents.find( (doc): doc is PrismicMigrationDocument> => doc.originalPrismicDocument?.id === id, ) } }