From 90e0c6bcf2ce2141690239ee119e8310438eb2a3 Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 23 Aug 2024 15:58:57 +0200 Subject: [PATCH 01/61] refactor: extract `BaseClient` from `Client` --- messages/prefer-repository-name.md | 3 + src/BaseClient.ts | 320 +++++ src/Client.ts | 1793 ++++++++++++++++++++++++++ src/createClient.ts | 1931 +--------------------------- src/createWriteClient.ts | 41 + src/index.ts | 20 +- src/lib/pLimit.ts | 112 ++ 7 files changed, 2287 insertions(+), 1933 deletions(-) create mode 100644 messages/prefer-repository-name.md create mode 100644 src/BaseClient.ts create mode 100644 src/Client.ts create mode 100644 src/createWriteClient.ts create mode 100644 src/lib/pLimit.ts diff --git a/messages/prefer-repository-name.md b/messages/prefer-repository-name.md new file mode 100644 index 00000000..0b2b52fa --- /dev/null +++ b/messages/prefer-repository-name.md @@ -0,0 +1,3 @@ +# Prefer repository name + +TODO diff --git a/src/BaseClient.ts b/src/BaseClient.ts new file mode 100644 index 00000000..7bde7bcf --- /dev/null +++ b/src/BaseClient.ts @@ -0,0 +1,320 @@ +import { type LimitFunction, pLimit } from "./lib/pLimit" + +import { PrismicError } from "./errors/PrismicError" + +/** + * A universal API to make network requests. A subset of the `fetch()` API. + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch} + */ +export type FetchLike = ( + input: string, + init?: RequestInitLike, +) => Promise + +/** + * An object that allows you to abort a `fetch()` request if needed via an + * `AbortController` object + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal} + */ +// `any` is used often here to ensure this type is universally valid among +// different AbortSignal implementations. The types of each property are not +// important to validate since it is blindly passed to a given `fetch()` +// function. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AbortSignalLike = any + +/** + * A subset of RequestInit properties to configure a `fetch()` request. + */ +// Only options relevant to the client are included. Extending from the full +// RequestInit would cause issues, such as accepting Header objects. +// +// An interface is used to allow other libraries to augment the type with +// environment-specific types. +export interface RequestInitLike extends Pick { + /** + * The HTTP method to use for the request. + */ + method?: string + + /** + * The request body to send with the request. + */ + // We want to keep the body type as compatible as possible, so + // we only declare the type we need and accept anything else. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body?: any | FormData | string + + /** + * An object literal to set the `fetch()` request's headers. + */ + headers?: Record + + /** + * An AbortSignal to set the `fetch()` request's signal. + * + * See: + * [https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) + */ + // NOTE: `AbortSignalLike` is `any`! It is left as `AbortSignalLike` + // for backwards compatibility (the type is exported) and to signal to + // other readers that this should be an AbortSignal-like object. + signal?: AbortSignalLike +} + +/** + * The minimum required properties from Response. + */ +export interface ResponseLike { + status: number + headers: HeadersLike + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(): Promise +} + +/** + * The minimum required properties from Headers. + */ +export interface HeadersLike { + get(name: string): string | null +} + +/** + * The minimum required properties to treat as an HTTP Request for automatic + * Prismic preview support. + */ +export type HttpRequestLike = + | /** + * Web API Request + * + * @see http://developer.mozilla.org/en-US/docs/Web/API/Request + */ + { + headers?: { + get(name: string): string | null + } + url?: string + } + + /** + * Express-style Request + */ + | { + headers?: { + cookie?: string + } + query?: Record + } + +/** + * Configuration for clients that determine how APIs are queried. + */ +export type BaseClientConfig = { + /** + * The function used to make network requests to the Prismic REST API. In + * environments where a global `fetch` function does not exist, such as + * Node.js, this function must be provided. + */ + fetch?: FetchLike + + /** + * Options provided to the client's `fetch()` on all network requests. These + * options will be merged with internally required options. They can also be + * overriden on a per-query basis using the query's `fetchOptions` parameter. + */ + fetchOptions?: RequestInitLike +} + +/** + * Parameters for any client method that use `fetch()`. + */ +export type FetchParams = { + /** + * Options provided to the client's `fetch()` on all network requests. These + * options will be merged with internally required options. They can also be + * overriden on a per-query basis using the query's `fetchOptions` parameter. + */ + fetchOptions?: RequestInitLike + + /** + * An `AbortSignal` provided by an `AbortController`. This allows the network + * request to be cancelled if necessary. + * + * @deprecated Move the `signal` parameter into `fetchOptions.signal`: + * + * @see \ + */ + signal?: AbortSignalLike +} + +/** + * The result of a `fetch()` job. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FetchJobResult = { + status: number + headers: HeadersLike + json: TJSON +} + +export class BaseClient { + /** + * The function used to make network requests to the Prismic REST API. In + * environments where a global `fetch` function does not exist, such as + * Node.js, this function must be provided. + */ + fetchFn: FetchLike + + fetchOptions?: RequestInitLike + + /** + * Active queued `fetch()` jobs keyed by URL and AbortSignal (if it exists). + */ + private queuedFetchJobs: Record = {} + + /** + * Active deduped `fetch()` jobs keyed by URL and AbortSignal (if it exists). + */ + private dedupedFetchJobs: Record< + string, + Map> + > = {} + + constructor(options: BaseClientConfig) { + this.fetchOptions = options.fetchOptions + + if (typeof options.fetch === "function") { + this.fetchFn = options.fetch + } else if (typeof globalThis.fetch === "function") { + this.fetchFn = globalThis.fetch as FetchLike + } else { + throw new PrismicError( + "A valid fetch implementation was not provided. In environments where fetch is not available (including Node.js), a fetch implementation must be provided via a polyfill or the `fetch` option.", + undefined, + undefined, + ) + } + + // If the global fetch function is used, we must bind it to the global scope. + if (this.fetchFn === globalThis.fetch) { + this.fetchFn = this.fetchFn.bind(globalThis) + } + } + + protected async fetch( + url: string, + params: FetchParams = {}, + ): Promise { + const requestInit: RequestInitLike = { + ...this.fetchOptions, + ...params.fetchOptions, + headers: { + ...this.fetchOptions?.headers, + ...params.fetchOptions?.headers, + }, + signal: + params.fetchOptions?.signal || + params.signal || + this.fetchOptions?.signal, + } + + // Request with a `body` are throttled, others are deduped. + if (params.fetchOptions?.body) { + return this.queueFetch(url, requestInit) + } else { + return this.dedupeFetch(url, requestInit) + } + } + + private queueFetch( + url: string, + requestInit: RequestInitLike = {}, + ): Promise { + // Rate limiting is done per hostname. + const hostname = new URL(url).hostname + + if (!this.queuedFetchJobs[hostname]) { + this.queuedFetchJobs[hostname] = pLimit({ + limit: 1, + interval: 1500, + }) + } + + const job = this.queuedFetchJobs[hostname](() => + this.createFetchJob(url, requestInit), + ) + + job.finally(() => { + if ( + this.queuedFetchJobs[hostname] && + this.queuedFetchJobs[hostname].queueSize === 0 + ) { + delete this.queuedFetchJobs[hostname] + } + }) + + return job + } + + private dedupeFetch( + url: string, + requestInit: RequestInitLike = {}, + ): Promise { + let job: Promise + + // `fetchJobs` is keyed twice: first by the URL and again by is + // signal, if one exists. + // + // Using two keys allows us to reuse fetch requests for + // equivalent URLs, but eject when we detect unique signals. + if ( + this.dedupedFetchJobs[url] && + this.dedupedFetchJobs[url].has(requestInit.signal) + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + job = this.dedupedFetchJobs[url].get(requestInit.signal)! + } else { + this.dedupedFetchJobs[url] = this.dedupedFetchJobs[url] || new Map() + + job = this.createFetchJob(url, requestInit).finally(() => { + this.dedupedFetchJobs[url]?.delete(requestInit.signal) + + if (this.dedupedFetchJobs[url]?.size === 0) { + delete this.dedupedFetchJobs[url] + } + }) + + this.dedupedFetchJobs[url].set(requestInit.signal, job) + } + + return job + } + + private createFetchJob( + url: string, + requestInit: RequestInitLike = {}, + ): Promise { + return this.fetchFn(url, requestInit).then(async (res) => { + // We can assume Prismic REST API responses + // will have a `application/json` + // Content Type. If not, this will + // throw, signaling an invalid + // response. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let json: any = undefined + try { + json = await res.json() + } catch { + // noop + } + + return { + status: res.status, + headers: res.headers, + json, + } + }) + } +} diff --git a/src/Client.ts b/src/Client.ts new file mode 100644 index 00000000..a9692011 --- /dev/null +++ b/src/Client.ts @@ -0,0 +1,1793 @@ +import { appendFilters } from "./lib/appendFilters" +import { castThunk } from "./lib/castThunk" +import { devMsg } from "./lib/devMsg" +import { everyTagFilter } from "./lib/everyTagFilter" +import { findMasterRef } from "./lib/findMasterRef" +import { findRefByID } from "./lib/findRefByID" +import { findRefByLabel } from "./lib/findRefByLabel" +import { getPreviewCookie } from "./lib/getPreviewCookie" +import { minifyGraphQLQuery } from "./lib/minifyGraphQLQuery" +import { someTagsFilter } from "./lib/someTagsFilter" +import { typeFilter } from "./lib/typeFilter" + +import type { Query } from "./types/api/query" +import type { Ref } from "./types/api/ref" +import type { Form, Repository } from "./types/api/repository" +import type { PrismicDocument } from "./types/value/document" + +import { ForbiddenError } from "./errors/ForbiddenError" +import { NotFoundError } from "./errors/NotFoundError" +import { ParsingError } from "./errors/ParsingError" +import { PreviewTokenExpiredError } from "./errors/PreviewTokenExpired" +import { PrismicError } from "./errors/PrismicError" +import { RefExpiredError } from "./errors/RefExpiredError" +import { RefNotFoundError } from "./errors/RefNotFoundError" +import { RepositoryNotFoundError } from "./errors/RepositoryNotFoundError" + +import type { LinkResolverFunction } from "./helpers/asLink" +import { asLink } from "./helpers/asLink" + +import { + type AbortSignalLike, + BaseClient, + type BaseClientConfig, + type FetchParams, + type HttpRequestLike, +} from "./BaseClient" +import type { BuildQueryURLArgs } from "./buildQueryURL" +import { buildQueryURL } from "./buildQueryURL" +import { filter } from "./filter" +import { getRepositoryEndpoint } from "./getRepositoryEndpoint" +import { getRepositoryName } from "./getRepositoryName" +import { isRepositoryEndpoint } from "./isRepositoryEndpoint" + +/** + * The largest page size allowed by the Prismic REST API V2. This value is used + * to minimize the number of requests required to query content. + */ +const MAX_PAGE_SIZE = 100 + +/** + * The number of milliseconds in which repository metadata is considered valid. + * A ref can be invalidated quickly depending on how frequently content is + * updated in the Prismic repository. As such, repository's metadata can only be + * considered valid for a short amount of time. + */ +export const REPOSITORY_CACHE_TTL = 5000 + +/** + * The number of milliseconds in which a multi-page `getAll` (e.g. `getAll`, + * `getAllByType`, `getAllByTag`) will wait between individual page requests. + * + * This is done to ensure API performance is sustainable and reduces the chance + * of a failed API request due to overloading. + */ +export const GET_ALL_QUERY_DELAY = 500 + +/** + * The default number of milliseconds to wait before retrying a rate-limited + * `fetch()` request (429 response code). The default value is only used if the + * response does not include a `retry-after` header. + * + * The API allows up to 200 requests per second. + */ +const DEFUALT_RETRY_AFTER_MS = 1000 + +/** + * 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 PrismicDocument, + TDocumentType extends TDocuments["type"], +> = + Extract extends never + ? TDocuments + : Extract + +/** + * Modes for client ref management. + */ +enum RefStateMode { + /** + * Use the repository's master ref. + */ + Master = "Master", + + /** + * Use a given Release identified by its ID. + */ + ReleaseID = "ReleaseID", + + /** + * Use a given Release identified by its label. + */ + ReleaseLabel = "ReleaseLabel", + + /** + * Use a given ref. + */ + Manual = "Manual", +} + +/** + * An object containing stateful information about a client's ref strategy. + */ +type RefState = { + /** + * Determines if automatic preview support is enabled. + */ + autoPreviewsEnabled: boolean + + /** + * An optional HTTP server request object used during previews if automatic + * previews are enabled. + */ + httpRequest?: HttpRequestLike +} & ( + | { + mode: RefStateMode.Master + } + | { + mode: RefStateMode.ReleaseID + releaseID: string + } + | { + mode: RefStateMode.ReleaseLabel + releaseLabel: string + } + | { + mode: RefStateMode.Manual + ref: RefStringOrThunk + } +) + +/** + * A ref or a function that returns a ref. If a static ref is known, one can be + * given. If the ref must be fetched on-demand, a function can be provided. This + * function can optionally be asynchronous. + */ +type RefStringOrThunk = + | string + | (() => string | undefined | Promise) + +/** + * Configuration for clients that determine how content is queried. + */ +export type ClientConfig = { + /** + * The full Rest API V2 endpoint for the repository. This is only helpful if + * you're using Prismic behind a proxy which we do not recommend. + * + * @defaultValue `getRepositoryEndpoint(repositoryNameOrEndpoint)` + */ + apiEndpoint?: string + + /** + * The secure token for accessing the Prismic repository. This is only + * required if the repository is set to private. + */ + accessToken?: string + + /** + * A string representing a version of the Prismic repository's content. This + * may point to the latest version (called the "master ref"), or a preview + * with draft content. + */ + ref?: RefStringOrThunk + + /** + * A list of route resolver objects that define how a document's `url` + * property is resolved. + * + * {@link https://prismic.io/docs/route-resolver} + */ + routes?: NonNullable + + /** + * The `brokenRoute` option allows you to define the route populated in the + * `url` property for broken link or content relationship fields. A broken + * link is a link or content relationship field whose linked document has been + * unpublished or deleted. + * + * {@link https://prismic.io/docs/route-resolver} + */ + brokenRoute?: NonNullable + + /** + * Default parameters that will be sent with each query. These parameters can + * be overridden on each query if needed. + */ + defaultParams?: Omit< + BuildQueryURLArgs, + "ref" | "integrationFieldsRef" | "accessToken" | "routes" | "brokenRoute" + > +} & BaseClientConfig + +/** + * Parameters specific to client methods that fetch all documents. These methods + * start with `getAll` (for example, `getAllByType`). + */ +type GetAllParams = { + /** + * Limit the number of documents queried. If a number is not provided, there + * will be no limit and all matching documents will be returned. + */ + limit?: number +} + +/** + * Arguments to determine how the URL for a preview session is resolved. + */ +type ResolvePreviewArgs = { + /** + * A function that maps a Prismic document to a URL within your app. + */ + linkResolver?: LinkResolverFunction + + /** + * A fallback URL if the link resolver does not return a value. + */ + defaultURL: string + + /** + * The preview token (also known as a ref) that will be used to query preview + * content from the Prismic repository. + */ + previewToken?: string + + /** + * The previewed document that will be used to determine the destination URL. + */ + documentID?: string +} + +/** + * A client that allows querying content from a Prismic repository. + * + * If used in an environment where a global `fetch` function is unavailable, + * such as Node.js, the `fetch` option must be provided as part of the `options` + * parameter. + * + * @typeParam TDocuments - Document types that are registered for the Prismic + * repository. Query methods will automatically be typed based on this type. + */ +export class Client< + TDocuments extends PrismicDocument = PrismicDocument, +> extends BaseClient { + #repositoryName: string | undefined + + /** + * The Prismic repository's name. + */ + set repositoryName(value: string) { + this.#repositoryName = value + } + + /** + * The Prismic repository's name. + */ + get repositoryName(): string { + if (!this.#repositoryName) { + throw new PrismicError( + `This client instance was created using a repository endpoint and the repository name could not be infered from the it (\`${this.apiEndpoint}\`). The method you're trying to use relies on the repository name to work and is therefore disabled. Please create the client using your repository name, and the \`apiEndpoint\` option if necessary, to ensure all methods are enabled. For more details, see ${devMsg("prefer-repository-name")}`, + undefined, + undefined, + ) + } + + return this.#repositoryName + } + + /** + * The Prismic REST API V2 endpoint for the repository (use + * `prismic.getRepositoryEndpoint` for the default endpoint). + */ + apiEndpoint: string + + /** + * The Prismic REST API V2 endpoint for the repository (use + * `prismic.getRepositoryEndpoint` for the default endpoint). + * + * @deprecated Use `apiEndpoint` instead. + */ + // TODO: Remove in v8. + set endpoint(value: string) { + this.apiEndpoint = value + } + + /** + * The Prismic REST API V2 endpoint for the repository (use + * `prismic.getRepositoryEndpoint` for the default endpoint). + * + * @deprecated Use `apiEndpoint` instead. + */ + // TODO: Remove in v8. + get endpoint(): string { + return this.apiEndpoint + } + + /** + * The secure token for accessing the API (only needed if your repository is + * set to private). + * + * {@link https://user-guides.prismic.io/en/articles/1036153-generating-an-access-token} + */ + accessToken?: string + + /** + * A list of route resolver objects that define how a document's `url` field + * is resolved. + * + * {@link https://prismic.io/docs/route-resolver} + */ + routes?: NonNullable + + /** + * The `brokenRoute` option allows you to define the route populated in the + * `url` property for broken link or content relationship fields. A broken + * link is a link or content relationship field whose linked document has been + * unpublished or deleted. + * + * {@link https://prismic.io/docs/route-resolver} + */ + brokenRoute?: NonNullable + + /** + * Default parameters that will be sent with each query. These parameters can + * be overridden on each query if needed. + */ + defaultParams?: Omit< + BuildQueryURLArgs, + "ref" | "integrationFieldsRef" | "accessToken" | "routes" + > + + /** + * The client's ref mode state. This determines which ref is used during + * queries. + */ + private refState: RefState = { + mode: RefStateMode.Master, + autoPreviewsEnabled: true, + } + + /** + * Cached repository value. + */ + private cachedRepository: Repository | undefined + + /** + * Timestamp at which the cached repository data is considered stale. + */ + private cachedRepositoryExpiration = 0 + + /** + * Creates a Prismic client that can be used to query a repository. + * + * If used in an environment where a global `fetch` function is unavailable, + * such as in some Node.js versions, the `fetch` option must be provided as + * part of the `options` parameter. + * + * @param repositoryNameOrEndpoint - The Prismic repository name or full Rest + * API V2 endpoint for the repository. + * @param options - Configuration that determines how content will be queried + * from the Prismic repository. + * + * @returns A client that can query content from the repository. + */ + constructor(repositoryNameOrEndpoint: string, options: ClientConfig = {}) { + super(options) + + if ( + (options.apiEndpoint || isRepositoryEndpoint(repositoryNameOrEndpoint)) && + process.env.NODE_ENV === "development" + ) { + const apiEndpoint = options.apiEndpoint || repositoryNameOrEndpoint + + // Matches non-API v2 `.prismic.io` endpoints, see: https://regex101.com/r/xRsavu/1 + if (/\.prismic\.io\/(?!api\/v2\/?)/i.test(apiEndpoint)) { + throw new PrismicError( + "@prismicio/client only supports Prismic Rest API V2. Please provide only the repository name to the first createClient() parameter or use the getRepositoryEndpoint() helper to generate a valid Rest API V2 endpoint URL.", + undefined, + undefined, + ) + } + + const hostname = new URL(apiEndpoint).hostname.toLowerCase() + + // Matches non-.cdn `.prismic.io` endpoints + if ( + hostname.endsWith(".prismic.io") && + !hostname.endsWith(".cdn.prismic.io") + ) { + const repositoryName = getRepositoryName(apiEndpoint) + const dotCDNEndpoint = getRepositoryEndpoint(repositoryName) + console.warn( + `[@prismicio/client] A non-.cdn endpoint was provided to create a client with (\`${apiEndpoint}\`). Non-.cdn endpoints can have unexpected side-effects and cause performance issues when querying Prismic. Please convert it to the \`.cdn\` alternative (\`${dotCDNEndpoint}\`) or use the repository name directly instead (\`${repositoryName}\`). For more details, see ${devMsg( + "endpoint-must-use-cdn", + )}`, + ) + } + + // Warn if the user provided both a repository endpoint and an `apiEndpoint` and they are different + if ( + options.apiEndpoint && + isRepositoryEndpoint(repositoryNameOrEndpoint) && + repositoryNameOrEndpoint !== options.apiEndpoint + ) { + console.warn( + `[@prismicio/client] A repository endpoint was provided to create a client with (\`${repositoryNameOrEndpoint}\`) along with a different \`apiEndpoint\` options. The \`apiEndpoint\` option will be preferred over the repository endpoint to query content. Please create the client using your repository name, and the \`apiEndpoint\` option if necessary, instead. For more details, see ${devMsg("prefer-repository-name")}`, + ) + } + } + + if (isRepositoryEndpoint(repositoryNameOrEndpoint)) { + this.apiEndpoint = repositoryNameOrEndpoint + try { + this.repositoryName = getRepositoryName(repositoryNameOrEndpoint) + } catch (error) { + console.warn( + `[@prismicio/client] Could not infer the repository name from the repository endpoint that was provided to create the client with (\`${repositoryNameOrEndpoint}\`). Methods requiring the repository name to work will be disabled. Please create the client using your repository name, and the \`apiEndpoint\` option if necessary, to ensure all methods are enabled. For more details, see ${devMsg("prefer-repository-name")}`, + ) + } + } else { + this.apiEndpoint = + options.apiEndpoint || getRepositoryEndpoint(repositoryNameOrEndpoint) + this.repositoryName = repositoryNameOrEndpoint + } + + this.accessToken = options.accessToken + this.routes = options.routes + this.brokenRoute = options.brokenRoute + this.defaultParams = options.defaultParams + + if (options.ref) { + this.queryContentFromRef(options.ref) + } + + this.graphQLFetch = this.graphQLFetch.bind(this) + } + + /** + * Enables the client to automatically query content from a preview session if + * one is active in browser environments. This is enabled by default in the + * browser. + * + * For server environments, use `enableAutoPreviewsFromReq`. + * + * @example + * + * ```ts + * client.enableAutoPreviews() + * ``` + * + * @see enableAutoPreviewsFromReq + */ + enableAutoPreviews(): void { + this.refState.autoPreviewsEnabled = true + } + + /** + * Enables the client to automatically query content from a preview session if + * one is active in server environments. This is disabled by default on the + * server. + * + * For browser environments, use `enableAutoPreviews`. + * + * @example + * + * ```ts + * // In an express app + * app.get("/", function (req, res) { + * client.enableAutoPreviewsFromReq(req) + * }) + * ``` + * + * @param req - An HTTP server request object containing the request's + * cookies. + */ + enableAutoPreviewsFromReq(req: R): void { + this.refState.httpRequest = req + this.refState.autoPreviewsEnabled = true + } + + /** + * Disables the client from automatically querying content from a preview + * session if one is active. + * + * Automatic preview content querying is enabled by default unless this method + * is called. + * + * @example + * + * ```ts + * client.disableAutoPreviews() + * ``` + */ + disableAutoPreviews(): void { + this.refState.autoPreviewsEnabled = false + } + + /** + * Queries content from the Prismic repository. + * + * @example + * + * ```ts + * const response = await client.get() + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param params - Parameters to filter, sort, and paginate results. + * + * @returns A paginated response containing the result of the query. + */ + async get( + params?: Partial & FetchParams, + ): Promise> { + const url = await this.buildQueryURL(params) + + return await this.fetch>(url, params) + } + + /** + * Queries content from the Prismic repository and returns only the first + * result, if any. + * + * @example + * + * ```ts + * const document = await client.getFirst() + * ``` + * + * @typeParam TDocument - Type of the Prismic document returned. + * + * @param params - Parameters to filter, sort, and paginate results. @returns + * The first result of the query, if any. + */ + async getFirst( + params?: Partial & FetchParams, + ): Promise { + const actualParams = { ...params } + if (!(params && params.page) && !params?.pageSize) { + actualParams.pageSize = this.defaultParams?.pageSize ?? 1 + } + const url = await this.buildQueryURL(actualParams) + const result = await this.fetch>(url, params) + + const firstResult = result.results[0] + + if (firstResult) { + return firstResult + } + + throw new NotFoundError("No documents were returned", url, undefined) + } + + /** + * **IMPORTANT**: Avoid using `dangerouslyGetAll` as it may be slower and + * require more resources than other methods. Prefer using other methods that + * filter by filters such as `getAllByType`. + * + * Queries content from the Prismic repository and returns all matching + * content. If no filters are provided, all documents will be fetched. + * + * This method may make multiple network requests to query all matching + * content. + * + * @example + * + * ```ts + * const response = await client.dangerouslyGetAll() + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param params - Parameters to filter, sort, and paginate results. + * + * @returns A list of documents matching the query. + */ + async dangerouslyGetAll( + params: Partial> & + GetAllParams & + FetchParams = {}, + ): Promise { + const { limit = Infinity, ...actualParams } = params + const resolvedParams = { + ...actualParams, + pageSize: Math.min( + limit, + actualParams.pageSize || this.defaultParams?.pageSize || MAX_PAGE_SIZE, + ), + } + + const documents: TDocument[] = [] + let latestResult: Query | undefined + + while ( + (!latestResult || latestResult.next_page) && + documents.length < limit + ) { + const page = latestResult ? latestResult.page + 1 : undefined + + latestResult = await this.get({ ...resolvedParams, page }) + documents.push(...latestResult.results) + + if (latestResult.next_page) { + await new Promise((res) => setTimeout(res, GET_ALL_QUERY_DELAY)) + } + } + + return documents.slice(0, limit) + } + + /** + * Queries a document from the Prismic repository with a specific ID. + * + * @remarks + * A document's UID is different from its ID. An ID is automatically generated + * for all documents and is made available on its `id` property. A UID is + * provided in the Prismic editor and is unique among all documents of its + * custom type. + * + * @example + * + * ```ts + * const document = await client.getByID("WW4bKScAAMAqmluX") + * ``` + * + * @typeParam TDocument- Type of the Prismic document returned. + * + * @param id - ID of the document. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns The document with an ID matching the `id` parameter, if a matching + * document exists. + */ + async getByID( + id: string, + params?: Partial & FetchParams, + ): Promise { + return await this.getFirst( + appendFilters(params, filter.at("document.id", id)), + ) + } + + /** + * Queries documents from the Prismic repository with specific IDs. + * + * @remarks + * A document's UID is different from its ID. An ID is automatically generated + * for all documents and is made available on its `id` property. A UID is + * provided in the Prismic editor and is unique among all documents of its + * custom type. + * + * @example + * + * ```ts + * const response = await client.getByIDs([ + * "WW4bKScAAMAqmluX", + * "U1kTRgEAAC8A5ldS", + * ]) + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param ids - A list of document IDs. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A paginated response containing documents with IDs matching the + * `ids` parameter. + */ + async getByIDs( + ids: string[], + params?: Partial & FetchParams, + ): Promise> { + return await this.get( + appendFilters(params, filter.in("document.id", ids)), + ) + } + + /** + * Queries all documents from the Prismic repository with specific IDs. + * + * This method may make multiple network requests to query all matching + * content. + * + * @remarks + * A document's UID is different from its ID. An ID is automatically generated + * for all documents and is made available on its `id` property. A UID is + * provided in the Prismic editor and is unique among all documents of its + * custom type. + * + * @example + * + * ```ts + * const response = await client.getAllByIDs([ + * "WW4bKScAAMAqmluX", + * "U1kTRgEAAC8A5ldS", + * ]) + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param ids - A list of document IDs. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A list of documents with IDs matching the `ids` parameter. + */ + async getAllByIDs( + ids: string[], + params?: Partial & GetAllParams & FetchParams, + ): Promise { + return await this.dangerouslyGetAll( + appendFilters(params, filter.in("document.id", ids)), + ) + } + + /** + * Queries a document from the Prismic repository with a specific UID and + * custom type. + * + * @remarks + * A document's UID is different from its ID. An ID is automatically generated + * for all documents and is made available on its `id` property. A UID is + * provided in the Prismic editor and is unique among all documents of its + * custom type. + * + * @example + * + * ```ts + * const document = await client.getByUID("blog_post", "my-first-post") + * ``` + * + * @typeParam TDocument - Type of the Prismic document returned. + * + * @param documentType - The API ID of the document's custom type. + * @param uid - UID of the document. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns The document with a UID matching the `uid` parameter, if a + * matching document exists. + */ + async getByUID< + TDocument extends TDocuments, + TDocumentType extends TDocument["type"] = TDocument["type"], + >( + documentType: TDocumentType, + uid: string, + params?: Partial & FetchParams, + ): Promise> { + return await this.getFirst>( + appendFilters(params, [ + typeFilter(documentType), + filter.at(`my.${documentType}.uid`, uid), + ]), + ) + } + + /** + * Queries document from the Prismic repository with specific UIDs and Custom + * Type. + * + * @remarks + * A document's UID is different from its ID. An ID is automatically generated + * for all documents and is made available on its `id` property. A UID is + * provided in the Prismic editor and is unique among all documents of its + * custom type. + * + * @example + * + * ```ts + * const document = await client.getByUIDs("blog_post", [ + * "my-first-post", + * "my-second-post", + * ]) + * ``` + * + * @typeParam TDocument - Type of the Prismic document returned. + * + * @param documentType - The API ID of the document's custom type. + * @param uids - A list of document UIDs. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A paginated response containing documents with UIDs matching the + * `uids` parameter. + */ + async getByUIDs< + TDocument extends TDocuments, + TDocumentType extends TDocument["type"] = TDocument["type"], + >( + documentType: TDocumentType, + uids: string[], + params?: Partial & FetchParams, + ): Promise>> { + return await this.get>( + appendFilters(params, [ + typeFilter(documentType), + filter.in(`my.${documentType}.uid`, uids), + ]), + ) + } + + /** + * Queries all documents from the Prismic repository with specific UIDs and + * custom type. + * + * This method may make multiple network requests to query all matching + * content. + * + * @remarks + * A document's UID is different from its ID. An ID is automatically generated + * for all documents and is made available on its `id` property. A UID is + * provided in the Prismic editor and is unique among all documents of its + * custom type. + * + * @example + * + * ```ts + * const response = await client.getAllByUIDs([ + * "my-first-post", + * "my-second-post", + * ]) + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param documentType - The API ID of the document's custom type. + * @param uids - A list of document UIDs. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A list of documents with UIDs matching the `uids` parameter. + */ + async getAllByUIDs< + TDocument extends TDocuments, + TDocumentType extends TDocument["type"] = TDocument["type"], + >( + documentType: TDocumentType, + uids: string[], + params?: Partial & GetAllParams & FetchParams, + ): Promise[]> { + return await this.dangerouslyGetAll< + ExtractDocumentType + >( + appendFilters(params, [ + typeFilter(documentType), + filter.in(`my.${documentType}.uid`, uids), + ]), + ) + } + + /** + * Queries a singleton document from the Prismic repository for a specific + * custom type. + * + * @remarks + * A singleton document is one that is configured in Prismic to only allow one + * instance. For example, a repository may be configured to contain just one + * Settings document. This is in contrast to a repeatable custom type which + * allows multiple instances of itself. + * + * @example + * + * ```ts + * const document = await client.getSingle("settings") + * ``` + * + * @typeParam TDocument - Type of the Prismic document returned. + * + * @param documentType - The API ID of the singleton custom type. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns The singleton document for the custom type, if a matching document + * exists. + */ + async getSingle< + TDocument extends TDocuments, + TDocumentType extends TDocument["type"] = TDocument["type"], + >( + documentType: TDocumentType, + params?: Partial & FetchParams, + ): Promise> { + return await this.getFirst>( + appendFilters(params, typeFilter(documentType)), + ) + } + + /** + * Queries documents from the Prismic repository for a specific custom type. + * + * Use `getAllByType` instead if you need to query all documents for a + * specific custom type. + * + * @example + * + * ```ts + * const response = await client.getByType("blog_post") + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param documentType - The API ID of the custom type. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A paginated response containing documents of the custom type. + */ + async getByType< + TDocument extends TDocuments, + TDocumentType extends TDocument["type"] = TDocument["type"], + >( + documentType: TDocumentType, + params?: Partial & FetchParams, + ): Promise>> { + return await this.get>( + appendFilters(params, typeFilter(documentType)), + ) + } + + /** + * Queries all documents from the Prismic repository for a specific Custom + * Type. + * + * This method may make multiple network requests to query all matching + * content. + * + * @example + * + * ```ts + * const response = await client.getByType("blog_post") + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param documentType - The API ID of the custom type. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A list of all documents of the custom type. + */ + async getAllByType< + TDocument extends TDocuments, + TDocumentType extends TDocument["type"] = TDocument["type"], + >( + documentType: TDocumentType, + params?: Partial> & + GetAllParams & + FetchParams, + ): Promise[]> { + return await this.dangerouslyGetAll< + ExtractDocumentType + >(appendFilters(params, typeFilter(documentType))) + } + + /** + * Queries documents from the Prismic repository with a specific tag. + * + * Use `getAllByTag` instead if you need to query all documents with a + * specific tag. + * + * @example + * + * ```ts + * const response = await client.getByTag("food") + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param tag - The tag that must be included on a document. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A paginated response containing documents with the tag. + */ + async getByTag( + tag: string, + params?: Partial & FetchParams, + ): Promise> { + return await this.get(appendFilters(params, someTagsFilter(tag))) + } + + /** + * Queries all documents from the Prismic repository with a specific tag. + * + * This method may make multiple network requests to query all matching + * content. + * + * @example + * + * ```ts + * const response = await client.getAllByTag("food") + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param tag - The tag that must be included on a document. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A list of all documents with the tag. + */ + async getAllByTag( + tag: string, + params?: Partial> & + GetAllParams & + FetchParams, + ): Promise { + return await this.dangerouslyGetAll( + appendFilters(params, someTagsFilter(tag)), + ) + } + + /** + * Queries documents from the Prismic repository with specific tags. A + * document must be tagged with all of the queried tags to be included. + * + * @example + * + * ```ts + * const response = await client.getByEveryTag(["food", "fruit"]) + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param tags - A list of tags that must be included on a document. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A paginated response containing documents with the tags. + */ + async getByEveryTag( + tags: string[], + params?: Partial & FetchParams, + ): Promise> { + return await this.get( + appendFilters(params, everyTagFilter(tags)), + ) + } + + /** + * Queries documents from the Prismic repository with specific tags. A + * document must be tagged with all of the queried tags to be included. + * + * This method may make multiple network requests to query all matching + * content. + * + * @example + * + * ```ts + * const response = await client.getAllByEveryTag(["food", "fruit"]) + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param tags - A list of tags that must be included on a document. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A list of all documents with the tags. + */ + async getAllByEveryTag( + tags: string[], + params?: Partial> & + GetAllParams & + FetchParams, + ): Promise { + return await this.dangerouslyGetAll( + appendFilters(params, everyTagFilter(tags)), + ) + } + + /** + * Queries documents from the Prismic repository with specific tags. A + * document must be tagged with at least one of the queried tags to be + * included. + * + * @example + * + * ```ts + * const response = await client.getByEveryTag(["food", "fruit"]) + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param tags - A list of tags that must be included on a document. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A paginated response containing documents with at least one of the + * tags. + */ + async getBySomeTags( + tags: string[], + params?: Partial & FetchParams, + ): Promise> { + return await this.get( + appendFilters(params, someTagsFilter(tags)), + ) + } + + /** + * Queries documents from the Prismic repository with specific tags. A + * document must be tagged with at least one of the queried tags to be + * included. + * + * This method may make multiple network requests to query all matching + * content. + * + * @example + * + * ```ts + * const response = await client.getAllBySomeTags(["food", "fruit"]) + * ``` + * + * @typeParam TDocument - Type of Prismic documents returned. + * + * @param tags - A list of tags that must be included on a document. + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A list of all documents with at least one of the tags. + */ + async getAllBySomeTags( + tags: string[], + params?: Partial> & + GetAllParams & + FetchParams, + ): Promise { + return await this.dangerouslyGetAll( + appendFilters(params, someTagsFilter(tags)), + ) + } + + /** + * Returns metadata about the Prismic repository, such as its refs, releases, + * and custom types. + * + * @returns Repository metadata. + */ + async getRepository(params?: FetchParams): Promise { + // TODO: Restore when Authorization header support works in browsers with CORS. + // return await this.fetch(this.endpoint); + + const url = new URL(this.apiEndpoint) + + if (this.accessToken) { + url.searchParams.set("access_token", this.accessToken) + } + + return await this.fetch(url.toString(), params) + } + + /** + * Returns a list of all refs for the Prismic repository. + * + * Refs are used to identify which version of the repository's content should + * be queried. All repositories will have at least one ref pointing to the + * latest published content called the "master ref". + * + * @returns A list of all refs for the Prismic repository. + */ + async getRefs(params?: FetchParams): Promise { + const repository = await this.getRepository(params) + + return repository.refs + } + + /** + * Returns a ref for the Prismic repository with a matching ID. + * + * @param id - ID of the ref. + * + * @returns The ref with a matching ID, if it exists. + */ + async getRefByID(id: string, params?: FetchParams): Promise { + const refs = await this.getRefs(params) + + return findRefByID(refs, id) + } + + /** + * Returns a ref for the Prismic repository with a matching label. + * + * @param label - Label of the ref. + * + * @returns The ref with a matching label, if it exists. + */ + async getRefByLabel(label: string, params?: FetchParams): Promise { + const refs = await this.getRefs(params) + + return findRefByLabel(refs, label) + } + + /** + * Returns the master ref for the Prismic repository. The master ref points to + * the repository's latest published content. + * + * @returns The repository's master ref. + */ + async getMasterRef(params?: FetchParams): Promise { + const refs = await this.getRefs(params) + + return findMasterRef(refs) + } + + /** + * Returns a list of all Releases for the Prismic repository. Releases are + * used to group content changes before publishing. + * + * @returns A list of all Releases for the Prismic repository. + */ + async getReleases(params?: FetchParams): Promise { + const refs = await this.getRefs(params) + + return refs.filter((ref) => !ref.isMasterRef) + } + + /** + * Returns a Release for the Prismic repository with a matching ID. + * + * @param id - ID of the Release. + * + * @returns The Release with a matching ID, if it exists. + */ + async getReleaseByID(id: string, params?: FetchParams): Promise { + const releases = await this.getReleases(params) + + return findRefByID(releases, id) + } + + /** + * Returns a Release for the Prismic repository with a matching label. + * + * @param label - Label of the ref. + * + * @returns The ref with a matching label, if it exists. + */ + async getReleaseByLabel(label: string, params?: FetchParams): Promise { + const releases = await this.getReleases(params) + + return findRefByLabel(releases, label) + } + + /** + * Returns a list of all tags used in the Prismic repository. + * + * @returns A list of all tags used in the repository. + */ + async getTags(params?: FetchParams): Promise { + try { + const tagsForm = await this.getCachedRepositoryForm("tags", params) + + const url = new URL(tagsForm.action) + + if (this.accessToken) { + url.searchParams.set("access_token", this.accessToken) + } + + return await this.fetch(url.toString(), params) + } catch { + const repository = await this.getRepository(params) + + return repository.tags + } + } + + /** + * Builds a URL used to query content from the Prismic repository. + * + * @param params - Parameters to filter, sort, and paginate the results. + * + * @returns A URL string that can be requested to query content. + */ + async buildQueryURL({ + signal, + fetchOptions, + ...params + }: Partial & FetchParams = {}): Promise { + const ref = + params.ref || (await this.getResolvedRefString({ signal, fetchOptions })) + const integrationFieldsRef = + params.integrationFieldsRef || + (await this.getCachedRepository({ signal, fetchOptions })) + .integrationFieldsRef || + undefined + + return buildQueryURL(this.apiEndpoint, { + ...this.defaultParams, + ...params, + ref, + integrationFieldsRef, + routes: params.routes || this.routes, + brokenRoute: params.brokenRoute || this.brokenRoute, + accessToken: params.accessToken || this.accessToken, + }) + } + + /** + * Determines the URL for a previewed document during an active preview + * session. The result of this method should be used to redirect the user to + * the document's URL. + * + * @example + * + * ```ts + * const url = client.resolvePreviewURL({ + * linkResolver: (document) => `/${document.uid}` + * defaultURL: '/' + * }) + * ``` + * + * @param args - Arguments to configure the URL resolving. + * + * @returns The URL for the previewed document during an active preview + * session. The user should be redirected to this URL. + */ + async resolvePreviewURL( + args: ResolvePreviewArgs & FetchParams, + ): Promise { + let documentID: string | undefined | null = args.documentID + let previewToken: string | undefined | null = args.previewToken + + if (typeof globalThis.location !== "undefined") { + const searchParams = new URLSearchParams(globalThis.location.search) + + documentID = documentID || searchParams.get("documentId") + previewToken = previewToken || searchParams.get("token") + } else if (this.refState.httpRequest) { + if ("query" in this.refState.httpRequest) { + documentID = + documentID || (this.refState.httpRequest.query?.documentId as string) + previewToken = + previewToken || (this.refState.httpRequest.query?.token as string) + } else if ( + "url" in this.refState.httpRequest && + this.refState.httpRequest.url + ) { + // Including "missing-host://" by default + // handles a case where Next.js Route Handlers + // only provide the pathname and search + // parameters in the `url` property + // (e.g. `/api/preview?foo=bar`). + const searchParams = new URL( + this.refState.httpRequest.url, + "missing-host://", + ).searchParams + + documentID = documentID || searchParams.get("documentId") + previewToken = previewToken || searchParams.get("token") + } + } + + if (documentID != null && previewToken != null) { + const document = await this.getByID(documentID, { + ref: previewToken, + lang: "*", + signal: args.signal, + fetchOptions: args.fetchOptions, + }) + + const url = asLink(document, { linkResolver: args.linkResolver }) + + if (typeof url === "string") { + return url + } + } + + return args.defaultURL + } + + /** + * Configures the client to query the latest published content for all future + * queries. + * + * If the `ref` parameter is provided during a query, it takes priority for + * that query. + * + * @example + * + * ```ts + * await client.queryLatestContent() + * const document = await client.getByID("WW4bKScAAMAqmluX") + * ``` + */ + queryLatestContent(): void { + this.refState.mode = RefStateMode.Master + } + + /** + * Configures the client to query content from a specific Release identified + * by its ID for all future queries. + * + * If the `ref` parameter is provided during a query, it takes priority for + * that query. + * + * @example + * + * ```ts + * await client.queryContentFromReleaseByID("YLB7OBAAACMA7Cpa") + * const document = await client.getByID("WW4bKScAAMAqmluX") + * ``` + * + * @param releaseID - The ID of the Release. + */ + queryContentFromReleaseByID(releaseID: string): void { + this.refState = { + ...this.refState, + mode: RefStateMode.ReleaseID, + releaseID, + } + } + + /** + * Configures the client to query content from a specific Release identified + * by its label for all future queries. + * + * If the `ref` parameter is provided during a query, it takes priority for + * that query. + * + * @example + * + * ```ts + * await client.queryContentFromReleaseByLabel("My Release") + * const document = await client.getByID("WW4bKScAAMAqmluX") + * ``` + * + * @param releaseLabel - The label of the Release. + */ + queryContentFromReleaseByLabel(releaseLabel: string): void { + this.refState = { + ...this.refState, + mode: RefStateMode.ReleaseLabel, + releaseLabel, + } + } + + /** + * Configures the client to query content from a specific ref. The ref can be + * provided as a string or a function. + * + * If a function is provided, the ref is fetched lazily before each query. The + * function may also be asynchronous. + * + * @example + * + * ```ts + * await client.queryContentFromRef("my-ref") + * const document = await client.getByID("WW4bKScAAMAqmluX") + * ``` + * + * @param ref - The ref or a function that returns the ref from which to query + * content. + */ + queryContentFromRef(ref: RefStringOrThunk): void { + this.refState = { + ...this.refState, + mode: RefStateMode.Manual, + ref, + } + } + + /** + * A `fetch()` function to be used with GraphQL clients configured for + * Prismic's GraphQL API. It automatically applies the necessary `prismic-ref` + * and Authorization headers. Queries will automatically be minified by + * removing whitespace where possible. + * + * @example + * + * ```ts + * const graphQLClient = new ApolloClient({ + * link: new HttpLink({ + * uri: prismic.getGraphQLEndpoint(repositoryName), + * // Provide `client.graphQLFetch` as the fetch implementation. + * fetch: client.graphQLFetch, + * // Using GET is required. + * useGETForQueries: true, + * }), + * cache: new InMemoryCache(), + * }) + * ``` + * + * @param input - The `fetch()` `input` parameter. Only strings are supported. + * @param init - The `fetch()` `init` parameter. Only plain objects are + * supported. + * + * @returns The `fetch()` Response for the request. + * + * @experimental + */ + async graphQLFetch( + input: RequestInfo, + init?: Omit & { signal?: AbortSignalLike }, + ): Promise { + const cachedRepository = await this.getCachedRepository() + const ref = await this.getResolvedRefString() + + const unsanitizedHeaders: Record = { + "Prismic-ref": ref, + Authorization: this.accessToken ? `Token ${this.accessToken}` : "", + // Asserting `init.headers` is a Record since popular GraphQL + // libraries pass this as a Record. Header objects as input + // are unsupported. + ...(init ? (init.headers as Record) : {}), + } + + if (cachedRepository.integrationFieldsRef) { + unsanitizedHeaders["Prismic-integration-field-ref"] = + cachedRepository.integrationFieldsRef + } + + // Normalize header keys to lowercase. This prevents header + // conflicts between the Prismic client and the GraphQL + // client. + const headers: Record = {} + for (const key in unsanitizedHeaders) { + if (unsanitizedHeaders[key]) { + headers[key.toLowerCase()] = + unsanitizedHeaders[key as keyof typeof unsanitizedHeaders] + } + } + + const url = new URL( + // Asserting `input` is a string since popular GraphQL + // libraries pass this as a string. Request objects as + // input are unsupported. + input as string, + ) + + // This prevents the request from being cached unnecessarily. + // Without adding this `ref` param, re-running a query + // could return a locally cached response, even if the + // `ref` changed. This happens because the URL is + // identical when the `ref` is not included. Caches may ignore + // headers. + // + // The Prismic GraphQL API ignores the `ref` param. + url.searchParams.set("ref", ref) + + const query = url.searchParams.get("query") + if (query) { + url.searchParams.set( + "query", + // Compress the GraphQL query (if it exists) by + // removing whitespace. This is done to + // optimize the query size and avoid + // hitting the upper limit of GET requests + // (2048 characters). + minifyGraphQLQuery(query), + ) + } + + return (await this.fetchFn(url.toString(), { + ...init, + headers, + })) as Response + } + + /** + * Returns a cached version of `getRepository` with a TTL. + * + * @returns Cached repository metadata. + */ + private async getCachedRepository(params?: FetchParams): Promise { + if ( + !this.cachedRepository || + Date.now() >= this.cachedRepositoryExpiration + ) { + this.cachedRepositoryExpiration = Date.now() + REPOSITORY_CACHE_TTL + this.cachedRepository = await this.getRepository(params) + } + + return this.cachedRepository + } + + /** + * Returns a cached Prismic repository form. Forms are used to determine API + * endpoints for types of repository data. + * + * @param name - Name of the form. + * + * @returns The repository form. + * + * @throws If a matching form cannot be found. + */ + private async getCachedRepositoryForm( + name: string, + params?: FetchParams, + ): Promise
{ + const cachedRepository = await this.getCachedRepository(params) + const form = cachedRepository.forms[name] + + if (!form) { + throw new PrismicError( + `Form with name "${name}" could not be found`, + undefined, + undefined, + ) + } + + return form + } + + /** + * Returns the ref needed to query based on the client's current state. This + * method may make a network request to fetch a ref or resolve the user's ref + * thunk. + * + * If auto previews are enabled, the preview ref takes priority if available. + * + * The following strategies are used depending on the client's state: + * + * - If the user called `queryLatestContent`: Use the repository's master ref. + * The ref is cached for 5 seconds. After 5 seconds, a new master ref is + * fetched. + * - If the user called `queryContentFromReleaseByID`: Use the release's ref. + * The ref is cached for 5 seconds. After 5 seconds, a new ref for the + * release is fetched. + * - If the user called `queryContentFromReleaseByLabel`: Use the release's ref. + * The ref is cached for 5 seconds. After 5 seconds, a new ref for the + * release is fetched. + * - If the user called `queryContentFromRef`: Use the provided ref. Fall back + * to the master ref if the ref is not a string. + * + * @returns The ref to use during a query. + */ + private async getResolvedRefString(params?: FetchParams): Promise { + if (this.refState.autoPreviewsEnabled) { + let previewRef: string | undefined + + let cookieJar: string | null | undefined + + if (this.refState.httpRequest?.headers) { + if ( + "get" in this.refState.httpRequest.headers && + typeof this.refState.httpRequest.headers.get === "function" + ) { + // Web API Headers + cookieJar = this.refState.httpRequest.headers.get("cookie") + } else if ("cookie" in this.refState.httpRequest.headers) { + // Express-style headers + cookieJar = this.refState.httpRequest.headers.cookie + } + } else if (globalThis.document?.cookie) { + cookieJar = globalThis.document.cookie + } + + if (cookieJar) { + previewRef = getPreviewCookie(cookieJar) + } + + if (previewRef) { + return previewRef + } + } + + const cachedRepository = await this.getCachedRepository(params) + + const refModeType = this.refState.mode + if (refModeType === RefStateMode.ReleaseID) { + return findRefByID(cachedRepository.refs, this.refState.releaseID).ref + } else if (refModeType === RefStateMode.ReleaseLabel) { + return findRefByLabel(cachedRepository.refs, this.refState.releaseLabel) + .ref + } else if (refModeType === RefStateMode.Manual) { + const res = await castThunk(this.refState.ref)() + + if (typeof res === "string") { + return res + } + } + + return findMasterRef(cachedRepository.refs).ref + } + + /** + * Performs a network request using the configured `fetch` function. It + * assumes all successful responses will have a JSON content type. It also + * normalizes unsuccessful network requests. + * + * @typeParam T - The JSON response. + * + * @param url - URL to the resource to fetch. + * @param params - Prismic REST API parameters for the network request. + * + * @returns The JSON response from the network request. + */ + protected async fetch( + url: string, + params: FetchParams = {}, + ): Promise { + const res = await super.fetch(url, params) + + console.log(res.status, params.fetchOptions?.method || "GET", url) + if (res.status === 429) { + console.log(url, params, res) + } + + if (res.status !== 404 && res.status !== 429 && res.json == null) { + throw new PrismicError(undefined, url, res.json) + } + + switch (res.status) { + // Successful + case 200: + case 201: { + return res.json + } + + // Bad Request + // - Invalid filter syntax + // - Ref not provided (ignored) + case 400: { + throw new ParsingError(res.json.message, url, res.json) + } + + // Unauthorized + // - Missing access token for repository endpoint + // - Incorrect access token for repository endpoint + case 401: + // Forbidden + // - Missing access token for query endpoint + // - Incorrect access token for query endpoint + case 403: { + throw new ForbiddenError( + res.json.error || res.json.message, + url, + res.json, + ) + } + + // Not Found + // - Incorrect repository name (this response has an empty body) + // - Ref does not exist + // - Preview token is expired + case 404: { + if (res.json === undefined) { + throw new RepositoryNotFoundError( + `Prismic repository not found. Check that "${this.apiEndpoint}" is pointing to the correct repository.`, + url, + undefined, + ) + } + + if (res.json.type === "api_notfound_error") { + throw new RefNotFoundError(res.json.message, url, res.json) + } + + if ( + res.json.type === "api_security_error" && + /preview token.*expired/i.test(res.json.message) + ) { + throw new PreviewTokenExpiredError(res.json.message, url, res.json) + } + + throw new NotFoundError(res.json.message, url, res.json) + } + + // Gone + // - Ref is expired + case 410: { + throw new RefExpiredError(res.json.message, url, res.json) + } + + // Too Many Requests + // - Exceeded the maximum number of requests per second + case 429: { + const parsedRetryAfter = Number(res.headers.get("retry-after")) + const delay = Number.isNaN(parsedRetryAfter) + ? DEFUALT_RETRY_AFTER_MS + : parsedRetryAfter + + return await new Promise((resolve, reject) => { + setTimeout(async () => { + try { + resolve(await this.fetch(url, params)) + } catch (error) { + reject(error) + } + }, delay) + }) + } + } + + throw new PrismicError(undefined, url, res.json) + } +} diff --git a/src/createClient.ts b/src/createClient.ts index 070e134e..4d64b703 100644 --- a/src/createClient.ts +++ b/src/createClient.ts @@ -1,374 +1,7 @@ -import { appendFilters } from "./lib/appendFilters" -import { castThunk } from "./lib/castThunk" -import { devMsg } from "./lib/devMsg" -import { everyTagFilter } from "./lib/everyTagFilter" -import { findMasterRef } from "./lib/findMasterRef" -import { findRefByID } from "./lib/findRefByID" -import { findRefByLabel } from "./lib/findRefByLabel" -import { getPreviewCookie } from "./lib/getPreviewCookie" -import { minifyGraphQLQuery } from "./lib/minifyGraphQLQuery" -import { someTagsFilter } from "./lib/someTagsFilter" -import { typeFilter } from "./lib/typeFilter" - -import type { Query } from "./types/api/query" -import type { Ref } from "./types/api/ref" -import type { Form, Repository } from "./types/api/repository" import type { PrismicDocument } from "./types/value/document" -import { ForbiddenError } from "./errors/ForbiddenError" -import { NotFoundError } from "./errors/NotFoundError" -import { ParsingError } from "./errors/ParsingError" -import { PreviewTokenExpiredError } from "./errors/PreviewTokenExpired" -import { PrismicError } from "./errors/PrismicError" -import { RefExpiredError } from "./errors/RefExpiredError" -import { RefNotFoundError } from "./errors/RefNotFoundError" -import { RepositoryNotFoundError } from "./errors/RepositoryNotFoundError" - -import type { LinkResolverFunction } from "./helpers/asLink" -import { asLink } from "./helpers/asLink" - -import type { BuildQueryURLArgs } from "./buildQueryURL" -import { buildQueryURL } from "./buildQueryURL" -import { filter } from "./filter" -import { getRepositoryEndpoint } from "./getRepositoryEndpoint" -import { getRepositoryName } from "./getRepositoryName" -import { isRepositoryEndpoint } from "./isRepositoryEndpoint" - -/** - * The largest page size allowed by the Prismic REST API V2. This value is used - * to minimize the number of requests required to query content. - */ -const MAX_PAGE_SIZE = 100 - -/** - * The number of milliseconds in which repository metadata is considered valid. - * A ref can be invalidated quickly depending on how frequently content is - * updated in the Prismic repository. As such, repository's metadata can only be - * considered valid for a short amount of time. - */ -export const REPOSITORY_CACHE_TTL = 5000 - -/** - * The number of milliseconds in which a multi-page `getAll` (e.g. `getAll`, - * `getAllByType`, `getAllByTag`) will wait between individual page requests. - * - * This is done to ensure API performance is sustainable and reduces the chance - * of a failed API request due to overloading. - */ -export const GET_ALL_QUERY_DELAY = 500 - -/** - * The default number of milliseconds to wait before retrying a rate-limited - * `fetch()` request (429 response code). The default value is only used if the - * response does not include a `retry-after` header. - * - * The API allows up to 200 requests per second. - */ -const DEFUALT_RETRY_AFTER_MS = 1000 - -/** - * 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 PrismicDocument, - TDocumentType extends TDocuments["type"], -> = - Extract extends never - ? TDocuments - : Extract - -/** - * A universal API to make network requests. A subset of the `fetch()` API. - * - * {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch} - */ -export type FetchLike = ( - input: string, - init?: RequestInitLike, -) => Promise - -/** - * An object that allows you to abort a `fetch()` request if needed via an - * `AbortController` object - * - * {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal} - */ -// `any` is used often here to ensure this type is universally valid among -// different AbortSignal implementations. The types of each property are not -// important to validate since it is blindly passed to a given `fetch()` -// function. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AbortSignalLike = any - -/** - * A subset of RequestInit properties to configure a `fetch()` request. - */ -// Only options relevant to the client are included. Extending from the full -// RequestInit would cause issues, such as accepting Header objects. -// -// An interface is used to allow other libraries to augment the type with -// environment-specific types. -export interface RequestInitLike extends Pick { - /** - * An object literal to set the `fetch()` request's headers. - */ - headers?: Record - - /** - * An AbortSignal to set the `fetch()` request's signal. - * - * See: - * [https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) - */ - // NOTE: `AbortSignalLike` is `any`! It is left as `AbortSignalLike` - // for backwards compatibility (the type is exported) and to signal to - // other readers that this should be an AbortSignal-like object. - signal?: AbortSignalLike -} - -/** - * The minimum required properties from Response. - */ -export interface ResponseLike { - status: number - headers: HeadersLike - // eslint-disable-next-line @typescript-eslint/no-explicit-any - json(): Promise -} - -/** - * The minimum required properties from Headers. - */ -export interface HeadersLike { - get(name: string): string | null -} - -/** - * The minimum required properties to treat as an HTTP Request for automatic - * Prismic preview support. - */ -export type HttpRequestLike = - | /** - * Web API Request - * - * @see http://developer.mozilla.org/en-US/docs/Web/API/Request - */ - { - headers?: { - get(name: string): string | null - } - url?: string - } - - /** - * Express-style Request - */ - | { - headers?: { - cookie?: string - } - query?: Record - } - -/** - * Modes for client ref management. - */ -enum RefStateMode { - /** - * Use the repository's master ref. - */ - Master = "Master", - - /** - * Use a given Release identified by its ID. - */ - ReleaseID = "ReleaseID", - - /** - * Use a given Release identified by its label. - */ - ReleaseLabel = "ReleaseLabel", - - /** - * Use a given ref. - */ - Manual = "Manual", -} - -/** - * An object containing stateful information about a client's ref strategy. - */ -type RefState = { - /** - * Determines if automatic preview support is enabled. - */ - autoPreviewsEnabled: boolean - - /** - * An optional HTTP server request object used during previews if automatic - * previews are enabled. - */ - httpRequest?: HttpRequestLike -} & ( - | { - mode: RefStateMode.Master - } - | { - mode: RefStateMode.ReleaseID - releaseID: string - } - | { - mode: RefStateMode.ReleaseLabel - releaseLabel: string - } - | { - mode: RefStateMode.Manual - ref: RefStringOrThunk - } -) - -/** - * A ref or a function that returns a ref. If a static ref is known, one can be - * given. If the ref must be fetched on-demand, a function can be provided. This - * function can optionally be asynchronous. - */ -type RefStringOrThunk = - | string - | (() => string | undefined | Promise) - -/** - * Configuration for clients that determine how content is queried. - */ -export type ClientConfig = { - /** - * The secure token for accessing the Prismic repository. This is only - * required if the repository is set to private. - */ - accessToken?: string - - /** - * A string representing a version of the Prismic repository's content. This - * may point to the latest version (called the "master ref"), or a preview - * with draft content. - */ - ref?: RefStringOrThunk - - /** - * A list of route resolver objects that define how a document's `url` - * property is resolved. - * - * {@link https://prismic.io/docs/route-resolver} - */ - routes?: NonNullable - - /** - * The `brokenRoute` option allows you to define the route populated in the - * `url` property for broken link or content relationship fields. A broken - * link is a link or content relationship field whose linked document has been - * unpublished or deleted. - * - * {@link https://prismic.io/docs/route-resolver} - */ - brokenRoute?: NonNullable - - /** - * Default parameters that will be sent with each query. These parameters can - * be overridden on each query if needed. - */ - defaultParams?: Omit< - BuildQueryURLArgs, - "ref" | "integrationFieldsRef" | "accessToken" | "routes" | "brokenRoute" - > - - /** - * The function used to make network requests to the Prismic REST API. In - * environments where a global `fetch` function does not exist, such as - * Node.js, this function must be provided. - */ - fetch?: FetchLike - - /** - * Options provided to the client's `fetch()` on all network requests. These - * options will be merged with internally required options. They can also be - * overriden on a per-query basis using the query's `fetchOptions` parameter. - */ - fetchOptions?: RequestInitLike -} - -/** - * Parameters for any client method that use `fetch()`. - */ -type FetchParams = { - /** - * Options provided to the client's `fetch()` on all network requests. These - * options will be merged with internally required options. They can also be - * overriden on a per-query basis using the query's `fetchOptions` parameter. - */ - fetchOptions?: RequestInitLike - - /** - * An `AbortSignal` provided by an `AbortController`. This allows the network - * request to be cancelled if necessary. - * - * @deprecated Move the `signal` parameter into `fetchOptions.signal`: - * - * @see \ - */ - signal?: AbortSignalLike -} - -/** - * Parameters specific to client methods that fetch all documents. These methods - * start with `getAll` (for example, `getAllByType`). - */ -type GetAllParams = { - /** - * Limit the number of documents queried. If a number is not provided, there - * will be no limit and all matching documents will be returned. - */ - limit?: number -} - -/** - * Arguments to determine how the URL for a preview session is resolved. - */ -type ResolvePreviewArgs = { - /** - * A function that maps a Prismic document to a URL within your app. - */ - linkResolver?: LinkResolverFunction - - /** - * A fallback URL if the link resolver does not return a value. - */ - defaultURL: string - - /** - * The preview token (also known as a ref) that will be used to query preview - * content from the Prismic repository. - */ - previewToken?: string - - /** - * The previewed document that will be used to determine the destination URL. - */ - documentID?: string -} - -/** - * The result of a `fetch()` job. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type FetchJobResult = { - status: number - headers: HeadersLike - json: TJSON -} +import type { ClientConfig } from "./Client" +import { Client } from "./Client" /** * Type definitions for the `createClient()` function. May be augmented by @@ -407,1563 +40,3 @@ export const createClient: CreateClient = ( repositoryNameOrEndpoint: string, options?: ClientConfig, ) => new Client(repositoryNameOrEndpoint, options) - -/** - * A client that allows querying content from a Prismic repository. - * - * If used in an environment where a global `fetch` function is unavailable, - * such as Node.js, the `fetch` option must be provided as part of the `options` - * parameter. - * - * @typeParam TDocuments - Document types that are registered for the Prismic - * repository. Query methods will automatically be typed based on this type. - */ -export class Client { - /** - * The Prismic REST API V2 endpoint for the repository (use - * `prismic.getRepositoryEndpoint` for the default endpoint). - */ - endpoint: string - - /** - * The secure token for accessing the API (only needed if your repository is - * set to private). - * - * {@link https://user-guides.prismic.io/en/articles/1036153-generating-an-access-token} - */ - accessToken?: string - - /** - * A list of route resolver objects that define how a document's `url` field - * is resolved. - * - * {@link https://prismic.io/docs/route-resolver} - */ - routes?: NonNullable - - /** - * The `brokenRoute` option allows you to define the route populated in the - * `url` property for broken link or content relationship fields. A broken - * link is a link or content relationship field whose linked document has been - * unpublished or deleted. - * - * {@link https://prismic.io/docs/route-resolver} - */ - brokenRoute?: NonNullable - - /** - * The function used to make network requests to the Prismic REST API. In - * environments where a global `fetch` function does not exist, such as - * Node.js, this function must be provided. - */ - fetchFn: FetchLike - - fetchOptions?: RequestInitLike - - /** - * Default parameters that will be sent with each query. These parameters can - * be overridden on each query if needed. - */ - defaultParams?: Omit< - BuildQueryURLArgs, - "ref" | "integrationFieldsRef" | "accessToken" | "routes" - > - - /** - * The client's ref mode state. This determines which ref is used during - * queries. - */ - private refState: RefState = { - mode: RefStateMode.Master, - autoPreviewsEnabled: true, - } - - /** - * Cached repository value. - */ - private cachedRepository: Repository | undefined - - /** - * Timestamp at which the cached repository data is considered stale. - */ - private cachedRepositoryExpiration = 0 - - /** - * Active `fetch()` jobs keyed by URL and AbortSignal (if it exists). - */ - private fetchJobs: Record< - string, - Map> - > = {} - - /** - * Creates a Prismic client that can be used to query a repository. - * - * If used in an environment where a global `fetch` function is unavailable, - * such as Node.js, the `fetch` option must be provided as part of the - * `options` parameter. - * - * @param repositoryNameOrEndpoint - The Prismic repository name or full Rest - * API V2 endpoint for the repository. - * @param options - Configuration that determines how content will be queried - * from the Prismic repository. - * - * @returns A client that can query content from the repository. - */ - constructor(repositoryNameOrEndpoint: string, options: ClientConfig = {}) { - if (isRepositoryEndpoint(repositoryNameOrEndpoint)) { - if (process.env.NODE_ENV === "development") { - // Matches non-API v2 `.prismic.io` endpoints, see: https://regex101.com/r/xRsavu/1 - if (/\.prismic\.io\/(?!api\/v2\/?)/i.test(repositoryNameOrEndpoint)) { - throw new PrismicError( - "@prismicio/client only supports Prismic Rest API V2. Please provide only the repository name to the first createClient() parameter or use the getRepositoryEndpoint() helper to generate a valid Rest API V2 endpoint URL.", - undefined, - undefined, - ) - } - - const hostname = new URL( - repositoryNameOrEndpoint, - ).hostname.toLowerCase() - - // Matches non-.cdn `.prismic.io` endpoints - if ( - hostname.endsWith(".prismic.io") && - !hostname.endsWith(".cdn.prismic.io") - ) { - const repositoryName = getRepositoryName(repositoryNameOrEndpoint) - const dotCDNEndpoint = getRepositoryEndpoint(repositoryName) - console.warn( - `[@prismicio/client] A non-.cdn endpoint was provided to create a client with (\`${repositoryNameOrEndpoint}\`). Non-.cdn endpoints can have unexpected side-effects and cause performance issues when querying Prismic. Please convert it to the \`.cdn\` alternative (\`${dotCDNEndpoint}\`) or use the repository name directly instead (\`${repositoryName}\`). For more details, see ${devMsg( - "endpoint-must-use-cdn", - )}`, - ) - } - } - - this.endpoint = repositoryNameOrEndpoint - } else { - this.endpoint = getRepositoryEndpoint(repositoryNameOrEndpoint) - } - - this.accessToken = options.accessToken - this.routes = options.routes - this.brokenRoute = options.brokenRoute - this.fetchOptions = options.fetchOptions - this.defaultParams = options.defaultParams - - if (options.ref) { - this.queryContentFromRef(options.ref) - } - - if (typeof options.fetch === "function") { - this.fetchFn = options.fetch - } else if (typeof globalThis.fetch === "function") { - this.fetchFn = globalThis.fetch as FetchLike - } else { - throw new PrismicError( - "A valid fetch implementation was not provided. In environments where fetch is not available (including Node.js), a fetch implementation must be provided via a polyfill or the `fetch` option.", - undefined, - undefined, - ) - } - - // If the global fetch function is used, we must bind it to the global scope. - if (this.fetchFn === globalThis.fetch) { - this.fetchFn = this.fetchFn.bind(globalThis) - } - - this.graphQLFetch = this.graphQLFetch.bind(this) - } - - /** - * Enables the client to automatically query content from a preview session if - * one is active in browser environments. This is enabled by default in the - * browser. - * - * For server environments, use `enableAutoPreviewsFromReq`. - * - * @example - * - * ```ts - * client.enableAutoPreviews() - * ``` - * - * @see enableAutoPreviewsFromReq - */ - enableAutoPreviews(): void { - this.refState.autoPreviewsEnabled = true - } - - /** - * Enables the client to automatically query content from a preview session if - * one is active in server environments. This is disabled by default on the - * server. - * - * For browser environments, use `enableAutoPreviews`. - * - * @example - * - * ```ts - * // In an express app - * app.get("/", function (req, res) { - * client.enableAutoPreviewsFromReq(req) - * }) - * ``` - * - * @param req - An HTTP server request object containing the request's - * cookies. - */ - enableAutoPreviewsFromReq(req: R): void { - this.refState.httpRequest = req - this.refState.autoPreviewsEnabled = true - } - - /** - * Disables the client from automatically querying content from a preview - * session if one is active. - * - * Automatic preview content querying is enabled by default unless this method - * is called. - * - * @example - * - * ```ts - * client.disableAutoPreviews() - * ``` - */ - disableAutoPreviews(): void { - this.refState.autoPreviewsEnabled = false - } - - /** - * Queries content from the Prismic repository. - * - * @example - * - * ```ts - * const response = await client.get() - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param params - Parameters to filter, sort, and paginate results. - * - * @returns A paginated response containing the result of the query. - */ - async get( - params?: Partial & FetchParams, - ): Promise> { - const url = await this.buildQueryURL(params) - - return await this.fetch>(url, params) - } - - /** - * Queries content from the Prismic repository and returns only the first - * result, if any. - * - * @example - * - * ```ts - * const document = await client.getFirst() - * ``` - * - * @typeParam TDocument - Type of the Prismic document returned. - * - * @param params - Parameters to filter, sort, and paginate results. @returns - * The first result of the query, if any. - */ - async getFirst( - params?: Partial & FetchParams, - ): Promise { - const actualParams = { ...params } - if (!(params && params.page) && !params?.pageSize) { - actualParams.pageSize = this.defaultParams?.pageSize ?? 1 - } - const url = await this.buildQueryURL(actualParams) - const result = await this.fetch>(url, params) - - const firstResult = result.results[0] - - if (firstResult) { - return firstResult - } - - throw new NotFoundError("No documents were returned", url, undefined) - } - - /** - * **IMPORTANT**: Avoid using `dangerouslyGetAll` as it may be slower and - * require more resources than other methods. Prefer using other methods that - * filter by filters such as `getAllByType`. - * - * Queries content from the Prismic repository and returns all matching - * content. If no filters are provided, all documents will be fetched. - * - * This method may make multiple network requests to query all matching - * content. - * - * @example - * - * ```ts - * const response = await client.dangerouslyGetAll() - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param params - Parameters to filter, sort, and paginate results. - * - * @returns A list of documents matching the query. - */ - async dangerouslyGetAll( - params: Partial> & - GetAllParams & - FetchParams = {}, - ): Promise { - const { limit = Infinity, ...actualParams } = params - const resolvedParams = { - ...actualParams, - pageSize: Math.min( - limit, - actualParams.pageSize || this.defaultParams?.pageSize || MAX_PAGE_SIZE, - ), - } - - const documents: TDocument[] = [] - let latestResult: Query | undefined - - while ( - (!latestResult || latestResult.next_page) && - documents.length < limit - ) { - const page = latestResult ? latestResult.page + 1 : undefined - - latestResult = await this.get({ ...resolvedParams, page }) - documents.push(...latestResult.results) - - if (latestResult.next_page) { - await new Promise((res) => setTimeout(res, GET_ALL_QUERY_DELAY)) - } - } - - return documents.slice(0, limit) - } - - /** - * Queries a document from the Prismic repository with a specific ID. - * - * @remarks - * A document's UID is different from its ID. An ID is automatically generated - * for all documents and is made available on its `id` property. A UID is - * provided in the Prismic editor and is unique among all documents of its - * custom type. - * - * @example - * - * ```ts - * const document = await client.getByID("WW4bKScAAMAqmluX") - * ``` - * - * @typeParam TDocument- Type of the Prismic document returned. - * - * @param id - ID of the document. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns The document with an ID matching the `id` parameter, if a matching - * document exists. - */ - async getByID( - id: string, - params?: Partial & FetchParams, - ): Promise { - return await this.getFirst( - appendFilters(params, filter.at("document.id", id)), - ) - } - - /** - * Queries documents from the Prismic repository with specific IDs. - * - * @remarks - * A document's UID is different from its ID. An ID is automatically generated - * for all documents and is made available on its `id` property. A UID is - * provided in the Prismic editor and is unique among all documents of its - * custom type. - * - * @example - * - * ```ts - * const response = await client.getByIDs([ - * "WW4bKScAAMAqmluX", - * "U1kTRgEAAC8A5ldS", - * ]) - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param ids - A list of document IDs. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A paginated response containing documents with IDs matching the - * `ids` parameter. - */ - async getByIDs( - ids: string[], - params?: Partial & FetchParams, - ): Promise> { - return await this.get( - appendFilters(params, filter.in("document.id", ids)), - ) - } - - /** - * Queries all documents from the Prismic repository with specific IDs. - * - * This method may make multiple network requests to query all matching - * content. - * - * @remarks - * A document's UID is different from its ID. An ID is automatically generated - * for all documents and is made available on its `id` property. A UID is - * provided in the Prismic editor and is unique among all documents of its - * custom type. - * - * @example - * - * ```ts - * const response = await client.getAllByIDs([ - * "WW4bKScAAMAqmluX", - * "U1kTRgEAAC8A5ldS", - * ]) - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param ids - A list of document IDs. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A list of documents with IDs matching the `ids` parameter. - */ - async getAllByIDs( - ids: string[], - params?: Partial & GetAllParams & FetchParams, - ): Promise { - return await this.dangerouslyGetAll( - appendFilters(params, filter.in("document.id", ids)), - ) - } - - /** - * Queries a document from the Prismic repository with a specific UID and - * custom type. - * - * @remarks - * A document's UID is different from its ID. An ID is automatically generated - * for all documents and is made available on its `id` property. A UID is - * provided in the Prismic editor and is unique among all documents of its - * custom type. - * - * @example - * - * ```ts - * const document = await client.getByUID("blog_post", "my-first-post") - * ``` - * - * @typeParam TDocument - Type of the Prismic document returned. - * - * @param documentType - The API ID of the document's custom type. - * @param uid - UID of the document. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns The document with a UID matching the `uid` parameter, if a - * matching document exists. - */ - async getByUID< - TDocument extends TDocuments, - TDocumentType extends TDocument["type"] = TDocument["type"], - >( - documentType: TDocumentType, - uid: string, - params?: Partial & FetchParams, - ): Promise> { - return await this.getFirst>( - appendFilters(params, [ - typeFilter(documentType), - filter.at(`my.${documentType}.uid`, uid), - ]), - ) - } - - /** - * Queries document from the Prismic repository with specific UIDs and Custom - * Type. - * - * @remarks - * A document's UID is different from its ID. An ID is automatically generated - * for all documents and is made available on its `id` property. A UID is - * provided in the Prismic editor and is unique among all documents of its - * custom type. - * - * @example - * - * ```ts - * const document = await client.getByUIDs("blog_post", [ - * "my-first-post", - * "my-second-post", - * ]) - * ``` - * - * @typeParam TDocument - Type of the Prismic document returned. - * - * @param documentType - The API ID of the document's custom type. - * @param uids - A list of document UIDs. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A paginated response containing documents with UIDs matching the - * `uids` parameter. - */ - async getByUIDs< - TDocument extends TDocuments, - TDocumentType extends TDocument["type"] = TDocument["type"], - >( - documentType: TDocumentType, - uids: string[], - params?: Partial & FetchParams, - ): Promise>> { - return await this.get>( - appendFilters(params, [ - typeFilter(documentType), - filter.in(`my.${documentType}.uid`, uids), - ]), - ) - } - - /** - * Queries all documents from the Prismic repository with specific UIDs and - * custom type. - * - * This method may make multiple network requests to query all matching - * content. - * - * @remarks - * A document's UID is different from its ID. An ID is automatically generated - * for all documents and is made available on its `id` property. A UID is - * provided in the Prismic editor and is unique among all documents of its - * custom type. - * - * @example - * - * ```ts - * const response = await client.getAllByUIDs([ - * "my-first-post", - * "my-second-post", - * ]) - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param documentType - The API ID of the document's custom type. - * @param uids - A list of document UIDs. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A list of documents with UIDs matching the `uids` parameter. - */ - async getAllByUIDs< - TDocument extends TDocuments, - TDocumentType extends TDocument["type"] = TDocument["type"], - >( - documentType: TDocumentType, - uids: string[], - params?: Partial & GetAllParams & FetchParams, - ): Promise[]> { - return await this.dangerouslyGetAll< - ExtractDocumentType - >( - appendFilters(params, [ - typeFilter(documentType), - filter.in(`my.${documentType}.uid`, uids), - ]), - ) - } - - /** - * Queries a singleton document from the Prismic repository for a specific - * custom type. - * - * @remarks - * A singleton document is one that is configured in Prismic to only allow one - * instance. For example, a repository may be configured to contain just one - * Settings document. This is in contrast to a repeatable custom type which - * allows multiple instances of itself. - * - * @example - * - * ```ts - * const document = await client.getSingle("settings") - * ``` - * - * @typeParam TDocument - Type of the Prismic document returned. - * - * @param documentType - The API ID of the singleton custom type. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns The singleton document for the custom type, if a matching document - * exists. - */ - async getSingle< - TDocument extends TDocuments, - TDocumentType extends TDocument["type"] = TDocument["type"], - >( - documentType: TDocumentType, - params?: Partial & FetchParams, - ): Promise> { - return await this.getFirst>( - appendFilters(params, typeFilter(documentType)), - ) - } - - /** - * Queries documents from the Prismic repository for a specific custom type. - * - * Use `getAllByType` instead if you need to query all documents for a - * specific custom type. - * - * @example - * - * ```ts - * const response = await client.getByType("blog_post") - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param documentType - The API ID of the custom type. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A paginated response containing documents of the custom type. - */ - async getByType< - TDocument extends TDocuments, - TDocumentType extends TDocument["type"] = TDocument["type"], - >( - documentType: TDocumentType, - params?: Partial & FetchParams, - ): Promise>> { - return await this.get>( - appendFilters(params, typeFilter(documentType)), - ) - } - - /** - * Queries all documents from the Prismic repository for a specific Custom - * Type. - * - * This method may make multiple network requests to query all matching - * content. - * - * @example - * - * ```ts - * const response = await client.getByType("blog_post") - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param documentType - The API ID of the custom type. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A list of all documents of the custom type. - */ - async getAllByType< - TDocument extends TDocuments, - TDocumentType extends TDocument["type"] = TDocument["type"], - >( - documentType: TDocumentType, - params?: Partial> & - GetAllParams & - FetchParams, - ): Promise[]> { - return await this.dangerouslyGetAll< - ExtractDocumentType - >(appendFilters(params, typeFilter(documentType))) - } - - /** - * Queries documents from the Prismic repository with a specific tag. - * - * Use `getAllByTag` instead if you need to query all documents with a - * specific tag. - * - * @example - * - * ```ts - * const response = await client.getByTag("food") - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param tag - The tag that must be included on a document. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A paginated response containing documents with the tag. - */ - async getByTag( - tag: string, - params?: Partial & FetchParams, - ): Promise> { - return await this.get(appendFilters(params, someTagsFilter(tag))) - } - - /** - * Queries all documents from the Prismic repository with a specific tag. - * - * This method may make multiple network requests to query all matching - * content. - * - * @example - * - * ```ts - * const response = await client.getAllByTag("food") - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param tag - The tag that must be included on a document. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A list of all documents with the tag. - */ - async getAllByTag( - tag: string, - params?: Partial> & - GetAllParams & - FetchParams, - ): Promise { - return await this.dangerouslyGetAll( - appendFilters(params, someTagsFilter(tag)), - ) - } - - /** - * Queries documents from the Prismic repository with specific tags. A - * document must be tagged with all of the queried tags to be included. - * - * @example - * - * ```ts - * const response = await client.getByEveryTag(["food", "fruit"]) - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param tags - A list of tags that must be included on a document. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A paginated response containing documents with the tags. - */ - async getByEveryTag( - tags: string[], - params?: Partial & FetchParams, - ): Promise> { - return await this.get( - appendFilters(params, everyTagFilter(tags)), - ) - } - - /** - * Queries documents from the Prismic repository with specific tags. A - * document must be tagged with all of the queried tags to be included. - * - * This method may make multiple network requests to query all matching - * content. - * - * @example - * - * ```ts - * const response = await client.getAllByEveryTag(["food", "fruit"]) - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param tags - A list of tags that must be included on a document. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A list of all documents with the tags. - */ - async getAllByEveryTag( - tags: string[], - params?: Partial> & - GetAllParams & - FetchParams, - ): Promise { - return await this.dangerouslyGetAll( - appendFilters(params, everyTagFilter(tags)), - ) - } - - /** - * Queries documents from the Prismic repository with specific tags. A - * document must be tagged with at least one of the queried tags to be - * included. - * - * @example - * - * ```ts - * const response = await client.getByEveryTag(["food", "fruit"]) - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param tags - A list of tags that must be included on a document. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A paginated response containing documents with at least one of the - * tags. - */ - async getBySomeTags( - tags: string[], - params?: Partial & FetchParams, - ): Promise> { - return await this.get( - appendFilters(params, someTagsFilter(tags)), - ) - } - - /** - * Queries documents from the Prismic repository with specific tags. A - * document must be tagged with at least one of the queried tags to be - * included. - * - * This method may make multiple network requests to query all matching - * content. - * - * @example - * - * ```ts - * const response = await client.getAllBySomeTags(["food", "fruit"]) - * ``` - * - * @typeParam TDocument - Type of Prismic documents returned. - * - * @param tags - A list of tags that must be included on a document. - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A list of all documents with at least one of the tags. - */ - async getAllBySomeTags( - tags: string[], - params?: Partial> & - GetAllParams & - FetchParams, - ): Promise { - return await this.dangerouslyGetAll( - appendFilters(params, someTagsFilter(tags)), - ) - } - - /** - * Returns metadata about the Prismic repository, such as its refs, releases, - * and custom types. - * - * @returns Repository metadata. - */ - async getRepository(params?: FetchParams): Promise { - // TODO: Restore when Authorization header support works in browsers with CORS. - // return await this.fetch(this.endpoint); - - const url = new URL(this.endpoint) - - if (this.accessToken) { - url.searchParams.set("access_token", this.accessToken) - } - - return await this.fetch(url.toString(), params) - } - - /** - * Returns a list of all refs for the Prismic repository. - * - * Refs are used to identify which version of the repository's content should - * be queried. All repositories will have at least one ref pointing to the - * latest published content called the "master ref". - * - * @returns A list of all refs for the Prismic repository. - */ - async getRefs(params?: FetchParams): Promise { - const repository = await this.getRepository(params) - - return repository.refs - } - - /** - * Returns a ref for the Prismic repository with a matching ID. - * - * @param id - ID of the ref. - * - * @returns The ref with a matching ID, if it exists. - */ - async getRefByID(id: string, params?: FetchParams): Promise { - const refs = await this.getRefs(params) - - return findRefByID(refs, id) - } - - /** - * Returns a ref for the Prismic repository with a matching label. - * - * @param label - Label of the ref. - * - * @returns The ref with a matching label, if it exists. - */ - async getRefByLabel(label: string, params?: FetchParams): Promise { - const refs = await this.getRefs(params) - - return findRefByLabel(refs, label) - } - - /** - * Returns the master ref for the Prismic repository. The master ref points to - * the repository's latest published content. - * - * @returns The repository's master ref. - */ - async getMasterRef(params?: FetchParams): Promise { - const refs = await this.getRefs(params) - - return findMasterRef(refs) - } - - /** - * Returns a list of all Releases for the Prismic repository. Releases are - * used to group content changes before publishing. - * - * @returns A list of all Releases for the Prismic repository. - */ - async getReleases(params?: FetchParams): Promise { - const refs = await this.getRefs(params) - - return refs.filter((ref) => !ref.isMasterRef) - } - - /** - * Returns a Release for the Prismic repository with a matching ID. - * - * @param id - ID of the Release. - * - * @returns The Release with a matching ID, if it exists. - */ - async getReleaseByID(id: string, params?: FetchParams): Promise { - const releases = await this.getReleases(params) - - return findRefByID(releases, id) - } - - /** - * Returns a Release for the Prismic repository with a matching label. - * - * @param label - Label of the ref. - * - * @returns The ref with a matching label, if it exists. - */ - async getReleaseByLabel(label: string, params?: FetchParams): Promise { - const releases = await this.getReleases(params) - - return findRefByLabel(releases, label) - } - - /** - * Returns a list of all tags used in the Prismic repository. - * - * @returns A list of all tags used in the repository. - */ - async getTags(params?: FetchParams): Promise { - try { - const tagsForm = await this.getCachedRepositoryForm("tags", params) - - const url = new URL(tagsForm.action) - - if (this.accessToken) { - url.searchParams.set("access_token", this.accessToken) - } - - return await this.fetch(url.toString(), params) - } catch { - const repository = await this.getRepository(params) - - return repository.tags - } - } - - /** - * Builds a URL used to query content from the Prismic repository. - * - * @param params - Parameters to filter, sort, and paginate the results. - * - * @returns A URL string that can be requested to query content. - */ - async buildQueryURL({ - signal, - fetchOptions, - ...params - }: Partial & FetchParams = {}): Promise { - const ref = - params.ref || (await this.getResolvedRefString({ signal, fetchOptions })) - const integrationFieldsRef = - params.integrationFieldsRef || - (await this.getCachedRepository({ signal, fetchOptions })) - .integrationFieldsRef || - undefined - - return buildQueryURL(this.endpoint, { - ...this.defaultParams, - ...params, - ref, - integrationFieldsRef, - routes: params.routes || this.routes, - brokenRoute: params.brokenRoute || this.brokenRoute, - accessToken: params.accessToken || this.accessToken, - }) - } - - /** - * Determines the URL for a previewed document during an active preview - * session. The result of this method should be used to redirect the user to - * the document's URL. - * - * @example - * - * ```ts - * const url = client.resolvePreviewURL({ - * linkResolver: (document) => `/${document.uid}` - * defaultURL: '/' - * }) - * ``` - * - * @param args - Arguments to configure the URL resolving. - * - * @returns The URL for the previewed document during an active preview - * session. The user should be redirected to this URL. - */ - async resolvePreviewURL( - args: ResolvePreviewArgs & FetchParams, - ): Promise { - let documentID: string | undefined | null = args.documentID - let previewToken: string | undefined | null = args.previewToken - - if (typeof globalThis.location !== "undefined") { - const searchParams = new URLSearchParams(globalThis.location.search) - - documentID = documentID || searchParams.get("documentId") - previewToken = previewToken || searchParams.get("token") - } else if (this.refState.httpRequest) { - if ("query" in this.refState.httpRequest) { - documentID = - documentID || (this.refState.httpRequest.query?.documentId as string) - previewToken = - previewToken || (this.refState.httpRequest.query?.token as string) - } else if ( - "url" in this.refState.httpRequest && - this.refState.httpRequest.url - ) { - // Including "missing-host://" by default - // handles a case where Next.js Route Handlers - // only provide the pathname and search - // parameters in the `url` property - // (e.g. `/api/preview?foo=bar`). - const searchParams = new URL( - this.refState.httpRequest.url, - "missing-host://", - ).searchParams - - documentID = documentID || searchParams.get("documentId") - previewToken = previewToken || searchParams.get("token") - } - } - - if (documentID != null && previewToken != null) { - const document = await this.getByID(documentID, { - ref: previewToken, - lang: "*", - signal: args.signal, - fetchOptions: args.fetchOptions, - }) - - const url = asLink(document, { linkResolver: args.linkResolver }) - - if (typeof url === "string") { - return url - } - } - - return args.defaultURL - } - - /** - * Configures the client to query the latest published content for all future - * queries. - * - * If the `ref` parameter is provided during a query, it takes priority for - * that query. - * - * @example - * - * ```ts - * await client.queryLatestContent() - * const document = await client.getByID("WW4bKScAAMAqmluX") - * ``` - */ - queryLatestContent(): void { - this.refState.mode = RefStateMode.Master - } - - /** - * Configures the client to query content from a specific Release identified - * by its ID for all future queries. - * - * If the `ref` parameter is provided during a query, it takes priority for - * that query. - * - * @example - * - * ```ts - * await client.queryContentFromReleaseByID("YLB7OBAAACMA7Cpa") - * const document = await client.getByID("WW4bKScAAMAqmluX") - * ``` - * - * @param releaseID - The ID of the Release. - */ - queryContentFromReleaseByID(releaseID: string): void { - this.refState = { - ...this.refState, - mode: RefStateMode.ReleaseID, - releaseID, - } - } - - /** - * Configures the client to query content from a specific Release identified - * by its label for all future queries. - * - * If the `ref` parameter is provided during a query, it takes priority for - * that query. - * - * @example - * - * ```ts - * await client.queryContentFromReleaseByLabel("My Release") - * const document = await client.getByID("WW4bKScAAMAqmluX") - * ``` - * - * @param releaseLabel - The label of the Release. - */ - queryContentFromReleaseByLabel(releaseLabel: string): void { - this.refState = { - ...this.refState, - mode: RefStateMode.ReleaseLabel, - releaseLabel, - } - } - - /** - * Configures the client to query content from a specific ref. The ref can be - * provided as a string or a function. - * - * If a function is provided, the ref is fetched lazily before each query. The - * function may also be asynchronous. - * - * @example - * - * ```ts - * await client.queryContentFromRef("my-ref") - * const document = await client.getByID("WW4bKScAAMAqmluX") - * ``` - * - * @param ref - The ref or a function that returns the ref from which to query - * content. - */ - queryContentFromRef(ref: RefStringOrThunk): void { - this.refState = { - ...this.refState, - mode: RefStateMode.Manual, - ref, - } - } - - /** - * A `fetch()` function to be used with GraphQL clients configured for - * Prismic's GraphQL API. It automatically applies the necessary `prismic-ref` - * and Authorization headers. Queries will automatically be minified by - * removing whitespace where possible. - * - * @example - * - * ```ts - * const graphQLClient = new ApolloClient({ - * link: new HttpLink({ - * uri: prismic.getGraphQLEndpoint(repositoryName), - * // Provide `client.graphQLFetch` as the fetch implementation. - * fetch: client.graphQLFetch, - * // Using GET is required. - * useGETForQueries: true, - * }), - * cache: new InMemoryCache(), - * }) - * ``` - * - * @param input - The `fetch()` `input` parameter. Only strings are supported. - * @param init - The `fetch()` `init` parameter. Only plain objects are - * supported. - * - * @returns The `fetch()` Response for the request. - * - * @experimental - */ - async graphQLFetch( - input: RequestInfo, - init?: Omit & { signal?: AbortSignalLike }, - ): Promise { - const cachedRepository = await this.getCachedRepository() - const ref = await this.getResolvedRefString() - - const unsanitizedHeaders: Record = { - "Prismic-ref": ref, - Authorization: this.accessToken ? `Token ${this.accessToken}` : "", - // Asserting `init.headers` is a Record since popular GraphQL - // libraries pass this as a Record. Header objects as input - // are unsupported. - ...(init ? (init.headers as Record) : {}), - } - - if (cachedRepository.integrationFieldsRef) { - unsanitizedHeaders["Prismic-integration-field-ref"] = - cachedRepository.integrationFieldsRef - } - - // Normalize header keys to lowercase. This prevents header - // conflicts between the Prismic client and the GraphQL - // client. - const headers: Record = {} - for (const key in unsanitizedHeaders) { - if (unsanitizedHeaders[key]) { - headers[key.toLowerCase()] = - unsanitizedHeaders[key as keyof typeof unsanitizedHeaders] - } - } - - const url = new URL( - // Asserting `input` is a string since popular GraphQL - // libraries pass this as a string. Request objects as - // input are unsupported. - input as string, - ) - - // This prevents the request from being cached unnecessarily. - // Without adding this `ref` param, re-running a query - // could return a locally cached response, even if the - // `ref` changed. This happens because the URL is - // identical when the `ref` is not included. Caches may ignore - // headers. - // - // The Prismic GraphQL API ignores the `ref` param. - url.searchParams.set("ref", ref) - - const query = url.searchParams.get("query") - if (query) { - url.searchParams.set( - "query", - // Compress the GraphQL query (if it exists) by - // removing whitespace. This is done to - // optimize the query size and avoid - // hitting the upper limit of GET requests - // (2048 characters). - minifyGraphQLQuery(query), - ) - } - - return (await this.fetchFn(url.toString(), { - ...init, - headers, - })) as Response - } - - /** - * Returns a cached version of `getRepository` with a TTL. - * - * @returns Cached repository metadata. - */ - private async getCachedRepository(params?: FetchParams): Promise { - if ( - !this.cachedRepository || - Date.now() >= this.cachedRepositoryExpiration - ) { - this.cachedRepositoryExpiration = Date.now() + REPOSITORY_CACHE_TTL - this.cachedRepository = await this.getRepository(params) - } - - return this.cachedRepository - } - - /** - * Returns a cached Prismic repository form. Forms are used to determine API - * endpoints for types of repository data. - * - * @param name - Name of the form. - * - * @returns The repository form. - * - * @throws If a matching form cannot be found. - */ - private async getCachedRepositoryForm( - name: string, - params?: FetchParams, - ): Promise { - const cachedRepository = await this.getCachedRepository(params) - const form = cachedRepository.forms[name] - - if (!form) { - throw new PrismicError( - `Form with name "${name}" could not be found`, - undefined, - undefined, - ) - } - - return form - } - - /** - * Returns the ref needed to query based on the client's current state. This - * method may make a network request to fetch a ref or resolve the user's ref - * thunk. - * - * If auto previews are enabled, the preview ref takes priority if available. - * - * The following strategies are used depending on the client's state: - * - * - If the user called `queryLatestContent`: Use the repository's master ref. - * The ref is cached for 5 seconds. After 5 seconds, a new master ref is - * fetched. - * - If the user called `queryContentFromReleaseByID`: Use the release's ref. - * The ref is cached for 5 seconds. After 5 seconds, a new ref for the - * release is fetched. - * - If the user called `queryContentFromReleaseByLabel`: Use the release's ref. - * The ref is cached for 5 seconds. After 5 seconds, a new ref for the - * release is fetched. - * - If the user called `queryContentFromRef`: Use the provided ref. Fall back - * to the master ref if the ref is not a string. - * - * @returns The ref to use during a query. - */ - private async getResolvedRefString(params?: FetchParams): Promise { - if (this.refState.autoPreviewsEnabled) { - let previewRef: string | undefined - - let cookieJar: string | null | undefined - - if (this.refState.httpRequest?.headers) { - if ( - "get" in this.refState.httpRequest.headers && - typeof this.refState.httpRequest.headers.get === "function" - ) { - // Web API Headers - cookieJar = this.refState.httpRequest.headers.get("cookie") - } else if ("cookie" in this.refState.httpRequest.headers) { - // Express-style headers - cookieJar = this.refState.httpRequest.headers.cookie - } - } else if (globalThis.document?.cookie) { - cookieJar = globalThis.document.cookie - } - - if (cookieJar) { - previewRef = getPreviewCookie(cookieJar) - } - - if (previewRef) { - return previewRef - } - } - - const cachedRepository = await this.getCachedRepository(params) - - const refModeType = this.refState.mode - if (refModeType === RefStateMode.ReleaseID) { - return findRefByID(cachedRepository.refs, this.refState.releaseID).ref - } else if (refModeType === RefStateMode.ReleaseLabel) { - return findRefByLabel(cachedRepository.refs, this.refState.releaseLabel) - .ref - } else if (refModeType === RefStateMode.Manual) { - const res = await castThunk(this.refState.ref)() - - if (typeof res === "string") { - return res - } - } - - return findMasterRef(cachedRepository.refs).ref - } - - /** - * Performs a network request using the configured `fetch` function. It - * assumes all successful responses will have a JSON content type. It also - * normalizes unsuccessful network requests. - * - * @typeParam T - The JSON response. - * - * @param url - URL to the resource to fetch. - * @param params - Prismic REST API parameters for the network request. - * - * @returns The JSON response from the network request. - */ - private async fetch( - url: string, - params: FetchParams = {}, - ): Promise { - const requestInit: RequestInitLike = { - ...this.fetchOptions, - ...params.fetchOptions, - headers: { - ...this.fetchOptions?.headers, - ...params.fetchOptions?.headers, - }, - signal: - params.fetchOptions?.signal || - params.signal || - this.fetchOptions?.signal, - } - - let job: Promise - - // `fetchJobs` is keyed twice: first by the URL and again by is - // signal, if one exists. - // - // Using two keys allows us to reuse fetch requests for - // equivalent URLs, but eject when we detect unique signals. - if (this.fetchJobs[url] && this.fetchJobs[url].has(requestInit.signal)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - job = this.fetchJobs[url].get(requestInit.signal)! - } else { - this.fetchJobs[url] = this.fetchJobs[url] || new Map() - - job = this.fetchFn(url, requestInit) - .then(async (res) => { - // We can assume Prismic REST API responses - // will have a `application/json` - // Content Type. If not, this will - // throw, signaling an invalid - // response. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let json: any = undefined - try { - json = await res.json() - } catch { - // noop - } - - return { - status: res.status, - headers: res.headers, - json, - } - }) - .finally(() => { - this.fetchJobs[url].delete(requestInit.signal) - - if (this.fetchJobs[url].size === 0) { - delete this.fetchJobs[url] - } - }) - - this.fetchJobs[url].set(requestInit.signal, job) - } - - const res = await job - - if (res.status !== 404 && res.json == null) { - throw new PrismicError(undefined, url, res.json) - } - - switch (res.status) { - // Successful - case 200: { - return res.json - } - - // Bad Request - // - Invalid filter syntax - // - Ref not provided (ignored) - case 400: { - throw new ParsingError(res.json.message, url, res.json) - } - - // Unauthorized - // - Missing access token for repository endpoint - // - Incorrect access token for repository endpoint - case 401: - // Forbidden - // - Missing access token for query endpoint - // - Incorrect access token for query endpoint - case 403: { - throw new ForbiddenError( - res.json.error || res.json.message, - url, - res.json, - ) - } - - // Not Found - // - Incorrect repository name (this response has an empty body) - // - Ref does not exist - // - Preview token is expired - case 404: { - if (res.json === undefined) { - throw new RepositoryNotFoundError( - `Prismic repository not found. Check that "${this.endpoint}" is pointing to the correct repository.`, - url, - undefined, - ) - } - - if (res.json.type === "api_notfound_error") { - throw new RefNotFoundError(res.json.message, url, res.json) - } - - if ( - res.json.type === "api_security_error" && - /preview token.*expired/i.test(res.json.message) - ) { - throw new PreviewTokenExpiredError(res.json.message, url, res.json) - } - - throw new NotFoundError(res.json.message, url, res.json) - } - - // Gone - // - Ref is expired - case 410: { - throw new RefExpiredError(res.json.message, url, res.json) - } - - // Too Many Requests - // - Exceeded the maximum number of requests per second - case 429: { - const parsedRetryAfter = Number(res.headers.get("retry-after")) - const delay = Number.isNaN(parsedRetryAfter) - ? DEFUALT_RETRY_AFTER_MS - : parsedRetryAfter - - return await new Promise((resolve, reject) => { - setTimeout(async () => { - try { - resolve(await this.fetch(url, params)) - } catch (error) { - reject(error) - } - }, delay) - }) - } - } - - throw new PrismicError(undefined, url, res.json) - } -} diff --git a/src/createWriteClient.ts b/src/createWriteClient.ts new file mode 100644 index 00000000..b2bee037 --- /dev/null +++ b/src/createWriteClient.ts @@ -0,0 +1,41 @@ +import type { PrismicDocument } from "./types/value/document" + +import type { WriteClientConfig } from "./WriteClient" +import { WriteClient } from "./WriteClient" + +/** + * Type definitions for the `createWriteClient()` function. May be augmented by + * third-party libraries. + */ +export interface CreateWriteClient { + ( + ...args: ConstructorParameters + ): WriteClient +} + +/** + * Creates a Prismic client that can be used to query and write content to a + * repository. + * + * @example + * + * ```ts + * // With a repository name. + * createWriteClient("qwerty", { writeToken: "***" }) + * ``` + * + * @typeParam TDocuments - A map of Prismic document type IDs mapped to their + * TypeScript type. + * + * @param repositoryName - The Prismic repository name for the repository. + * @param options - Configuration that determines how content will be queried + * from and written to the Prismic repository. + * + * @returns A client that can query and write content to the repository. + */ +export const createWriteClient: CreateWriteClient = < + TDocuments extends PrismicDocument, +>( + repositoryName: string, + options: WriteClientConfig, +) => new WriteClient(repositoryName, options) diff --git a/src/index.ts b/src/index.ts index 157d3d8e..318aebc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,16 @@ import { filter } from "./filter" //============================================================================= // Primary Client API. -export { createClient, Client } from "./createClient" +export { createClient } from "./createClient" +export { Client } from "./Client" + +// Write Client API. +export { createWriteClient } from "./createWriteClient" +export { WriteClient } from "./WriteClient" + +// Migration helper. +export { createMigration } from "./createMigration" +export { Migration } from "./Migration" // API endpoint helpers. export { getRepositoryEndpoint } from "./getRepositoryEndpoint" @@ -41,21 +50,24 @@ export { filter, predicate } export * as cookie from "./cookie" // General types used to query content from Prismic. These are made public to allow users to better type their projects. +export type { CreateClient } from "./createClient" +export type { ClientConfig } from "./Client" +export type { CreateWriteClient } from "./createWriteClient" +export type { WriteClientConfig } from "./WriteClient" export type { AbortSignalLike, - ClientConfig, - CreateClient, FetchLike, HttpRequestLike, RequestInitLike, ResponseLike, -} from "./createClient" +} from "./BaseClient" export type { BuildQueryURLArgs, Ordering, QueryParams, Route, } from "./buildQueryURL" +export type { CreateMigration } from "./createMigration" //============================================================================= // Helpers - Manipulate content from Prismic. diff --git a/src/lib/pLimit.ts b/src/lib/pLimit.ts new file mode 100644 index 00000000..4a0539c0 --- /dev/null +++ b/src/lib/pLimit.ts @@ -0,0 +1,112 @@ +/* + ** Core logic from https://github.com/sindresorhus/p-limit + ** Many thanks to @sindresorhus + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyFunction = (...arguments_: readonly any[]) => unknown + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +export type LimitFunction = { + /** + * The number of queued items waiting to be executed. + */ + readonly queueSize: number + + /** + * @param fn - Promise-returning/async function. + * @param arguments - Any arguments to pass through to `fn`. Support for + * passing arguments on to the `fn` is provided in order to be able to avoid + * creating unnecessary closures. You probably don't need this optimization + * unless you're pushing a lot of functions. + * + * @returns The promise returned by calling `fn(...arguments)`. + */ + ( + function_: ( + ...arguments_: Arguments + ) => PromiseLike | ReturnType, + ...arguments_: Arguments + ): Promise +} + +export const pLimit = ({ + limit, + interval, +}: { + limit: number + interval?: number +}): LimitFunction => { + const queue: AnyFunction[] = [] + let activeCount = 0 + + const resumeNext = () => { + if (activeCount < limit && queue.length > 0) { + queue.shift()?.() + // Since `pendingCount` has been decreased by one, increase `activeCount` by one. + activeCount++ + } + } + + const next = () => { + activeCount-- + + resumeNext() + } + + const run = async ( + function_: AnyFunction, + resolve: (value: unknown) => void, + arguments_: unknown[], + ) => { + if (interval) { + await sleep(interval) + } + const result = (async () => function_(...arguments_))() + + resolve(result) + + try { + await result + } catch {} + + next() + } + + const enqueue = ( + function_: AnyFunction, + resolve: (value: unknown) => void, + arguments_: unknown[], + ) => { + // Queue `internalResolve` instead of the `run` function + // to preserve asynchronous context. + new Promise((internalResolve) => { + queue.push(internalResolve) + }).then(run.bind(undefined, function_, resolve, arguments_)) + ;(async () => { + // This function needs to wait until the next microtask before comparing + // `activeCount` to `concurrency`, because `activeCount` is updated asynchronously + // after the `internalResolve` function is dequeued and called. The comparison in the if-statement + // needs to happen asynchronously as well to get an up-to-date value for `activeCount`. + await Promise.resolve() + + if (activeCount < limit) { + resumeNext() + } + })() + } + + const generator = (function_: AnyFunction, ...arguments_: unknown[]) => + new Promise((resolve) => { + enqueue(function_, resolve, arguments_) + }) + + Object.defineProperties(generator, { + queueSize: { + get: () => queue.length, + }, + }) + + return generator as LimitFunction +} From 3378dc24730e0a5235fb0bd1fc0c6468e9c7814b Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 23 Aug 2024 15:59:13 +0200 Subject: [PATCH 02/61] feat: `WriteClient` --- src/WriteClient.ts | 396 +++++++++++++++++++++++++++++++++++ src/types/api/asset/asset.ts | 103 +++++++++ src/types/api/asset/tag.ts | 22 ++ 3 files changed, 521 insertions(+) create mode 100644 src/WriteClient.ts create mode 100644 src/types/api/asset/asset.ts create mode 100644 src/types/api/asset/tag.ts diff --git a/src/WriteClient.ts b/src/WriteClient.ts new file mode 100644 index 00000000..2d938de8 --- /dev/null +++ b/src/WriteClient.ts @@ -0,0 +1,396 @@ +import { pLimit } from "./lib/pLimit" + +import type { + Asset, + BulkDeleteAssetsParams, + GetAssetsParams, + GetAssetsResult, + PatchAssetsParams, + PatchAssetsResult, + PostAssetsParams, + PostAssetsResult, +} from "./types/api/asset/asset" +import type { + GetTagsResult, + PostTagParams, + PostTagResult, + Tag, +} from "./types/api/asset/tag" +import type { PrismicDocument } from "./types/value/document" + +import type { FetchParams } from "./BaseClient" +import { Client } from "./Client" +import type { ClientConfig } from "./Client" + +type GetAssetsReturnType = { + results: Asset[] + total_results_size: number + missing_ids?: string[] + next?: () => Promise +} + +/** + * Configuration for clients that determine how content is queried. + */ +export type WriteClientConfig = { + writeToken: string + + migrationAPIKey: string + + assetAPIEndpoint?: string + + migrationAPIEndpoint?: string +} & ClientConfig + +/** + * A client that allows querying and writing content to a Prismic repository. + * + * If used in an environment where a global `fetch` function is unavailable, + * such as Node.js, the `fetch` option must be provided as part of the `options` + * parameter. + * + * @typeParam TDocuments - Document types that are registered for the Prismic + * repository. Query methods will automatically be typed based on this type. + */ +export class WriteClient< + TDocuments extends PrismicDocument = PrismicDocument, +> extends Client { + writeToken: string + migrationAPIKey: string + + assetAPIEndpoint = "https://asset-api.prismic.io/" + migrationAPIEndpoint = "https://migration.prismic.io/" + + /** + * Creates a Prismic client that can be used to query and write content to a + * repository. + * + * If used in an environment where a global `fetch` function is unavailable, + * such as in some Node.js versions, the `fetch` option must be provided as + * part of the `options` parameter. + * + * @param repositoryName - The Prismic repository name for the repository. + * @param options - Configuration that determines how content will be queried + * from and written to the Prismic repository. + * + * @returns A client that can query and write content to the repository. + */ + constructor(repositoryName: string, options: WriteClientConfig) { + super(repositoryName, options) + + this.writeToken = options.writeToken + this.migrationAPIKey = options.migrationAPIKey + + if (options.assetAPIEndpoint) { + this.assetAPIEndpoint = `${options.assetAPIEndpoint}/` + } + + if (options.migrationAPIEndpoint) { + this.migrationAPIEndpoint = `${options.migrationAPIEndpoint}/` + } + } + + async getAssets({ + pageSize, + cursor, + assetType, + keyword, + ids, + tags, + ...params + }: GetAssetsParams & FetchParams = {}): Promise { + // Resolve tags if any + if (tags && tags.length) { + tags = await this.resolveAssetTagIDs(tags, params) + } + + const url = this.buildWriteQueryURL( + new URL("assets", this.assetAPIEndpoint), + { + pageSize, + cursor, + assetType, + keyword, + ids, + tags, + }, + ) + + const { + items, + total, + missing_ids, + cursor: nextCursor, + } = await this.fetch( + url, + this.buildWriteQueryParams({ params }), + ) + + return { + results: items, + total_results_size: total, + missing_ids: missing_ids || [], + next: nextCursor + ? () => + this.getAssets({ + pageSize, + cursor: nextCursor, + assetType, + keyword, + ids, + tags, + ...params, + }) + : undefined, + } + } + + async createAsset( + file: PostAssetsParams["file"], + filename: string, + { + notes, + credits, + alt, + tags, + ...params + }: { + notes?: string + credits?: string + alt?: string + tags?: string[] + } & FetchParams = {}, + ): Promise { + const url = new URL("assets", this.assetAPIEndpoint) + + const formData = new FormData() + formData.append("file", new File([file], filename)) + + if (notes) { + formData.append("notes", notes) + } + + if (credits) { + formData.append("credits", credits) + } + + if (alt) { + formData.append("alt", alt) + } + + const asset = await this.fetch( + url.toString(), + this.buildWriteQueryParams({ + method: "POST", + body: formData, + params, + }), + ) + + if (tags && tags.length) { + return this.updateAsset(asset.id, { tags }) + } + + return asset + } + + async updateAsset( + id: string, + { + notes, + credits, + alt, + filename, + tags, + ...params + }: PatchAssetsParams & FetchParams = {}, + ): Promise { + const url = this.buildWriteQueryURL( + new URL(`assets/${id}`, this.assetAPIEndpoint), + ) + + // Resolve tags if any and create missing ones + if (tags && tags.length) { + tags = await this.resolveAssetTagIDs(tags, { + createTags: true, + ...params, + }) + } + + return this.fetch( + url, + this.buildWriteQueryParams({ + method: "PATCH", + body: { + notes, + credits, + alt, + filename, + tags, + }, + params, + }), + ) + } + + async deleteAsset( + assetOrID: string | Asset, + params?: FetchParams, + ): Promise { + const url = this.buildWriteQueryURL( + new URL( + `assets/${typeof assetOrID === "string" ? assetOrID : assetOrID.id}`, + this.assetAPIEndpoint, + ), + ) + + await this.fetch( + url, + this.buildWriteQueryParams({ method: "DELETE", params }), + ) + } + + async deleteAssets( + assetsOrIDs: (string | Asset)[], + params?: FetchParams, + ): Promise { + const url = this.buildWriteQueryURL( + new URL("assets/bulk-delete", this.assetAPIEndpoint), + ) + + await this.fetch( + url, + this.buildWriteQueryParams({ + method: "POST", + body: { + ids: assetsOrIDs.map((assetOrID) => + typeof assetOrID === "string" ? assetOrID : assetOrID.id, + ), + }, + params, + }), + ) + } + + private _resolveAssetTagIDsLimit = pLimit({ limit: 1 }) + private async resolveAssetTagIDs( + tagNamesOrIDs: string[] = [], + { createTags, ...params }: { createTags?: boolean } & FetchParams = {}, + ): Promise { + return this._resolveAssetTagIDsLimit(async () => { + const existingTags = await this.getAssetTags(params) + const existingTagMap: Record = {} + for (const tag of existingTags) { + existingTagMap[tag.name] = tag + } + + const resolvedTagIDs = [] + for (const tagNameOrID of tagNamesOrIDs) { + // Taken from @sinclair/typebox which is the uuid type checker of the asset API + // See: https://github.com/sinclairzx81/typebox/blob/e36f5658e3a56d8c32a711aa616ec8bb34ca14b4/test/runtime/compiler/validate.ts#L15 + // Tag is already a tag ID + if ( + /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test( + tagNameOrID, + ) + ) { + resolvedTagIDs.push(tagNameOrID) + continue + } + + // Tag does not exists yet, we create it if `createTags` is set + if (!existingTagMap[tagNameOrID] && createTags) { + existingTagMap[tagNameOrID] = await this.createAssetTag( + tagNameOrID, + params, + ) + } + + // Add tag if found + if (existingTagMap[tagNameOrID]) { + resolvedTagIDs.push(existingTagMap[tagNameOrID].id) + } + } + + return resolvedTagIDs + }) + } + + private async createAssetTag( + name: string, + params?: FetchParams, + ): Promise { + const url = this.buildWriteQueryURL(new URL("tags", this.assetAPIEndpoint)) + + return this.fetch( + url, + this.buildWriteQueryParams({ + method: "POST", + body: { name }, + params, + }), + ) + } + + private async getAssetTags(params?: FetchParams): Promise { + const url = this.buildWriteQueryURL(new URL("tags", this.assetAPIEndpoint)) + + const { items } = await this.fetch( + url, + this.buildWriteQueryParams({ params }), + ) + + return items + } + + private buildWriteQueryURL>( + url: URL, + searchParams?: TSearchParams, + ): string { + if (searchParams) { + Object.entries(searchParams).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((item) => url.searchParams.append(key, item.toString())) + } else if (value) { + url.searchParams.set(key, value.toString()) + } + }) + } + + return url.toString() + } + + private buildWriteQueryParams>({ + method, + body, + isMigrationAPI, + params, + }: { + method?: string + body?: TBody + isMigrationAPI?: boolean + params?: FetchParams + }): FetchParams { + return { + ...params, + fetchOptions: { + ...params?.fetchOptions, + method, + body: body + ? body instanceof FormData + ? body + : JSON.stringify(body) + : undefined, + headers: { + ...params?.fetchOptions?.headers, + authorization: `Bearer ${this.writeToken}`, + repository: this.repositoryName, + ...(body instanceof FormData + ? {} + : { "content-type": "application/json" }), + ...(isMigrationAPI ? { "x-api-key": this.migrationAPIKey } : {}), + }, + }, + } + } +} diff --git a/src/types/api/asset/asset.ts b/src/types/api/asset/asset.ts new file mode 100644 index 00000000..bfa3b2f2 --- /dev/null +++ b/src/types/api/asset/asset.ts @@ -0,0 +1,103 @@ +import type { Tag } from "./tag" + +/** + * Asset types + */ +export const AssetType = { + All: "all", + Audio: "audio", + Document: "document", + Image: "image", + Video: "video", +} as const + +export type Asset = { + id: string + url: string + + created_at: number + last_modified: number + + filename: string + extension: string + size: number + kind: Exclude< + (typeof AssetType)[keyof typeof AssetType], + (typeof AssetType)["All"] + > + + width?: number + height?: number + + notes?: string + credits?: string + alt?: string + + tags?: Tag[] + + /** + * @internal + */ + origin_url?: string + + /** + * @internal + */ + uploader_id?: string + + /** + * @internal + */ + search_highlight?: { + filename?: string[] + notes?: string[] + credits?: string[] + alt?: string[] + } +} + +export type GetAssetsParams = { + // Pagination + pageSize?: number + /** + * @internal + */ + cursor?: string + + // Filtering + assetType?: (typeof AssetType)[keyof typeof AssetType] + keyword?: string + ids?: string[] + tags?: string[] +} + +export type GetAssetsResult = { + items: Asset[] + total: number + cursor?: string + missing_ids?: string[] + is_opensearch_result: boolean +} + +export type PostAssetsParams = { + file: BlobPart + notes?: string + credits?: string + alt?: string +} + +export type PostAssetsResult = Asset + +export type PatchAssetsParams = { + notes?: string + credits?: string + alt?: string + filename?: string + tags?: string[] +} + +export type PatchAssetsResult = Asset + +export type BulkDeleteAssetsParams = { + ids: string[] +} diff --git a/src/types/api/asset/tag.ts b/src/types/api/asset/tag.ts new file mode 100644 index 00000000..80220e14 --- /dev/null +++ b/src/types/api/asset/tag.ts @@ -0,0 +1,22 @@ +export type Tag = { + id: string + name: string + + created_at: number + last_modified: number + + /** + * @internal + */ + uploader_id?: string + + count?: number +} + +export type GetTagsResult = { items: Tag[] } + +export type PostTagParams = { + name: string +} + +export type PostTagResult = Tag From 956d6f68c49a786a0d4ce79099fb10a676494e8c Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 23 Aug 2024 15:59:33 +0200 Subject: [PATCH 03/61] feat: `Migration` --- src/Migration.ts | 135 ++++++++++++++++++++++++++ src/createMigration.ts | 32 ++++++ src/types/migration/asset.ts | 9 ++ src/types/migration/document.ts | 166 ++++++++++++++++++++++++++++++++ src/types/migration/fields.ts | 30 ++++++ src/types/migration/richText.ts | 11 +++ 6 files changed, 383 insertions(+) create mode 100644 src/Migration.ts create mode 100644 src/createMigration.ts create mode 100644 src/types/migration/asset.ts create mode 100644 src/types/migration/document.ts create mode 100644 src/types/migration/fields.ts create mode 100644 src/types/migration/richText.ts diff --git a/src/Migration.ts b/src/Migration.ts new file mode 100644 index 00000000..d95e574c --- /dev/null +++ b/src/Migration.ts @@ -0,0 +1,135 @@ +import type { Asset } from "./types/api/asset/asset" +import type { MigrationAsset } from "./types/migration/asset" +import type { MigrationPrismicDocument } from "./types/migration/document" +import { + MigrationFieldType, + type MigrationImageField, + type MigrationLinkToMediaField, +} from "./types/migration/fields" +import type { PrismicDocument } from "./types/value/document" + +type CreateAssetReturnType = MigrationImageField & { + image: MigrationImageField + linkToMedia: MigrationLinkToMediaField +} + +const SINGLE_KEY = "__SINGLE__" + +/** + * 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< + TDocuments extends PrismicDocument = PrismicDocument, + TMigrationDocuments extends + MigrationPrismicDocument = MigrationPrismicDocument, +> { + #documents: TMigrationDocuments[] = [] + #documentsByUID: Record> = {} + + #assets: Map = new Map() + + constructor() {} + + createAsset(asset: Asset): CreateAssetReturnType + createAsset( + file: MigrationAsset["file"], + filename: MigrationAsset["filename"], + options?: { + notes?: string + credits?: string + alt?: string + tags?: string[] + }, + ): CreateAssetReturnType + createAsset( + fileOrAsset: MigrationAsset["file"] | Asset, + filename?: MigrationAsset["filename"], + { + notes, + credits, + alt, + tags, + }: { + notes?: string + credits?: string + alt?: string + tags?: string[] + } = {}, + ): CreateAssetReturnType { + let asset: MigrationAsset + if (typeof fileOrAsset === "object" && "url" in fileOrAsset) { + asset = { + id: fileOrAsset.id, + file: fileOrAsset.url, + filename: fileOrAsset.filename, + notes: fileOrAsset.notes, + credits: fileOrAsset.credits, + alt: fileOrAsset.alt, + tags: fileOrAsset.tags?.map(({ name }) => name), + } + } else { + asset = { + id: fileOrAsset, + file: fileOrAsset, + filename: filename!, + notes, + credits, + alt, + tags, + } + } + + this.#assets.set(asset.id, asset) + + return { + migrationType: MigrationFieldType.Image, + ...asset, + image: { + migrationType: MigrationFieldType.Image, + ...asset, + }, + linkToMedia: { + migrationType: MigrationFieldType.LinkToMedia, + ...asset, + }, + } + } + + createDocument(document: TMigrationDocuments): TMigrationDocuments { + this.#documents.push(document) + + if (!(document.type in this.#documentsByUID)) { + this.#documentsByUID[document.type] = {} + } + this.#documentsByUID[document.type][document.uid || SINGLE_KEY] = document + + return document + } + + getByUID< + TType extends TMigrationDocuments["type"], + TMigrationDocument extends Extract< + TMigrationDocuments, + { type: TType } + > = Extract, + >(documentType: TType, uid: string): TMigrationDocument | undefined { + return this.#documentsByUID[documentType]?.[uid] as + | TMigrationDocument + | undefined + } + + getSingle< + TType extends TMigrationDocuments["type"], + TMigrationDocument extends Extract< + TMigrationDocuments, + { type: TType } + > = Extract, + >(documentType: TType): TMigrationDocument | undefined | undefined { + return this.#documentsByUID[documentType]?.[SINGLE_KEY] as + | TMigrationDocument + | undefined + } +} diff --git a/src/createMigration.ts b/src/createMigration.ts new file mode 100644 index 00000000..3fc63125 --- /dev/null +++ b/src/createMigration.ts @@ -0,0 +1,32 @@ +import type { PrismicDocument } from "./types/value/document" + +import { Migration } from "./Migration" + +/** + * Type definitions for the `createMigration()` function. May be augmented by + * third-party libraries. + */ +export interface CreateMigration { + ( + ...args: ConstructorParameters + ): Migration +} + +/** + * Creates a Prismic migration instance that can be used to prepare your + * migration to Prismic. + * + * @example + * + * ```ts + * createMigration() + * ``` + * + * @typeParam TDocuments - A map of Prismic document type IDs mapped to their + * TypeScript type. + * + * @returns A migration instance to prepare your migration. + */ +export const createMigration: CreateMigration = < + TDocuments extends PrismicDocument, +>() => new Migration() diff --git a/src/types/migration/asset.ts b/src/types/migration/asset.ts new file mode 100644 index 00000000..93a5b535 --- /dev/null +++ b/src/types/migration/asset.ts @@ -0,0 +1,9 @@ +export type MigrationAsset = { + id: string | URL | File | NonNullable[0]>[0] + file: string | URL | File | NonNullable[0]>[0] + filename: string + notes?: string + credits?: string + alt?: string + tags?: string[] +} diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts new file mode 100644 index 00000000..d98619cf --- /dev/null +++ b/src/types/migration/document.ts @@ -0,0 +1,166 @@ +import type { AnyRegularField } from "../value/types" + +import type { PrismicDocument } from "../value/document" +import type { GroupField } from "../value/group" +import type { ImageField } from "../value/image" +import type { LinkField } from "../value/link" +import type { + RTBlockNode, + RTImageNode, + RTInlineNode, + RTTextNode, + RichTextField, +} from "../value/richText" +import type { SharedSlice } from "../value/sharedSlice" +import type { Slice } from "../value/slice" +import type { SliceZone } from "../value/sliceZone" + +import type { MigrationImageField, MigrationLinkField } from "./fields" +import type { MigrationRTImageNode, MigrationRTLinkNode } from "./richText" + +type RichTextTextNodeToMigrationField = Omit< + TRTNode, + "spans" +> & { + spans: (RTInlineNode | MigrationRTLinkNode)[] +} + +type RichTextBlockNodeToMigrationField = + TRTNode extends RTImageNode + ? + | MigrationRTImageNode + | (Omit & { + linkTo?: RTImageNode["linkTo"] | MigrationLinkField + }) + : TRTNode extends RTTextNode + ? RichTextTextNodeToMigrationField + : TRTNode + +export type RichTextFieldToMigrationField = { + [Index in keyof TField]: RichTextBlockNodeToMigrationField +} + +type RegularFieldToMigrationField = + | (TField extends ImageField + ? MigrationImageField + : TField extends LinkField + ? MigrationLinkField + : TField extends RichTextField + ? RichTextFieldToMigrationField + : never) + | TField + +type GroupFieldToMigrationField< + TField extends GroupField | Slice["items"] | SharedSlice["items"], +> = { + [Index in keyof TField]: FieldsToMigrationFields +} + +type SliceToMigrationField = Omit< + TField, + "primary" | "items" +> & { + primary: FieldsToMigrationFields + items: GroupFieldToMigrationField +} + +type SliceZoneToMigrationField = { + [Index in keyof TField]: SliceToMigrationField +} + +type FieldToMigrationField< + TField extends AnyRegularField | GroupField | SliceZone, +> = TField extends AnyRegularField + ? RegularFieldToMigrationField + : TField extends GroupField + ? GroupFieldToMigrationField + : TField extends SliceZone + ? SliceZoneToMigrationField + : never + +type FieldsToMigrationFields< + TFields extends Record, +> = { + [Key in keyof TFields]: FieldToMigrationField +} + +/** + * Makes the UID of `MigrationPrismicDocument` optional on custom types without + * UID. TypeScript fails to infer correct types if done with a type + * intersection. + * + * @internal + */ +type MakeUIDOptional = + TMigrationDocument["uid"] extends string + ? TMigrationDocument + : Omit & { uid?: TMigrationDocument["uid"] } + +/** + * A Prismic document compatible with the migration API. + * + * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} + */ +export type MigrationPrismicDocument< + TDocument extends PrismicDocument = PrismicDocument, +> = + TDocument extends PrismicDocument + ? MakeUIDOptional<{ + /** + * Title of the document displayed in the editor. + */ + title: string + + /** + * A link to the master language document. + */ + // We're forced to inline `MigrationContentRelationshipField` here, otherwise + // it creates a circular reference to itself which makes TypeScript unhappy. + // (but I think it's weird and it doesn't make sense :thinking:) + masterLanguageDocument?: + | PrismicDocument + | MigrationPrismicDocument + | (() => + | Promise + | PrismicDocument + | MigrationPrismicDocument + | undefined) + + /** + * Type of the document. + */ + type: TType + + /** + * The unique identifier for the document. Guaranteed to be unique among + * all Prismic documents of the same type. + */ + uid: TDocument["uid"] + + /** + * Tags associated with document. + */ + // Made optional compared to the original type. + tags?: TDocument["tags"] + + /** + * Language of document. + */ + lang: TLang + + /** + * Alternate language documents from Prismic content API. Used as a + * substitute to the `masterLanguageDocument` property when the latter + * is not available. + * + * @internal + */ + // Made optional compared to the original type. + alternate_languages?: TDocument["alternate_languages"] + + /** + * Data contained in the document. + */ + data: FieldsToMigrationFields + }> + : never diff --git a/src/types/migration/fields.ts b/src/types/migration/fields.ts new file mode 100644 index 00000000..288a4fae --- /dev/null +++ b/src/types/migration/fields.ts @@ -0,0 +1,30 @@ +import type { PrismicDocument } from "../value/document" + +import type { MigrationAsset } from "./asset" +import type { MigrationPrismicDocument } from "./document" + +export const MigrationFieldType = { + Image: "image", + LinkToMedia: "linkToMedia", +} as const + +export type MigrationImageField = MigrationAsset & { + migrationType: typeof MigrationFieldType.Image +} + +export type MigrationLinkToMediaField = MigrationAsset & { + migrationType: typeof MigrationFieldType.LinkToMedia +} + +export type MigrationContentRelationshipField = + | PrismicDocument + | MigrationPrismicDocument + | (() => + | Promise + | PrismicDocument + | MigrationPrismicDocument + | undefined) + +export type MigrationLinkField = + | MigrationLinkToMediaField + | MigrationContentRelationshipField diff --git a/src/types/migration/richText.ts b/src/types/migration/richText.ts new file mode 100644 index 00000000..2f8935e1 --- /dev/null +++ b/src/types/migration/richText.ts @@ -0,0 +1,11 @@ +import type { RTImageNode, RTLinkNode } from "../value/richText" + +import type { MigrationImageField, MigrationLinkField } from "./fields" + +export type MigrationRTLinkNode = Omit & { + data: MigrationLinkField +} + +export type MigrationRTImageNode = MigrationImageField & { + linkTo?: RTImageNode["linkTo"] | MigrationLinkField +} From ba2c7333f3b618c3eece5723150936993aee0c7e Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 23 Aug 2024 17:29:39 +0200 Subject: [PATCH 04/61] test: type test migration types --- src/Migration.ts | 21 ++- src/index.ts | 20 +++ src/types/migration/document.ts | 2 +- test/types/migration-document.types.ts | 234 +++++++++++++++++++++++++ test/types/migration.types.ts | 105 +++++++++++ 5 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 test/types/migration-document.types.ts create mode 100644 test/types/migration.types.ts diff --git a/src/Migration.ts b/src/Migration.ts index d95e574c..4092fdc2 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -8,6 +8,23 @@ import { } from "./types/migration/fields" import type { PrismicDocument } from "./types/value/document" +/** + * 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 TMigrationDocuments - Prismic migration document types from which + * to extract. + * @typeParam TType - Type(s) to match `TMigrationDocuments` against. + */ +type ExtractMigrationDocumentType< + TMigrationDocuments extends MigrationPrismicDocument, + TType extends TMigrationDocuments["type"], +> = + Extract extends never + ? TMigrationDocuments + : Extract + type CreateAssetReturnType = MigrationImageField & { image: MigrationImageField linkToMedia: MigrationLinkToMediaField @@ -98,7 +115,9 @@ export class Migration< } } - createDocument(document: TMigrationDocuments): TMigrationDocuments { + createDocument( + document: ExtractMigrationDocumentType, + ): ExtractMigrationDocumentType { this.#documents.push(document) if (!(document.type in this.#documentsByUID)) { diff --git a/src/index.ts b/src/index.ts index 318aebc6..e728600d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -325,6 +325,26 @@ export type { CustomTypeModelFieldForSlicePrimary, } from "./types/model/types" +// Migrations - Types representing Prismic migration API content values. +export { MigrationFieldType } from "./types/migration/fields" + +export type { + MigrationPrismicDocument, + RichTextFieldToMigrationField, +} from "./types/migration/document" + +export type { + MigrationImageField, + MigrationLinkToMediaField, + MigrationContentRelationshipField, + MigrationLinkField, +} from "./types/migration/fields" + +export type { + MigrationRTImageNode, + MigrationRTLinkNode, +} from "./types/migration/richText" + // API - Types representing Prismic Rest API V2 responses. export type { Query } from "./types/api/query" diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts index d98619cf..43af8974 100644 --- a/src/types/migration/document.ts +++ b/src/types/migration/document.ts @@ -94,7 +94,7 @@ type FieldsToMigrationFields< type MakeUIDOptional = TMigrationDocument["uid"] extends string ? TMigrationDocument - : Omit & { uid?: TMigrationDocument["uid"] } + : Omit & Partial> /** * A Prismic document compatible with the migration API. diff --git a/test/types/migration-document.types.ts b/test/types/migration-document.types.ts new file mode 100644 index 00000000..a745ced2 --- /dev/null +++ b/test/types/migration-document.types.ts @@ -0,0 +1,234 @@ +import type { TypeOf } from "ts-expect" +import { expectNever, expectType } from "ts-expect" + +import type * as prismic from "../../src" + +;(value: prismic.MigrationPrismicDocument): true => { + switch (typeof value) { + case "object": { + if (value === null) { + expectNever(value) + } + + return true + } + + default: { + return expectNever(value) + } + } +} + +expectType({ + title: "", + uid: "", + type: "", + lang: "", + data: {}, +}) + +/** + * Supports any field when generic. + */ +expectType({ + title: "", + uid: "", + type: "", + lang: "", + data: { + any: "", + }, +}) + +/** + * `PrismicDocument` is assignable to `MigrationPrismicDocument` with added + * `title`. + */ +expectType< + TypeOf< + prismic.MigrationPrismicDocument, + prismic.PrismicDocument & { title: string } + > +>(true) + +// Migration Documents +type FooDocument = prismic.PrismicDocument, "foo"> +type BarDocument = prismic.PrismicDocument<{ bar: prismic.KeyTextField }, "bar"> +type BazDocument = prismic.PrismicDocument, "baz"> +type Documents = FooDocument | BarDocument | BazDocument + +type MigrationDocuments = prismic.MigrationPrismicDocument + +/** + * Infers data type from document type. + */ +expectType({ + title: "", + uid: "", + type: "foo", + lang: "", + data: {}, +}) + +// @ts-expect-error - `FooDocument` has no `bar` field in `data` +expectType({ + title: "", + uid: "", + type: "foo", + lang: "", + data: { + bar: "", + }, +}) + +expectType({ + title: "", + uid: "", + type: "bar", + lang: "", + data: { + bar: "", + }, +}) + +// @ts-expect-error - `bar` is missing in `data` +expectType({ + title: "", + uid: "", + type: "bar", + lang: "", + data: {}, +}) + +/** + * Accepts migration field types. + */ +type Fields = { + image: prismic.ImageField + migrationImage: prismic.ImageField + link: prismic.LinkField + migrationLink: prismic.LinkField + linkToMedia: prismic.LinkToMediaField + migrationLinkToMedia: prismic.LinkToMediaField + contentRelationship: prismic.ContentRelationshipField + migrationContentRelationship: prismic.ContentRelationshipField +} + +type StaticDocument = prismic.PrismicDocument +type GroupDocument = prismic.PrismicDocument< + { group: prismic.GroupField }, + "group" +> +type SliceDocument = prismic.PrismicDocument< + { + slices: prismic.SliceZone< + prismic.SharedSlice< + "default", + prismic.SharedSliceVariation< + "default", + Fields & { group: prismic.GroupField }, + Fields + > + > + > + }, + "slice" +> +type AdvancedDocuments = StaticDocument | GroupDocument | SliceDocument +type MigrationAdvancedDocuments = + prismic.MigrationPrismicDocument + +// Static +expectType({ + title: "", + uid: "", + type: "static", + lang: "", + data: { + image: {} as prismic.ImageField, + migrationImage: {} as prismic.MigrationImageField, + link: {} as prismic.LinkField, + migrationLink: {} as prismic.MigrationLinkField, + linkToMedia: {} as prismic.LinkToMediaField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + contentRelationship: {} as prismic.ContentRelationshipField, + migrationContentRelationship: {} as prismic.ContentRelationshipField, + }, +}) + +// Group +expectType({ + title: "", + uid: "", + type: "group", + lang: "", + data: { + group: [ + { + image: {} as prismic.ImageField, + migrationImage: {} as prismic.MigrationImageField, + link: {} as prismic.LinkField, + migrationLink: {} as prismic.MigrationLinkField, + linkToMedia: {} as prismic.LinkToMediaField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + contentRelationship: {} as prismic.ContentRelationshipField, + migrationContentRelationship: {} as prismic.ContentRelationshipField, + }, + ], + }, +}) + +// Slice +expectType({ + title: "", + uid: "", + type: "slice", + lang: "", + data: { + slices: [ + { + slice_type: "default", + slice_label: null, + id: "", + variation: "default", + version: "", + primary: { + image: {} as prismic.ImageField, + migrationImage: {} as prismic.MigrationImageField, + link: {} as prismic.LinkField, + migrationLink: {} as prismic.MigrationLinkField, + linkToMedia: {} as prismic.LinkToMediaField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + contentRelationship: {} as prismic.ContentRelationshipField, + migrationContentRelationship: {} as prismic.ContentRelationshipField, + group: [ + { + image: {} as prismic.ImageField, + migrationImage: {} as prismic.MigrationImageField, + link: {} as prismic.LinkField, + migrationLink: {} as prismic.MigrationLinkField, + linkToMedia: {} as prismic.LinkToMediaField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + contentRelationship: {} as prismic.ContentRelationshipField, + migrationContentRelationship: + {} as prismic.ContentRelationshipField, + }, + ], + }, + items: [ + { + image: {} as prismic.ImageField, + migrationImage: {} as prismic.MigrationImageField, + link: {} as prismic.LinkField, + migrationLink: {} as prismic.MigrationLinkField, + linkToMedia: {} as prismic.LinkToMediaField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + contentRelationship: {} as prismic.ContentRelationshipField, + migrationContentRelationship: + {} as prismic.ContentRelationshipField, + }, + ], + }, + ], + }, +}) diff --git a/test/types/migration.types.ts b/test/types/migration.types.ts new file mode 100644 index 00000000..a6899bd0 --- /dev/null +++ b/test/types/migration.types.ts @@ -0,0 +1,105 @@ +import type { TypeEqual, TypeOf } from "ts-expect" +import { expectType } from "ts-expect" + +import * as prismic from "../../src" + +// Default migration +const defaultMigration = prismic.createMigration() + +// Migration Documents +type FooDocument = prismic.PrismicDocument, "foo"> +type BarDocument = prismic.PrismicDocument, "bar"> +type BazDocument = prismic.PrismicDocument, "baz"> +type Documents = FooDocument | BarDocument | BazDocument + +const documentsMigration = prismic.createMigration() + +/** + * Ensuring no full overlap between test types as we're testing for narrowing. + */ +expectType>(false) +expectType< + TypeEqual, prismic.Query> +>(false) +expectType>(false) + +expectType>(false) +expectType, prismic.Query>>( + false, +) +expectType>(false) + +expectType>(false) +expectType< + TypeEqual, prismic.Query> +>(false) +expectType>(false) + +/** + * createAsset + */ + +// Default +const defaultCreateAsset = defaultMigration.createAsset("url", "name") +expectType>(true) +expectType< + TypeEqual +>(true) +expectType< + TypeEqual< + typeof defaultCreateAsset.linkToMedia, + prismic.MigrationLinkToMediaField + > +>(true) +expectType< + TypeOf +>(true) + +// Documents +const documentsCreateAsset = defaultMigration.createAsset("url", "name") +expectType>( + true, +) +expectType< + TypeEqual +>(true) +expectType< + TypeEqual< + typeof documentsCreateAsset.linkToMedia, + prismic.MigrationLinkToMediaField + > +>(true) +expectType< + TypeOf +>(true) + +/** + * createDocument - basic + */ + +// Default +const defaultCreateDocument = defaultMigration.createDocument({ + title: "", + type: "", + uid: "", + lang: "", + data: {}, +}) +expectType< + TypeEqual +>(true) + +// Documents +const documentsCreateDocument = documentsMigration.createDocument({ + title: "", + type: "foo", + uid: "", + lang: "", + data: {}, +}) +expectType< + TypeEqual< + typeof documentsCreateDocument, + prismic.MigrationPrismicDocument + > +>(true) From 9c1ade863feb6630d29dc6314d3133ef0f40ec9a Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 26 Aug 2024 12:20:02 +0200 Subject: [PATCH 05/61] refactor: `MigrationPrismicDocumentParams` --- src/Migration.ts | 31 +++++++++++----- src/types/migration/asset.ts | 36 ++++++++++++++++++ src/types/migration/document.ts | 51 +++++++++++++++----------- src/types/migration/fields.ts | 40 ++++++++++++++++---- src/types/migration/richText.ts | 8 ++++ test/types/migration-document.types.ts | 9 ----- test/types/migration.types.ts | 32 +++++++++------- 7 files changed, 145 insertions(+), 62 deletions(-) diff --git a/src/Migration.ts b/src/Migration.ts index 4092fdc2..9becfa56 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -1,6 +1,9 @@ import type { Asset } from "./types/api/asset/asset" import type { MigrationAsset } from "./types/migration/asset" -import type { MigrationPrismicDocument } from "./types/migration/document" +import type { + MigrationPrismicDocument, + MigrationPrismicDocumentParams, +} from "./types/migration/document" import { MigrationFieldType, type MigrationImageField, @@ -43,8 +46,11 @@ export class Migration< TMigrationDocuments extends MigrationPrismicDocument = MigrationPrismicDocument, > { - #documents: TMigrationDocuments[] = [] - #documentsByUID: Record> = {} + #documents: { + document: TMigrationDocuments + params: MigrationPrismicDocumentParams + }[] = [] + #indexedDocuments: Record> = {} #assets: Map = new Map() @@ -54,7 +60,7 @@ export class Migration< createAsset( file: MigrationAsset["file"], filename: MigrationAsset["filename"], - options?: { + params?: { notes?: string credits?: string alt?: string @@ -117,13 +123,18 @@ export class Migration< createDocument( document: ExtractMigrationDocumentType, + documentName: MigrationPrismicDocumentParams["documentName"], + params: Omit = {}, ): ExtractMigrationDocumentType { - this.#documents.push(document) + this.#documents.push({ + document, + params: { documentName, ...params }, + }) - if (!(document.type in this.#documentsByUID)) { - this.#documentsByUID[document.type] = {} + if (!(document.type in this.#indexedDocuments)) { + this.#indexedDocuments[document.type] = {} } - this.#documentsByUID[document.type][document.uid || SINGLE_KEY] = document + this.#indexedDocuments[document.type][document.uid || SINGLE_KEY] = document return document } @@ -135,7 +146,7 @@ export class Migration< { type: TType } > = Extract, >(documentType: TType, uid: string): TMigrationDocument | undefined { - return this.#documentsByUID[documentType]?.[uid] as + return this.#indexedDocuments[documentType]?.[uid] as | TMigrationDocument | undefined } @@ -147,7 +158,7 @@ export class Migration< { type: TType } > = Extract, >(documentType: TType): TMigrationDocument | undefined | undefined { - return this.#documentsByUID[documentType]?.[SINGLE_KEY] as + return this.#indexedDocuments[documentType]?.[SINGLE_KEY] as | TMigrationDocument | undefined } diff --git a/src/types/migration/asset.ts b/src/types/migration/asset.ts index 93a5b535..088ae5cc 100644 --- a/src/types/migration/asset.ts +++ b/src/types/migration/asset.ts @@ -1,9 +1,45 @@ +/** + * An asset to be uploaded to Prismic media library + */ export type MigrationAsset = { + /** + * ID of the asset used to reference it in Prismic documents. + * + * @internal + */ id: string | URL | File | NonNullable[0]>[0] + + /** + * File to be uploaded as an asset. + */ file: string | URL | File | NonNullable[0]>[0] + + /** + * Filename of the asset. + */ filename: string + + /** + * Notes about the asset. Notes are private and only visible in Prismic media + * library. + */ notes?: string + + /** + * Credits and copyright for the asset if any. + */ credits?: string + + /** + * Alternate text for the asset. + */ alt?: string + + /** + * Tags associated with the asset. + * + * @remarks + * Tags should be at least 3 characters long and 20 characters at most. + */ tags?: string[] } diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts index 43af8974..fcf0b2c1 100644 --- a/src/types/migration/document.ts +++ b/src/types/migration/document.ts @@ -106,26 +106,6 @@ export type MigrationPrismicDocument< > = TDocument extends PrismicDocument ? MakeUIDOptional<{ - /** - * Title of the document displayed in the editor. - */ - title: string - - /** - * A link to the master language document. - */ - // We're forced to inline `MigrationContentRelationshipField` here, otherwise - // it creates a circular reference to itself which makes TypeScript unhappy. - // (but I think it's weird and it doesn't make sense :thinking:) - masterLanguageDocument?: - | PrismicDocument - | MigrationPrismicDocument - | (() => - | Promise - | PrismicDocument - | MigrationPrismicDocument - | undefined) - /** * Type of the document. */ @@ -150,8 +130,8 @@ export type MigrationPrismicDocument< /** * Alternate language documents from Prismic content API. Used as a - * substitute to the `masterLanguageDocument` property when the latter - * is not available. + * substitute to the `masterLanguageDocument` options when the latter is + * not available. * * @internal */ @@ -164,3 +144,30 @@ export type MigrationPrismicDocument< data: FieldsToMigrationFields }> : never + +/** + * Parameters used when creating a Prismic document with the migration API. + * + * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} + */ +export type MigrationPrismicDocumentParams = { + /** + * Name of the document displayed in the editor. + */ + documentName: string + + /** + * A link to the master language document. + */ + // We're forced to inline `MigrationContentRelationshipField` here, otherwise + // it creates a circular reference to itself which makes TypeScript unhappy. + // (but I think it's weird and it doesn't make sense :thinking:) + masterLanguageDocument?: + | PrismicDocument + | MigrationPrismicDocument + | (() => + | Promise + | PrismicDocument + | MigrationPrismicDocument + | undefined) +} diff --git a/src/types/migration/fields.ts b/src/types/migration/fields.ts index 288a4fae..ca2b13e0 100644 --- a/src/types/migration/fields.ts +++ b/src/types/migration/fields.ts @@ -1,4 +1,12 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { ContentRelationshipField } from "../value/contentRelationship" import type { PrismicDocument } from "../value/document" +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { ImageField } from "../value/image" +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { LinkField } from "../value/link" +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { LinkToMediaField } from "../value/linkToMedia" import type { MigrationAsset } from "./asset" import type { MigrationPrismicDocument } from "./document" @@ -8,23 +16,41 @@ export const MigrationFieldType = { LinkToMedia: "linkToMedia", } as const -export type MigrationImageField = MigrationAsset & { - migrationType: typeof MigrationFieldType.Image -} +/** + * An alternate version of the {@link ImageField} for use with the migration API. + */ +export type MigrationImageField = + | (MigrationAsset & { + migrationType: typeof MigrationFieldType.Image + }) + | undefined -export type MigrationLinkToMediaField = MigrationAsset & { - migrationType: typeof MigrationFieldType.LinkToMedia -} +/** + * An alternate version of the {@link LinkToMediaField} for use with the + * migration API. + */ +export type MigrationLinkToMediaField = + | (MigrationAsset & { + migrationType: typeof MigrationFieldType.LinkToMedia + }) + | undefined +/** + * An alternate version of the {@link ContentRelationshipField} for use with the + * migration API. + */ export type MigrationContentRelationshipField = | PrismicDocument - | MigrationPrismicDocument | (() => | Promise | PrismicDocument | MigrationPrismicDocument | undefined) + | undefined +/** + * An alternate version of the {@link LinkField} for use with the migration API. + */ export type MigrationLinkField = | MigrationLinkToMediaField | MigrationContentRelationshipField diff --git a/src/types/migration/richText.ts b/src/types/migration/richText.ts index 2f8935e1..18e86f7e 100644 --- a/src/types/migration/richText.ts +++ b/src/types/migration/richText.ts @@ -2,10 +2,18 @@ import type { RTImageNode, RTLinkNode } from "../value/richText" import type { MigrationImageField, MigrationLinkField } from "./fields" +/** + * An alternate version of {@link RTLinkNode} that supports + * {@link MigrationLinkField} for use with the migration API. + */ export type MigrationRTLinkNode = Omit & { data: MigrationLinkField } +/** + * An alternate version of {@link RTImageNode} that supports + * {@link MigrationImageField} for use with the migration API. + */ export type MigrationRTImageNode = MigrationImageField & { linkTo?: RTImageNode["linkTo"] | MigrationLinkField } diff --git a/test/types/migration-document.types.ts b/test/types/migration-document.types.ts index a745ced2..38d73793 100644 --- a/test/types/migration-document.types.ts +++ b/test/types/migration-document.types.ts @@ -20,7 +20,6 @@ import type * as prismic from "../../src" } expectType({ - title: "", uid: "", type: "", lang: "", @@ -31,7 +30,6 @@ expectType({ * Supports any field when generic. */ expectType({ - title: "", uid: "", type: "", lang: "", @@ -63,7 +61,6 @@ type MigrationDocuments = prismic.MigrationPrismicDocument * Infers data type from document type. */ expectType({ - title: "", uid: "", type: "foo", lang: "", @@ -72,7 +69,6 @@ expectType({ // @ts-expect-error - `FooDocument` has no `bar` field in `data` expectType({ - title: "", uid: "", type: "foo", lang: "", @@ -82,7 +78,6 @@ expectType({ }) expectType({ - title: "", uid: "", type: "bar", lang: "", @@ -93,7 +88,6 @@ expectType({ // @ts-expect-error - `bar` is missing in `data` expectType({ - title: "", uid: "", type: "bar", lang: "", @@ -140,7 +134,6 @@ type MigrationAdvancedDocuments = // Static expectType({ - title: "", uid: "", type: "static", lang: "", @@ -158,7 +151,6 @@ expectType({ // Group expectType({ - title: "", uid: "", type: "group", lang: "", @@ -180,7 +172,6 @@ expectType({ // Slice expectType({ - title: "", uid: "", type: "slice", lang: "", diff --git a/test/types/migration.types.ts b/test/types/migration.types.ts index a6899bd0..8e8a0d8b 100644 --- a/test/types/migration.types.ts +++ b/test/types/migration.types.ts @@ -78,25 +78,29 @@ expectType< */ // Default -const defaultCreateDocument = defaultMigration.createDocument({ - title: "", - type: "", - uid: "", - lang: "", - data: {}, -}) +const defaultCreateDocument = defaultMigration.createDocument( + { + type: "", + uid: "", + lang: "", + data: {}, + }, + "", +) expectType< TypeEqual >(true) // Documents -const documentsCreateDocument = documentsMigration.createDocument({ - title: "", - type: "foo", - uid: "", - lang: "", - data: {}, -}) +const documentsCreateDocument = documentsMigration.createDocument( + { + type: "foo", + uid: "", + lang: "", + data: {}, + }, + "", +) expectType< TypeEqual< typeof documentsCreateDocument, From e24274e8595ce2275d3359948e63015aed27a2b8 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 26 Aug 2024 18:16:25 +0200 Subject: [PATCH 06/61] wip: `client.migrate` --- src/BaseClient.ts | 2 + src/Client.ts | 5 - src/Migration.ts | 14 +- src/WriteClient.ts | 506 ++++++++++++++++++++++++---- src/types/api/asset/asset.ts | 8 +- src/types/api/migration/document.ts | 50 +++ src/types/api/repository.ts | 5 + src/types/migration/document.ts | 19 +- test/types/api-language.types.ts | 1 + test/types/api-repository.types.ts | 2 +- 10 files changed, 535 insertions(+), 77 deletions(-) create mode 100644 src/types/api/migration/document.ts diff --git a/src/BaseClient.ts b/src/BaseClient.ts index 7bde7bcf..fb05f2b7 100644 --- a/src/BaseClient.ts +++ b/src/BaseClient.ts @@ -68,10 +68,12 @@ export interface RequestInitLike extends Pick { * The minimum required properties from Response. */ export interface ResponseLike { + ok: boolean status: number headers: HeadersLike // eslint-disable-next-line @typescript-eslint/no-explicit-any json(): Promise + blob(): Promise } /** diff --git a/src/Client.ts b/src/Client.ts index a9692011..c75463c8 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1697,11 +1697,6 @@ export class Client< ): Promise { const res = await super.fetch(url, params) - console.log(res.status, params.fetchOptions?.method || "GET", url) - if (res.status === 429) { - console.log(url, params, res) - } - if (res.status !== 404 && res.status !== 429 && res.json == null) { throw new PrismicError(undefined, url, res.json) } diff --git a/src/Migration.ts b/src/Migration.ts index 9becfa56..198a1173 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -46,13 +46,19 @@ export class Migration< TMigrationDocuments extends MigrationPrismicDocument = MigrationPrismicDocument, > { - #documents: { + /** + * @internal + */ + documents: { document: TMigrationDocuments params: MigrationPrismicDocumentParams }[] = [] #indexedDocuments: Record> = {} - #assets: Map = new Map() + /** + * @internal + */ + assets: Map = new Map() constructor() {} @@ -105,7 +111,7 @@ export class Migration< } } - this.#assets.set(asset.id, asset) + this.assets.set(asset.id, asset) return { migrationType: MigrationFieldType.Image, @@ -126,7 +132,7 @@ export class Migration< documentName: MigrationPrismicDocumentParams["documentName"], params: Omit = {}, ): ExtractMigrationDocumentType { - this.#documents.push({ + this.documents.push({ document, params: { documentName, ...params }, }) diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 2d938de8..ec28006b 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -5,10 +5,10 @@ import type { BulkDeleteAssetsParams, GetAssetsParams, GetAssetsResult, - PatchAssetsParams, - PatchAssetsResult, - PostAssetsParams, - PostAssetsResult, + PatchAssetParams, + PatchAssetResult, + PostAssetParams, + PostAssetResult, } from "./types/api/asset/asset" import type { GetTagsResult, @@ -16,11 +16,43 @@ import type { PostTagResult, Tag, } from "./types/api/asset/tag" +import type { PutDocumentResult } from "./types/api/migration/document" +import { + type PostDocumentParams, + type PostDocumentResult, + type PutDocumentParams, +} from "./types/api/migration/document" +import type { MigrationAsset } from "./types/migration/asset" +import type { + MigrationPrismicDocument, + MigrationPrismicDocumentParams, +} from "./types/migration/document" +import type { FilledContentRelationshipField } from "./types/value/contentRelationship" import type { PrismicDocument } from "./types/value/document" +import { LinkType } from "./types/value/link" + +import { PrismicError } from "./errors/PrismicError" -import type { FetchParams } from "./BaseClient" +import type { FetchParams, RequestInitLike } from "./BaseClient" import { Client } from "./Client" import type { ClientConfig } from "./Client" +import type { Migration } from "./Migration" + +/** + * 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 type GetAssetsReturnType = { results: Asset[] @@ -90,6 +122,290 @@ export class WriteClient< } } + async migrate( + migration: Migration, + { + reporter, + ...params + }: { reporter?: (message: string) => void } & FetchParams = {}, + ): Promise { + const _assets = await this.migrateCreateAssets(migration, { + reporter: (message) => reporter?.(`01_createAssets - ${message}`), + ...params, + }) + + const _documents = await this.migrationCreateDocuments(migration, { + reporter: (message) => reporter?.(`02_createDocuments - ${message}`), + ...params, + }) + } + + private async migrateCreateAssets( + migration: Migration, + { + reporter, + ...fetchParams + }: { reporter?: (message: string) => void } & FetchParams = {}, + ): Promise> { + const assets = new Map() + + // Get all existing assets + let getAssetsResult: GetAssetsReturnType | undefined = undefined + do { + if (getAssetsResult) { + getAssetsResult = await getAssetsResult.next!() + } else { + getAssetsResult = await this.getAssets({ + pageSize: 100, + ...fetchParams, + }) + } + + for (const asset of getAssetsResult.results) { + assets.set(asset.id, asset) + } + } while (getAssetsResult?.next) + + reporter?.(`Found ${assets.size} existing assets`) + + // Create assets + if (migration.assets.size) { + let i = 0 + for (const [_, { id, file, filename, ...params }] of migration.assets) { + reporter?.(`Creating asset - ${++i}/${migration.assets.size}`) + + if (typeof id !== "string" || !assets.has(id)) { + let resolvedFile: PostAssetParams["file"] | File + if (typeof file === "string") { + let url: URL | undefined + try { + url = new URL(file) + } catch (error) { + // noop + } + + if (url) { + resolvedFile = await this.fetchForeignAsset( + url.toString(), + fetchParams, + ) + } else { + resolvedFile = file + } + } else if (file instanceof URL) { + resolvedFile = await this.fetchForeignAsset( + file.toString(), + fetchParams, + ) + } else { + resolvedFile = file + } + + const asset = await this.createAsset(resolvedFile, filename, { + ...params, + ...fetchParams, + }) + + assets.set(id, asset) + } + } + + reporter?.(`Created ${i} assets`) + } else { + reporter?.(`No assets to create`) + } + + return assets + } + + private async migrationCreateDocuments( + migration: Migration, + { + reporter, + ...fetchParams + }: { reporter?: (message: string) => void } & FetchParams = {}, + ): Promise< + Map< + | string + | TDocuments + | MigrationPrismicDocument + | MigrationPrismicDocument, + FilledContentRelationshipField + > + > { + // Should this be a function of `Client`? + // Resolve master locale + const repository = await this.getRepository(fetchParams) + const masterLocale = repository.languages.find((lang) => lang.is_master)!.id + reporter?.(`Resolved master locale \`${masterLocale}\``) + + // Get all existing documents + const existingDocuments = await this.dangerouslyGetAll(fetchParams) + + reporter?.(`Found ${existingDocuments.length} existing documents`) + + const documents: Map< + | string + | TDocuments + | MigrationPrismicDocument + | MigrationPrismicDocument, + FilledContentRelationshipField + > = new Map() + for (const document of existingDocuments) { + const contentRelationship = { + link_type: LinkType.Document, + id: document.id, + uid: document.uid ?? undefined, + type: document.type, + tags: document.tags, + lang: document.lang, + url: undefined, + slug: undefined, + isBroken: false, + data: undefined, + } + + documents.set(document, contentRelationship) + documents.set(document.id, contentRelationship) + } + + const masterLocaleDocuments: { + document: MigrationPrismicDocument + params: MigrationPrismicDocumentParams + }[] = [] + const nonMasterLocaleDocuments: { + document: MigrationPrismicDocument + params: MigrationPrismicDocumentParams + }[] = [] + + for (const { document, params } of migration.documents) { + if (document.lang === masterLocale) { + masterLocaleDocuments.push({ document, params }) + } else { + nonMasterLocaleDocuments.push({ document, params }) + } + } + + // Create master locale documents first + let i = 0 + let created = 0 + if (masterLocaleDocuments.length) { + for (const { document, params } of masterLocaleDocuments) { + if (document.id && documents.has(document.id)) { + reporter?.( + `Skipping existing master locale document \`${params.documentName}\` - ${++i}/${masterLocaleDocuments.length}`, + ) + + documents.set(document, documents.get(document.id)!) + } else { + created++ + reporter?.( + `Creating master locale document \`${params.documentName}\` - ${++i}/${masterLocaleDocuments.length}`, + ) + + const { id } = await this.createDocument( + { ...document, data: {} }, + params.documentName, + { + ...params, + ...fetchParams, + }, + ) + + documents.set(document, { + link_type: LinkType.Document, + id, + uid: document.uid ?? undefined, + type: document.type, + tags: document.tags ?? [], + lang: document.lang, + url: undefined, + slug: undefined, + isBroken: false, + data: undefined, + }) + } + } + } + + if (created > 0) { + reporter?.(`Created ${created} master locale documents`) + } else { + reporter?.(`No master locale documents to create`) + } + + // Create non-master locale documents + i = 0 + created = 0 + if (nonMasterLocaleDocuments.length) { + for (const { document, params } of nonMasterLocaleDocuments) { + if (document.id && documents.has(document.id)) { + reporter?.( + `Skipping existing non-master locale document \`${params.documentName}\` - ${++i}/${nonMasterLocaleDocuments.length}`, + ) + + documents.set(document, documents.get(document.id)!) + } else { + created++ + reporter?.( + `Creating non-master locale document \`${params.documentName}\` - ${++i}/${nonMasterLocaleDocuments.length}`, + ) + + let masterLanguageDocumentID: string | undefined + if (params.masterLanguageDocument) { + if (typeof params.masterLanguageDocument === "function") { + const masterLanguageDocument = + await params.masterLanguageDocument() + + if (masterLanguageDocument) { + masterLanguageDocument + masterLanguageDocumentID = documents.get( + masterLanguageDocument, + )?.id + } + } else { + masterLanguageDocumentID = params.masterLanguageDocument.id + } + } else if (document.alternate_languages) { + masterLanguageDocumentID = document.alternate_languages.find( + ({ lang }) => lang === masterLocale, + )?.id + } + + const { id } = await this.createDocument( + { ...document, data: {} }, + params.documentName, + { + masterLanguageDocumentID, + ...fetchParams, + }, + ) + + documents.set(document, { + link_type: LinkType.Document, + id, + uid: document.uid ?? undefined, + type: document.type, + tags: document.tags ?? [], + lang: document.lang, + url: undefined, + slug: undefined, + isBroken: false, + data: undefined, + }) + } + } + } + + if (created > 0) { + reporter?.(`Created ${created} non-master locale documents`) + } else { + reporter?.(`No non-master locale documents to create`) + } + + return documents + } + async getAssets({ pageSize, cursor, @@ -104,17 +420,31 @@ export class WriteClient< tags = await this.resolveAssetTagIDs(tags, params) } - const url = this.buildWriteQueryURL( - new URL("assets", this.assetAPIEndpoint), - { - pageSize, - cursor, - assetType, - keyword, - ids, - tags, - }, - ) + const url = new URL("assets", this.assetAPIEndpoint) + + if (pageSize) { + url.searchParams.set("pageSize", pageSize.toString()) + } + + if (cursor) { + url.searchParams.set("cursor", cursor) + } + + if (assetType) { + url.searchParams.set("assetType", assetType) + } + + if (keyword) { + url.searchParams.set("keyword", keyword) + } + + if (ids) { + ids.forEach((id) => url.searchParams.append("ids", id)) + } + + if (tags) { + tags.forEach((tag) => url.searchParams.append("tags", tag)) + } const { items, @@ -122,7 +452,7 @@ export class WriteClient< missing_ids, cursor: nextCursor, } = await this.fetch( - url, + url.toString(), this.buildWriteQueryParams({ params }), ) @@ -145,8 +475,8 @@ export class WriteClient< } } - async createAsset( - file: PostAssetsParams["file"], + private async createAsset( + file: PostAssetParams["file"] | File, filename: string, { notes, @@ -178,7 +508,7 @@ export class WriteClient< formData.append("alt", alt) } - const asset = await this.fetch( + const asset = await this.fetch( url.toString(), this.buildWriteQueryParams({ method: "POST", @@ -194,7 +524,7 @@ export class WriteClient< return asset } - async updateAsset( + private async updateAsset( id: string, { notes, @@ -203,11 +533,9 @@ export class WriteClient< filename, tags, ...params - }: PatchAssetsParams & FetchParams = {}, + }: PatchAssetParams & FetchParams = {}, ): Promise { - const url = this.buildWriteQueryURL( - new URL(`assets/${id}`, this.assetAPIEndpoint), - ) + const url = new URL(`assets/${id}`, this.assetAPIEndpoint) // Resolve tags if any and create missing ones if (tags && tags.length) { @@ -217,9 +545,9 @@ export class WriteClient< }) } - return this.fetch( - url, - this.buildWriteQueryParams({ + return this.fetch( + url.toString(), + this.buildWriteQueryParams({ method: "PATCH", body: { notes, @@ -233,33 +561,29 @@ export class WriteClient< ) } - async deleteAsset( + private async deleteAsset( assetOrID: string | Asset, params?: FetchParams, ): Promise { - const url = this.buildWriteQueryURL( - new URL( - `assets/${typeof assetOrID === "string" ? assetOrID : assetOrID.id}`, - this.assetAPIEndpoint, - ), + const url = new URL( + `assets/${typeof assetOrID === "string" ? assetOrID : assetOrID.id}`, + this.assetAPIEndpoint, ) await this.fetch( - url, + url.toString(), this.buildWriteQueryParams({ method: "DELETE", params }), ) } - async deleteAssets( + private async deleteAssets( assetsOrIDs: (string | Asset)[], params?: FetchParams, ): Promise { - const url = this.buildWriteQueryURL( - new URL("assets/bulk-delete", this.assetAPIEndpoint), - ) + const url = new URL("assets/bulk-delete", this.assetAPIEndpoint) await this.fetch( - url, + url.toString(), this.buildWriteQueryParams({ method: "POST", body: { @@ -272,6 +596,32 @@ export class WriteClient< ) } + private async fetchForeignAsset( + url: string, + params: FetchParams = {}, + ): Promise { + const requestInit: RequestInitLike = { + ...this.fetchOptions, + ...params.fetchOptions, + headers: { + ...this.fetchOptions?.headers, + ...params.fetchOptions?.headers, + }, + signal: + params.fetchOptions?.signal || + params.signal || + this.fetchOptions?.signal, + } + + const res = await this.fetchFn(url, requestInit) + + if (!res.ok) { + throw new PrismicError("Could not fetch foreign asset", url, undefined) + } + + return res.blob() + } + private _resolveAssetTagIDsLimit = pLimit({ limit: 1 }) private async resolveAssetTagIDs( tagNamesOrIDs: string[] = [], @@ -320,10 +670,10 @@ export class WriteClient< name: string, params?: FetchParams, ): Promise { - const url = this.buildWriteQueryURL(new URL("tags", this.assetAPIEndpoint)) + const url = new URL("tags", this.assetAPIEndpoint) return this.fetch( - url, + url.toString(), this.buildWriteQueryParams({ method: "POST", body: { name }, @@ -333,31 +683,71 @@ export class WriteClient< } private async getAssetTags(params?: FetchParams): Promise { - const url = this.buildWriteQueryURL(new URL("tags", this.assetAPIEndpoint)) + const url = new URL("tags", this.assetAPIEndpoint) const { items } = await this.fetch( - url, + url.toString(), this.buildWriteQueryParams({ params }), ) return items } - private buildWriteQueryURL>( - url: URL, - searchParams?: TSearchParams, - ): string { - if (searchParams) { - Object.entries(searchParams).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((item) => url.searchParams.append(key, item.toString())) - } else if (value) { - url.searchParams.set(key, value.toString()) - } - }) - } + private async createDocument( + document: MigrationPrismicDocument>, + documentName: MigrationPrismicDocumentParams["documentName"], + { + masterLanguageDocumentID, + ...params + }: { masterLanguageDocumentID?: string } & FetchParams = {}, + ): Promise<{ id: string }> { + const url = new URL("documents", this.migrationAPIEndpoint) - return url.toString() + const result = await this.fetch( + url.toString(), + this.buildWriteQueryParams({ + method: "POST", + body: { + title: documentName, + type: document.type, + uid: document.uid || undefined, + lang: document.lang, + alternate_language_id: masterLanguageDocumentID, + tags: document.tags, + data: document.data, + }, + params, + }), + ) + + return { id: result.id } + } + + private async updateDocument( + id: string, + document: Pick< + MigrationPrismicDocument>, + "uid" | "tags" | "data" + > & { + documentName?: MigrationPrismicDocumentParams["documentName"] + }, + params?: FetchParams, + ): Promise { + const url = new URL(`documents/${id}`, this.migrationAPIEndpoint) + + await this.fetch( + url.toString(), + this.buildWriteQueryParams({ + method: "PUT", + body: { + title: document.documentName, + uid: document.uid || undefined, + tags: document.tags, + data: document.data, + }, + params, + }), + ) } private buildWriteQueryParams>({ diff --git a/src/types/api/asset/asset.ts b/src/types/api/asset/asset.ts index bfa3b2f2..77fac0ca 100644 --- a/src/types/api/asset/asset.ts +++ b/src/types/api/asset/asset.ts @@ -79,16 +79,16 @@ export type GetAssetsResult = { is_opensearch_result: boolean } -export type PostAssetsParams = { +export type PostAssetParams = { file: BlobPart notes?: string credits?: string alt?: string } -export type PostAssetsResult = Asset +export type PostAssetResult = Asset -export type PatchAssetsParams = { +export type PatchAssetParams = { notes?: string credits?: string alt?: string @@ -96,7 +96,7 @@ export type PatchAssetsParams = { tags?: string[] } -export type PatchAssetsResult = Asset +export type PatchAssetResult = Asset export type BulkDeleteAssetsParams = { ids: string[] diff --git a/src/types/api/migration/document.ts b/src/types/api/migration/document.ts new file mode 100644 index 00000000..e164206e --- /dev/null +++ b/src/types/api/migration/document.ts @@ -0,0 +1,50 @@ +import type { PrismicDocument } from "../../value/document" + +export type PostDocumentParams< + TDocument extends PrismicDocument = PrismicDocument, +> = + TDocument extends PrismicDocument + ? { + title: string + + type: TType + uid?: string + lang: TLang + alternate_language_id?: string + + tags?: string[] + data: TData | Record + } + : never + +export type PostDocumentResult< + TDocument extends PrismicDocument = PrismicDocument, +> = + TDocument extends PrismicDocument + ? { + title: string + + id: string + type: TType + lang: TLang + } & (TDocument["uid"] extends string + ? { uid: TDocument["uid"] } + : Record) + : never + +export type PutDocumentParams< + TDocument extends PrismicDocument = PrismicDocument, +> = { + title?: string + + uid?: string + + tags?: string[] + // We don't need to infer the document type as above here because we don't + // need to pair the document type with the document data in put requests. + data: TDocument["data"] | Record +} + +export type PutDocumentResult< + TDocument extends PrismicDocument = PrismicDocument, +> = PostDocumentResult diff --git a/src/types/api/repository.ts b/src/types/api/repository.ts index aeec2033..35d00723 100644 --- a/src/types/api/repository.ts +++ b/src/types/api/repository.ts @@ -91,6 +91,11 @@ export interface Language { * The name of the language. */ name: string + + /** + * Whether or not the language is the default language for the repository. + */ + is_master: boolean } /** diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts index fcf0b2c1..fde1bc11 100644 --- a/src/types/migration/document.ts +++ b/src/types/migration/document.ts @@ -118,15 +118,18 @@ export type MigrationPrismicDocument< uid: TDocument["uid"] /** - * Tags associated with document. + * Language of document. */ - // Made optional compared to the original type. - tags?: TDocument["tags"] + lang: TLang /** - * Language of document. + * The identifier for the document. Used for compatibily with the + * content API. + * + * @internal */ - lang: TLang + // Made optional compared to the original type. + id?: TDocument["id"] /** * Alternate language documents from Prismic content API. Used as a @@ -138,6 +141,12 @@ export type MigrationPrismicDocument< // Made optional compared to the original type. alternate_languages?: TDocument["alternate_languages"] + /** + * Tags associated with document. + */ + // Made optional compared to the original type. + tags?: TDocument["tags"] + /** * Data contained in the document. */ diff --git a/test/types/api-language.types.ts b/test/types/api-language.types.ts index c58beb45..a36a0836 100644 --- a/test/types/api-language.types.ts +++ b/test/types/api-language.types.ts @@ -21,4 +21,5 @@ import type * as prismic from "../../src" expectType({ id: "string", name: "string", + is_master: true, }) diff --git a/test/types/api-repository.types.ts b/test/types/api-repository.types.ts index 24fd1794..fb064fe7 100644 --- a/test/types/api-repository.types.ts +++ b/test/types/api-repository.types.ts @@ -29,7 +29,7 @@ expectType({ }, ], integrationFieldsRef: "string", - languages: [{ id: "string", name: "string" }], + languages: [{ id: "string", name: "string", is_master: true }], types: { foo: "string" }, tags: ["string"], forms: { From 02aa3e85d3b2c0524fad69ed252e7734da04953c Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 28 Aug 2024 16:16:05 +0200 Subject: [PATCH 07/61] feat: patch migration document data --- src/WriteClient.ts | 195 +++++---- src/lib/patchMigrationDocumentData.ts | 575 ++++++++++++++++++++++++++ src/types/migration/document.ts | 27 +- 3 files changed, 683 insertions(+), 114 deletions(-) create mode 100644 src/lib/patchMigrationDocumentData.ts diff --git a/src/WriteClient.ts b/src/WriteClient.ts index ec28006b..df5af112 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -1,8 +1,9 @@ import { pLimit } from "./lib/pLimit" +import type { AssetMap, DocumentMap } from "./lib/patchMigrationDocumentData" +import { patchMigrationDocumentData } from "./lib/patchMigrationDocumentData" import type { Asset, - BulkDeleteAssetsParams, GetAssetsParams, GetAssetsResult, PatchAssetParams, @@ -22,14 +23,11 @@ import { type PostDocumentResult, type PutDocumentParams, } from "./types/api/migration/document" -import type { MigrationAsset } from "./types/migration/asset" import type { MigrationPrismicDocument, MigrationPrismicDocumentParams, } from "./types/migration/document" -import type { FilledContentRelationshipField } from "./types/value/contentRelationship" import type { PrismicDocument } from "./types/value/document" -import { LinkType } from "./types/value/link" import { PrismicError } from "./errors/PrismicError" @@ -129,15 +127,24 @@ export class WriteClient< ...params }: { reporter?: (message: string) => void } & FetchParams = {}, ): Promise { - const _assets = await this.migrateCreateAssets(migration, { + const assets = await this.migrateCreateAssets(migration, { reporter: (message) => reporter?.(`01_createAssets - ${message}`), ...params, }) - const _documents = await this.migrationCreateDocuments(migration, { + const documents = await this.migrateCreateDocuments(migration, { reporter: (message) => reporter?.(`02_createDocuments - ${message}`), ...params, }) + + await this.migrateUpdateDocuments(migration, assets, documents, { + reporter: (message) => reporter?.(`03_updateDocuments - ${message}`), + ...params, + }) + + reporter?.( + `Migration complete, migrated ${migration.documents.length} documents and ${migration.assets.size} assets`, + ) } private async migrateCreateAssets( @@ -146,8 +153,8 @@ export class WriteClient< reporter, ...fetchParams }: { reporter?: (message: string) => void } & FetchParams = {}, - ): Promise> { - const assets = new Map() + ): Promise { + const assets: AssetMap = new Map() // Get all existing assets let getAssetsResult: GetAssetsReturnType | undefined = undefined @@ -218,21 +225,13 @@ export class WriteClient< return assets } - private async migrationCreateDocuments( + private async migrateCreateDocuments( migration: Migration, { reporter, ...fetchParams }: { reporter?: (message: string) => void } & FetchParams = {}, - ): Promise< - Map< - | string - | TDocuments - | MigrationPrismicDocument - | MigrationPrismicDocument, - FilledContentRelationshipField - > - > { + ): Promise> { // Should this be a function of `Client`? // Resolve master locale const repository = await this.getRepository(fetchParams) @@ -244,29 +243,11 @@ export class WriteClient< reporter?.(`Found ${existingDocuments.length} existing documents`) - const documents: Map< - | string - | TDocuments - | MigrationPrismicDocument - | MigrationPrismicDocument, - FilledContentRelationshipField - > = new Map() + const documents: DocumentMap = new Map() for (const document of existingDocuments) { - const contentRelationship = { - link_type: LinkType.Document, - id: document.id, - uid: document.uid ?? undefined, - type: document.type, - tags: document.tags, - lang: document.lang, - url: undefined, - slug: undefined, - isBroken: false, - data: undefined, - } - - documents.set(document, contentRelationship) - documents.set(document.id, contentRelationship) + // Index on document and document ID + documents.set(document, document) + documents.set(document.id, document) } const masterLocaleDocuments: { @@ -296,6 +277,7 @@ export class WriteClient< `Skipping existing master locale document \`${params.documentName}\` - ${++i}/${masterLocaleDocuments.length}`, ) + // Index the migration document documents.set(document, documents.get(document.id)!) } else { created++ @@ -304,6 +286,7 @@ export class WriteClient< ) const { id } = await this.createDocument( + // We'll upload docuements data later on. { ...document, data: {} }, params.documentName, { @@ -312,18 +295,7 @@ export class WriteClient< }, ) - documents.set(document, { - link_type: LinkType.Document, - id, - uid: document.uid ?? undefined, - type: document.type, - tags: document.tags ?? [], - lang: document.lang, - url: undefined, - slug: undefined, - isBroken: false, - data: undefined, - }) + documents.set(document, { ...document, id }) } } } @@ -351,6 +323,7 @@ export class WriteClient< `Creating non-master locale document \`${params.documentName}\` - ${++i}/${nonMasterLocaleDocuments.length}`, ) + // Resolve master language document ID let masterLanguageDocumentID: string | undefined if (params.masterLanguageDocument) { if (typeof params.masterLanguageDocument === "function") { @@ -358,7 +331,6 @@ export class WriteClient< await params.masterLanguageDocument() if (masterLanguageDocument) { - masterLanguageDocument masterLanguageDocumentID = documents.get( masterLanguageDocument, )?.id @@ -373,6 +345,7 @@ export class WriteClient< } const { id } = await this.createDocument( + // We'll upload docuements data later on. { ...document, data: {} }, params.documentName, { @@ -381,18 +354,7 @@ export class WriteClient< }, ) - documents.set(document, { - link_type: LinkType.Document, - id, - uid: document.uid ?? undefined, - type: document.type, - tags: document.tags ?? [], - lang: document.lang, - url: undefined, - slug: undefined, - isBroken: false, - data: undefined, - }) + documents.set(document, { ...document, id }) } } } @@ -406,6 +368,38 @@ export class WriteClient< return documents } + private async migrateUpdateDocuments( + migration: Migration, + assets: AssetMap, + documents: DocumentMap, + { + reporter, + ...fetchParams + }: { reporter?: (message: string) => void } & FetchParams = {}, + ): Promise { + if (migration.documents.length) { + let i = 0 + for (const { document, params } of migration.documents) { + reporter?.( + `Updating document \`${params.documentName}\` - ${++i}/${migration.documents.length}`, + ) + + const id = documents.get(document)!.id + const data = await patchMigrationDocumentData( + document.data, + assets, + documents, + ) + + await this.updateDocument(id, { data }, fetchParams) + } + + reporter?.(`Updated ${i} documents`) + } else { + reporter?.(`No documents to update`) + } + } + async getAssets({ pageSize, cursor, @@ -561,40 +555,43 @@ export class WriteClient< ) } - private async deleteAsset( - assetOrID: string | Asset, - params?: FetchParams, - ): Promise { - const url = new URL( - `assets/${typeof assetOrID === "string" ? assetOrID : assetOrID.id}`, - this.assetAPIEndpoint, - ) - - await this.fetch( - url.toString(), - this.buildWriteQueryParams({ method: "DELETE", params }), - ) - } - - private async deleteAssets( - assetsOrIDs: (string | Asset)[], - params?: FetchParams, - ): Promise { - const url = new URL("assets/bulk-delete", this.assetAPIEndpoint) - - await this.fetch( - url.toString(), - this.buildWriteQueryParams({ - method: "POST", - body: { - ids: assetsOrIDs.map((assetOrID) => - typeof assetOrID === "string" ? assetOrID : assetOrID.id, - ), - }, - params, - }), - ) - } + // We don't want to expose those utilities for now, + // and we don't have any internal use for them yet. + + // private async deleteAsset( + // assetOrID: string | Asset, + // params?: FetchParams, + // ): Promise { + // const url = new URL( + // `assets/${typeof assetOrID === "string" ? assetOrID : assetOrID.id}`, + // this.assetAPIEndpoint, + // ) + + // await this.fetch( + // url.toString(), + // this.buildWriteQueryParams({ method: "DELETE", params }), + // ) + // } + + // private async deleteAssets( + // assetsOrIDs: (string | Asset)[], + // params?: FetchParams, + // ): Promise { + // const url = new URL("assets/bulk-delete", this.assetAPIEndpoint) + + // await this.fetch( + // url.toString(), + // this.buildWriteQueryParams({ + // method: "POST", + // body: { + // ids: assetsOrIDs.map((assetOrID) => + // typeof assetOrID === "string" ? assetOrID : assetOrID.id, + // ), + // }, + // params, + // }), + // ) + // } private async fetchForeignAsset( url: string, diff --git a/src/lib/patchMigrationDocumentData.ts b/src/lib/patchMigrationDocumentData.ts new file mode 100644 index 00000000..d7c9b8f2 --- /dev/null +++ b/src/lib/patchMigrationDocumentData.ts @@ -0,0 +1,575 @@ +import type { Asset } from "../types/api/asset/asset" +import type { MigrationAsset } from "../types/migration/asset" +import type { + FieldToMigrationField, + FieldsToMigrationFields, + GroupFieldToMigrationField, + MigrationPrismicDocument, + RichTextFieldToMigrationField, + SliceZoneToMigrationField, +} from "../types/migration/document" +import type { + MigrationImageField, + MigrationLinkField, +} from "../types/migration/fields" +import { MigrationFieldType } from "../types/migration/fields" +import type { FilledContentRelationshipField } from "../types/value/contentRelationship" +import type { PrismicDocument } from "../types/value/document" +import type { GroupField, NestedGroupField } from "../types/value/group" +import type { FilledImageFieldImage, ImageField } from "../types/value/image" +import { LinkType } from "../types/value/link" +import type { LinkField } from "../types/value/link" +import type { LinkToMediaField } from "../types/value/linkToMedia" +import type { RTImageNode, RTInlineNode } from "../types/value/richText" +import { type RichTextField, RichTextNodeType } from "../types/value/richText" +import type { SharedSlice } from "../types/value/sharedSlice" +import type { Slice } from "../types/value/slice" +import type { SliceZone } from "../types/value/sliceZone" +import type { AnyRegularField } from "../types/value/types" + +import * as isFilled from "../helpers/isFilled" + +/** + * A map of asset IDs to asset used to resolve assets when patching migration + * Prismic documents. + * + * @internal + */ +export type AssetMap = Map + +/** + * A map of document IDs, documents, and migraiton documents to content + * relationship field used to resolve content relationships when patching + * migration Prismic documents. + * + * @internal + */ +export type DocumentMap = + Map< + | string + | TDocuments + | MigrationPrismicDocument + | MigrationPrismicDocument, + | PrismicDocument + | (Omit, "id"> & { id: string }) + > + +/** + * Convert an asset to an image field. + */ +const assetToImageField = ({ + id, + url, + width, + height, + alt, + credits, +}: Asset): ImageField => { + return { + id, + url, + dimensions: { width: width!, height: height! }, + edit: { x: 0, y: 0, zoom: 0, background: "transparent" }, + alt: alt || null, + copyright: credits || null, + } +} + +/** + * Convert an asset to a link to media field. + */ +const assetToLinkToMediaField = ({ + id, + filename, + kind, + url, + size, + width, + height, +}: Asset): LinkToMediaField<"filled"> => { + return { + id, + link_type: LinkType.Media, + name: filename, + kind, + url, + size: `${size}`, + width: width ? `${width}` : undefined, + height: height ? `${height}` : undefined, + } +} + +/** + * Convert an asset to an RT image node. + */ +const assetToRTImageNode = (asset: Asset): RTImageNode => { + return { + ...assetToImageField(asset), + type: RichTextNodeType.image, + } +} + +/** + * Convert a document to a content relationship field. + */ +const documentToContentRelationship = < + TDocuments extends PrismicDocument = PrismicDocument, +>( + document: + | TDocuments + | (Omit & { id: string }), +): FilledContentRelationshipField => { + return { + link_type: LinkType.Document, + id: document.id, + uid: document.uid ?? undefined, + type: document.type, + tags: document.tags ?? [], + lang: document.lang, + url: undefined, + slug: undefined, + isBroken: false, + data: undefined, + } +} + +/** + * Inherit query parameters from an original URL to a new URL. + */ +const inheritQueryParams = (url: string, original: string): string => { + const queryParams = original.split("?")[1] || "" + + return `${url.split("?")[0]}${queryParams ? `?${queryParams}` : ""}` +} + +/** + * Check if a field is a slice zone. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +const isSliceZone = ( + field: FieldToMigrationField, +): field is SliceZoneToMigrationField => { + return ( + Array.isArray(field) && + field.every((item) => "slice_type" in item && "id" in item) + ) +} + +/** + * Check if a field is a rich text field. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +const isRichText = ( + field: FieldToMigrationField, +): field is RichTextFieldToMigrationField => { + return ( + Array.isArray(field) && + field.every( + (item) => + ("type" in item && typeof item.type === "string") || + ("migrationType" in item && + item.migrationType === MigrationFieldType.Image), + ) + ) +} + +/** + * Check if a field is a group field. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +const isGroup = ( + field: FieldToMigrationField, +): field is GroupFieldToMigrationField => { + return !isSliceZone(field) && !isRichText(field) && Array.isArray(field) +} + +/** + * Check if a field is a link field. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +const isLink = ( + field: FieldToMigrationField, +): field is MigrationLinkField | LinkField => { + if (typeof field === "function") { + // Lazy content relationship field + return true + } else if (field && typeof field === "object" && !("version" in field)) { + if ( + "migrationType" in field && + field.migrationType === MigrationFieldType.LinkToMedia + ) { + // Migration link to media field + return true + } else if ( + "type" in field && + "lang" in field && + typeof field.lang === "string" && + field.id + ) { + // Content relationship field declared using another repository document + return true + } else if ( + "link_type" in field && + (field.link_type === LinkType.Document || + field.link_type === LinkType.Media || + field.link_type === LinkType.Web) + ) { + // Regular link field + return true + } + } + + return false +} + +/** + * Check if a field is an image field. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +const isImage = ( + field: FieldToMigrationField, +): field is MigrationImageField | ImageField => { + if (field && typeof field === "object" && !("version" in field)) { + if ( + "migrationType" in field && + field.migrationType === MigrationFieldType.Image + ) { + // Migration image field + return true + } else if ( + "id" in field && + "dimensions" in field && + field.dimensions && + isFilled.image(field) + ) { + // Regular image field + return true + } + } + + return false +} + +/** + * Patch a slice zone. + */ +const patchSliceZone = async < + TDocuments extends PrismicDocument = PrismicDocument, +>( + sliceZone: SliceZoneToMigrationField, + assets: AssetMap, + documents: DocumentMap, +): Promise => { + const result = [] as unknown as SliceZone + + for (const slice of sliceZone) { + const { primary, items, ...properties } = slice + const patchedPrimary = await patchRecord( + // We cast to `Slice["primary"]` which is stricter than `SharedSlice["primary"]` + // otherwise TypeScript gets confused while creating the patched slice below. + primary as Slice["primary"], + assets, + documents, + ) + const patchedItems = await patchGroup(items, assets, documents) + + result.push({ + ...properties, + primary: patchedPrimary, + items: patchedItems, + }) + } + + return result +} + +/** + * Patch a rich text field. + */ +const patchRichText = async < + TDocuments extends PrismicDocument = PrismicDocument, +>( + richText: RichTextFieldToMigrationField, + assets: AssetMap, + documents: DocumentMap, +): Promise => { + const result = [] as unknown as RichTextField<"filled"> + + for (const node of richText) { + if ("type" in node && typeof node.type === "string") { + if (node.type === RichTextNodeType.embed) { + result.push(node) + } else if (node.type === RichTextNodeType.image) { + const image = patchImage(node, assets) + + if (isFilled.image(image)) { + const linkTo = await patchLink(node.linkTo, assets, documents) + + result.push({ + ...image, + type: RichTextNodeType.image, + linkTo: isFilled.link(linkTo) ? linkTo : undefined, + }) + } + } else { + const { spans, ...properties } = node + + const patchedSpans: RTInlineNode[] = [] + + for (const span of spans) { + if (span.type === RichTextNodeType.hyperlink) { + const data = await patchLink(span.data, assets, documents) + + if (isFilled.link(data)) { + patchedSpans.push({ ...span, data }) + } + } else { + patchedSpans.push(span) + } + } + + result.push({ + ...properties, + spans: patchedSpans, + }) + } + } else { + // Migration image node + const asset = assets.get(node.id) + + const linkTo = await patchLink(node.linkTo, assets, documents) + + if (asset) { + result.push({ + ...assetToRTImageNode(asset), + linkTo: isFilled.link(linkTo) ? linkTo : undefined, + }) + } + } + } + + return result +} + +/** + * Patch a group field. + */ +const patchGroup = async < + TMigrationGroup extends GroupFieldToMigrationField< + GroupField | Slice["items"] | SharedSlice["items"] + >, + TDocuments extends PrismicDocument = PrismicDocument, +>( + group: TMigrationGroup, + assets: AssetMap, + documents: DocumentMap, +): Promise< + TMigrationGroup extends GroupFieldToMigrationField + ? TGroup + : never +> => { + const result = [] as unknown as GroupField< + Record, + "filled" + > + + for (const item of group) { + const patched = await patchRecord(item, assets, documents) + + result.push(patched) + } + + return result as TMigrationGroup extends GroupFieldToMigrationField< + infer TGroup + > + ? TGroup + : never +} + +/** + * Patch a link field. + */ +const patchLink = async ( + link: MigrationLinkField | LinkField, + assets: AssetMap, + documents: DocumentMap, +): Promise => { + if (link) { + if (typeof link === "function") { + const resolved = await link() + + if (resolved) { + // Documents and migration documents are indexed. + const maybeRelationship = documents.get(resolved) + + if (maybeRelationship) { + return documentToContentRelationship(maybeRelationship) + } + } + } else if ("migrationType" in link) { + // Migration link field + const asset = assets.get(link.id) + if (asset) { + return assetToLinkToMediaField(asset) + } + } else if ("link_type" in link) { + switch (link.link_type) { + case LinkType.Document: + // Existing content relationship + if (isFilled.contentRelationship(link)) { + const id = documents.get(link.id)?.id + if (id) { + return { ...link, id } + } else { + return { ...link, isBroken: true } + } + } + case LinkType.Media: + // Existing link to media + if (isFilled.linkToMedia(link)) { + const id = assets.get(link.id)?.id + if (id) { + return { ...link, id } + } + break + } + case LinkType.Web: + default: + return link + } + } else { + const maybeRelationship = documents.get(link.id) + + if (maybeRelationship) { + return documentToContentRelationship(maybeRelationship) + } + } + } + + return { + link_type: LinkType.Any, + } +} + +/** + * Patch an image field. + */ +const patchImage = ( + image: MigrationImageField | ImageField, + assets: AssetMap, +): ImageField => { + if (image) { + if ( + "migrationType" in image && + image.migrationType === MigrationFieldType.Image + ) { + // Migration image field + const asset = assets.get(image.id) + if (asset) { + return assetToImageField(asset) + } + } else if ( + "dimensions" in image && + image.dimensions && + isFilled.image(image) + ) { + // Regular image field + const { + id, + url, + dimensions, + edit, + alt, + copyright: _, + ...thumbnails + } = image + const asset = assets.get(id) + + if (asset) { + const result = { + id: asset.id, + url: inheritQueryParams(asset.url, url), + dimensions, + edit, + alt: alt || null, + copyright: asset.credits || null, + } as ImageField + + if (Object.keys(thumbnails).length > 0) { + for (const name in thumbnails) { + const { url, dimensions, edit, alt } = ( + thumbnails as Record + )[name] + + result[name] = { + id: asset.id, + url: inheritQueryParams(asset.url, url), + dimensions, + edit, + alt: alt || null, + copyright: asset.credits || null, + } + } + } + + return result + } + } + } + + return {} +} + +const patchRecord = async < + TFields extends Record, + TDocuments extends PrismicDocument = PrismicDocument, +>( + record: FieldsToMigrationFields, + assets: AssetMap, + documents: DocumentMap, +): Promise => { + const result: Record = {} + + for (const [key, field] of Object.entries(record)) { + if (isSliceZone(field)) { + result[key] = await patchSliceZone(field, assets, documents) + } else if (isRichText(field)) { + result[key] = await patchRichText(field, assets, documents) + } else if (isGroup(field)) { + result[key] = await patchGroup(field, assets, documents) + } else if (isLink(field)) { + result[key] = await patchLink(field, assets, documents) + } else if (isImage(field)) { + result[key] = patchImage(field, assets) + } else { + result[key] = field + } + } + + return result as TFields +} + +export const patchMigrationDocumentData = async < + TDocuments extends PrismicDocument = PrismicDocument, +>( + data: MigrationPrismicDocument["data"], + assets: AssetMap, + documents: DocumentMap, +): Promise => { + return patchRecord(data, assets, documents) +} diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts index fde1bc11..e9eebc54 100644 --- a/src/types/migration/document.ts +++ b/src/types/migration/document.ts @@ -50,11 +50,9 @@ type RegularFieldToMigrationField = : never) | TField -type GroupFieldToMigrationField< +export type GroupFieldToMigrationField< TField extends GroupField | Slice["items"] | SharedSlice["items"], -> = { - [Index in keyof TField]: FieldsToMigrationFields -} +> = FieldsToMigrationFields[] type SliceToMigrationField = Omit< TField, @@ -64,11 +62,10 @@ type SliceToMigrationField = Omit< items: GroupFieldToMigrationField } -type SliceZoneToMigrationField = { - [Index in keyof TField]: SliceToMigrationField -} +export type SliceZoneToMigrationField = + SliceToMigrationField[] -type FieldToMigrationField< +export type FieldToMigrationField< TField extends AnyRegularField | GroupField | SliceZone, > = TField extends AnyRegularField ? RegularFieldToMigrationField @@ -78,7 +75,7 @@ type FieldToMigrationField< ? SliceZoneToMigrationField : never -type FieldsToMigrationFields< +export type FieldsToMigrationFields< TFields extends Record, > = { [Key in keyof TFields]: FieldToMigrationField @@ -102,9 +99,9 @@ type MakeUIDOptional = * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} */ export type MigrationPrismicDocument< - TDocument extends PrismicDocument = PrismicDocument, + TDocuments extends PrismicDocument = PrismicDocument, > = - TDocument extends PrismicDocument + TDocuments extends PrismicDocument ? MakeUIDOptional<{ /** * Type of the document. @@ -115,7 +112,7 @@ export type MigrationPrismicDocument< * The unique identifier for the document. Guaranteed to be unique among * all Prismic documents of the same type. */ - uid: TDocument["uid"] + uid: TDocuments["uid"] /** * Language of document. @@ -129,7 +126,7 @@ export type MigrationPrismicDocument< * @internal */ // Made optional compared to the original type. - id?: TDocument["id"] + id?: TDocuments["id"] /** * Alternate language documents from Prismic content API. Used as a @@ -139,13 +136,13 @@ export type MigrationPrismicDocument< * @internal */ // Made optional compared to the original type. - alternate_languages?: TDocument["alternate_languages"] + alternate_languages?: TDocuments["alternate_languages"] /** * Tags associated with document. */ // Made optional compared to the original type. - tags?: TDocument["tags"] + tags?: TDocuments["tags"] /** * Data contained in the document. From ba31288f5479960c951ca33719fbbfade721995a Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 28 Aug 2024 17:34:41 +0200 Subject: [PATCH 08/61] feat: discover existing images automatically --- src/Migration.ts | 137 ++++++++++++++++++++++-- src/lib/migrationIsField.ts | 143 ++++++++++++++++++++++++++ src/lib/patchMigrationDocumentData.ts | 136 ++---------------------- 3 files changed, 277 insertions(+), 139 deletions(-) create mode 100644 src/lib/migrationIsField.ts diff --git a/src/Migration.ts b/src/Migration.ts index 198a1173..4da6aecd 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -1,6 +1,9 @@ +import * as is from "./lib/migrationIsField" + import type { Asset } from "./types/api/asset/asset" import type { MigrationAsset } from "./types/migration/asset" import type { + FieldsToMigrationFields, MigrationPrismicDocument, MigrationPrismicDocumentParams, } from "./types/migration/document" @@ -10,6 +13,84 @@ import { type MigrationLinkToMediaField, } from "./types/migration/fields" import type { PrismicDocument } from "./types/value/document" +import type { GroupField } from "./types/value/group" +import type { FilledImageFieldImage } from "./types/value/image" +import { LinkType } from "./types/value/link" +import type { FilledLinkToMediaField } from "./types/value/linkToMedia" +import { RichTextNodeType } from "./types/value/richText" +import type { SliceZone } from "./types/value/sliceZone" +import type { AnyRegularField } from "./types/value/types" + +import * as isFilled from "./helpers/isFilled" + +/** + * Discover assets in a record of Prismic fields. + * + * @param record - Record of Prismic fields to loook for assets in. + * @param onAsset - Callback that is called for each asset found. + */ +const discoverAssets = ( + record: FieldsToMigrationFields< + Record + >, + onAsset: (asset: FilledImageFieldImage | FilledLinkToMediaField) => void, +) => { + for (const field of Object.values(record)) { + if (is.sliceZone(field)) { + for (const slice of field) { + discoverAssets(slice.primary, onAsset) + for (const item of slice.items) { + discoverAssets(item, onAsset) + } + } + } else if (is.richText(field)) { + for (const node of field) { + if ("type" in node) { + if (node.type === RichTextNodeType.image) { + onAsset(node) + if ( + node.linkTo && + "link_type" in node.linkTo && + node.linkTo.link_type === LinkType.Media + ) { + onAsset(node.linkTo) + } + } else if (node.type !== RichTextNodeType.embed) { + for (const span of node.spans) { + if ( + span.type === "hyperlink" && + span.data && + "link_type" in span.data && + span.data.link_type === LinkType.Media + ) { + onAsset(span.data) + } + } + } + } + } + } else if (is.group(field)) { + for (const item of field) { + discoverAssets(item, onAsset) + } + } else if ( + is.image(field) && + field && + "dimensions" in field && + isFilled.image(field) + ) { + onAsset(field) + } else if ( + is.link(field) && + field && + "link_type" in field && + field.link_type === LinkType.Media && + isFilled.linkToMedia(field) + ) { + onAsset(field) + } + } +} /** * Extracts one or more Prismic document types that match a given Prismic @@ -62,7 +143,9 @@ export class Migration< constructor() {} - createAsset(asset: Asset): CreateAssetReturnType + createAsset( + asset: Asset | FilledImageFieldImage | FilledLinkToMediaField, + ): CreateAssetReturnType createAsset( file: MigrationAsset["file"], filename: MigrationAsset["filename"], @@ -74,7 +157,11 @@ export class Migration< }, ): CreateAssetReturnType createAsset( - fileOrAsset: MigrationAsset["file"] | Asset, + fileOrAsset: + | MigrationAsset["file"] + | Asset + | FilledImageFieldImage + | FilledLinkToMediaField, filename?: MigrationAsset["filename"], { notes, @@ -90,14 +177,40 @@ export class Migration< ): CreateAssetReturnType { let asset: MigrationAsset if (typeof fileOrAsset === "object" && "url" in fileOrAsset) { - asset = { - id: fileOrAsset.id, - file: fileOrAsset.url, - filename: fileOrAsset.filename, - notes: fileOrAsset.notes, - credits: fileOrAsset.credits, - alt: fileOrAsset.alt, - tags: fileOrAsset.tags?.map(({ name }) => name), + if ("dimensions" in fileOrAsset || "link_type" in fileOrAsset) { + const url = fileOrAsset.url.split("?")[0] + const filename = + "name" in fileOrAsset + ? fileOrAsset.name + : url.split("_").pop() || + fileOrAsset.alt?.slice(0, 500) || + "unknown" + const credits = + "copyright" in fileOrAsset && fileOrAsset.copyright + ? fileOrAsset.copyright + : undefined + const alt = + "alt" in fileOrAsset && fileOrAsset.alt ? fileOrAsset.alt : undefined + + asset = { + id: fileOrAsset.id, + file: fileOrAsset.url, + filename, + notes: undefined, + credits, + alt, + tags: undefined, + } + } else { + asset = { + id: fileOrAsset.id, + file: fileOrAsset.url, + filename: fileOrAsset.filename, + notes: fileOrAsset.notes, + credits: fileOrAsset.credits, + alt: fileOrAsset.alt, + tags: fileOrAsset.tags?.map(({ name }) => name), + } } } else { asset = { @@ -137,11 +250,15 @@ export class Migration< params: { documentName, ...params }, }) + // Index document if (!(document.type in this.#indexedDocuments)) { this.#indexedDocuments[document.type] = {} } this.#indexedDocuments[document.type][document.uid || SINGLE_KEY] = document + // Find other assets in document + discoverAssets(document.data, this.createAsset.bind(this)) + return document } diff --git a/src/lib/migrationIsField.ts b/src/lib/migrationIsField.ts new file mode 100644 index 00000000..e9499ef4 --- /dev/null +++ b/src/lib/migrationIsField.ts @@ -0,0 +1,143 @@ +import type { + FieldToMigrationField, + GroupFieldToMigrationField, + RichTextFieldToMigrationField, + SliceZoneToMigrationField, +} from "../types/migration/document" +import type { + MigrationImageField, + MigrationLinkField, +} from "../types/migration/fields" +import { MigrationFieldType } from "../types/migration/fields" +import type { GroupField } from "../types/value/group" +import type { ImageField } from "../types/value/image" +import type { LinkField } from "../types/value/link" +import { LinkType } from "../types/value/link" +import type { RichTextField } from "../types/value/richText" +import type { SliceZone } from "../types/value/sliceZone" +import type { AnyRegularField } from "../types/value/types" + +import * as isFilled from "../helpers/isFilled" + +/** + * Check if a field is a slice zone. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +export const sliceZone = ( + field: FieldToMigrationField, +): field is SliceZoneToMigrationField => { + return ( + Array.isArray(field) && + field.every((item) => "slice_type" in item && "id" in item) + ) +} + +/** + * Check if a field is a rich text field. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +export const richText = ( + field: FieldToMigrationField, +): field is RichTextFieldToMigrationField => { + return ( + Array.isArray(field) && + field.every( + (item) => + ("type" in item && typeof item.type === "string") || + ("migrationType" in item && + item.migrationType === MigrationFieldType.Image), + ) + ) +} + +/** + * Check if a field is a group field. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +export const group = ( + field: FieldToMigrationField, +): field is GroupFieldToMigrationField => { + return !sliceZone(field) && !richText(field) && Array.isArray(field) +} + +/** + * Check if a field is a link field. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +export const link = ( + field: FieldToMigrationField, +): field is MigrationLinkField | LinkField => { + if (typeof field === "function") { + // Lazy content relationship field + return true + } else if (field && typeof field === "object" && !("version" in field)) { + if ( + "migrationType" in field && + field.migrationType === MigrationFieldType.LinkToMedia + ) { + // Migration link to media field + return true + } else if ( + "type" in field && + "lang" in field && + typeof field.lang === "string" && + field.id + ) { + // Content relationship field declared using another repository document + return true + } else if ( + "link_type" in field && + (field.link_type === LinkType.Document || + field.link_type === LinkType.Media || + field.link_type === LinkType.Web) + ) { + // Regular link field + return true + } + } + + return false +} + +/** + * Check if a field is an image field. + * + * @remarks + * This is not an official helper function and it's only designed to work in the + * case of migration fields. + */ +export const image = ( + field: FieldToMigrationField, +): field is MigrationImageField | ImageField => { + if (field && typeof field === "object" && !("version" in field)) { + if ( + "migrationType" in field && + field.migrationType === MigrationFieldType.Image + ) { + // Migration image field + return true + } else if ( + "id" in field && + "dimensions" in field && + field.dimensions && + isFilled.image(field) + ) { + // Regular image field + return true + } + } + + return false +} diff --git a/src/lib/patchMigrationDocumentData.ts b/src/lib/patchMigrationDocumentData.ts index d7c9b8f2..434051ec 100644 --- a/src/lib/patchMigrationDocumentData.ts +++ b/src/lib/patchMigrationDocumentData.ts @@ -1,7 +1,6 @@ import type { Asset } from "../types/api/asset/asset" import type { MigrationAsset } from "../types/migration/asset" import type { - FieldToMigrationField, FieldsToMigrationFields, GroupFieldToMigrationField, MigrationPrismicDocument, @@ -29,6 +28,8 @@ import type { AnyRegularField } from "../types/value/types" import * as isFilled from "../helpers/isFilled" +import * as is from "./migrationIsField" + /** * A map of asset IDs to asset used to resolve assets when patching migration * Prismic documents. @@ -142,129 +143,6 @@ const inheritQueryParams = (url: string, original: string): string => { return `${url.split("?")[0]}${queryParams ? `?${queryParams}` : ""}` } -/** - * Check if a field is a slice zone. - * - * @remarks - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -const isSliceZone = ( - field: FieldToMigrationField, -): field is SliceZoneToMigrationField => { - return ( - Array.isArray(field) && - field.every((item) => "slice_type" in item && "id" in item) - ) -} - -/** - * Check if a field is a rich text field. - * - * @remarks - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -const isRichText = ( - field: FieldToMigrationField, -): field is RichTextFieldToMigrationField => { - return ( - Array.isArray(field) && - field.every( - (item) => - ("type" in item && typeof item.type === "string") || - ("migrationType" in item && - item.migrationType === MigrationFieldType.Image), - ) - ) -} - -/** - * Check if a field is a group field. - * - * @remarks - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -const isGroup = ( - field: FieldToMigrationField, -): field is GroupFieldToMigrationField => { - return !isSliceZone(field) && !isRichText(field) && Array.isArray(field) -} - -/** - * Check if a field is a link field. - * - * @remarks - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -const isLink = ( - field: FieldToMigrationField, -): field is MigrationLinkField | LinkField => { - if (typeof field === "function") { - // Lazy content relationship field - return true - } else if (field && typeof field === "object" && !("version" in field)) { - if ( - "migrationType" in field && - field.migrationType === MigrationFieldType.LinkToMedia - ) { - // Migration link to media field - return true - } else if ( - "type" in field && - "lang" in field && - typeof field.lang === "string" && - field.id - ) { - // Content relationship field declared using another repository document - return true - } else if ( - "link_type" in field && - (field.link_type === LinkType.Document || - field.link_type === LinkType.Media || - field.link_type === LinkType.Web) - ) { - // Regular link field - return true - } - } - - return false -} - -/** - * Check if a field is an image field. - * - * @remarks - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -const isImage = ( - field: FieldToMigrationField, -): field is MigrationImageField | ImageField => { - if (field && typeof field === "object" && !("version" in field)) { - if ( - "migrationType" in field && - field.migrationType === MigrationFieldType.Image - ) { - // Migration image field - return true - } else if ( - "id" in field && - "dimensions" in field && - field.dimensions && - isFilled.image(field) - ) { - // Regular image field - return true - } - } - - return false -} - /** * Patch a slice zone. */ @@ -546,15 +424,15 @@ const patchRecord = async < const result: Record = {} for (const [key, field] of Object.entries(record)) { - if (isSliceZone(field)) { + if (is.sliceZone(field)) { result[key] = await patchSliceZone(field, assets, documents) - } else if (isRichText(field)) { + } else if (is.richText(field)) { result[key] = await patchRichText(field, assets, documents) - } else if (isGroup(field)) { + } else if (is.group(field)) { result[key] = await patchGroup(field, assets, documents) - } else if (isLink(field)) { + } else if (is.link(field)) { result[key] = await patchLink(field, assets, documents) - } else if (isImage(field)) { + } else if (is.image(field)) { result[key] = patchImage(field, assets) } else { result[key] = field From f0d9c582d948a91c29470a0a1728a844cf7f9b25 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 28 Aug 2024 20:31:25 +0200 Subject: [PATCH 09/61] fix: handle edge cases and log API responses properly --- src/BaseClient.ts | 21 +++++++++++++++++---- src/Client.ts | 2 +- src/WriteClient.ts | 25 ++++++++++++++++++++----- src/lib/patchMigrationDocumentData.ts | 26 ++++++++++++++++---------- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/BaseClient.ts b/src/BaseClient.ts index fb05f2b7..0b807f7a 100644 --- a/src/BaseClient.ts +++ b/src/BaseClient.ts @@ -73,6 +73,7 @@ export interface ResponseLike { headers: HeadersLike // eslint-disable-next-line @typescript-eslint/no-explicit-any json(): Promise + text(): Promise blob(): Promise } @@ -159,6 +160,7 @@ type FetchJobResult = { status: number headers: HeadersLike json: TJSON + text?: string } export class BaseClient { @@ -306,16 +308,27 @@ export class BaseClient { // response. // eslint-disable-next-line @typescript-eslint/no-explicit-any let json: any = undefined - try { - json = await res.json() - } catch { - // noop + let text: string | undefined = undefined + if (res.ok) { + try { + json = await res.json() + } catch { + // noop + } + } else { + try { + text = await res.text() + json = JSON.stringify(text) + } catch { + // noop + } } return { status: res.status, headers: res.headers, json, + text, } }) } diff --git a/src/Client.ts b/src/Client.ts index c75463c8..d4193ee7 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1698,7 +1698,7 @@ export class Client< const res = await super.fetch(url, params) if (res.status !== 404 && res.status !== 429 && res.json == null) { - throw new PrismicError(undefined, url, res.json) + throw new PrismicError(undefined, url, res.json || res.text) } switch (res.status) { diff --git a/src/WriteClient.ts b/src/WriteClient.ts index df5af112..9a5e80d2 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -295,6 +295,10 @@ export class WriteClient< }, ) + // Index old ID for Prismic to Prismic migration + if (document.id) { + documents.set(document.id, { ...document, id }) + } documents.set(document, { ...document, id }) } } @@ -384,14 +388,14 @@ export class WriteClient< `Updating document \`${params.documentName}\` - ${++i}/${migration.documents.length}`, ) - const id = documents.get(document)!.id + const { id, uid } = documents.get(document)! const data = await patchMigrationDocumentData( document.data, assets, documents, ) - await this.updateDocument(id, { data }, fetchParams) + await this.updateDocument(id, { uid, data }, fetchParams) } reporter?.(`Updated ${i} documents`) @@ -488,7 +492,12 @@ export class WriteClient< const url = new URL("assets", this.assetAPIEndpoint) const formData = new FormData() - formData.append("file", new File([file], filename)) + formData.append( + "file", + new File([file], filename, { + type: file instanceof File ? file.type : undefined, + }), + ) if (notes) { formData.append("notes", notes) @@ -596,7 +605,7 @@ export class WriteClient< private async fetchForeignAsset( url: string, params: FetchParams = {}, - ): Promise { + ): Promise { const requestInit: RequestInitLike = { ...this.fetchOptions, ...params.fetchOptions, @@ -616,7 +625,11 @@ export class WriteClient< throw new PrismicError("Could not fetch foreign asset", url, undefined) } - return res.blob() + const blob = await res.blob() + + return new File([blob], "", { + type: res.headers.get("content-type") || "", + }) } private _resolveAssetTagIDsLimit = pLimit({ limit: 1 }) @@ -703,6 +716,7 @@ export class WriteClient< const result = await this.fetch( url.toString(), this.buildWriteQueryParams({ + isMigrationAPI: true, method: "POST", body: { title: documentName, @@ -735,6 +749,7 @@ export class WriteClient< await this.fetch( url.toString(), this.buildWriteQueryParams({ + isMigrationAPI: true, method: "PUT", body: { title: document.documentName, diff --git a/src/lib/patchMigrationDocumentData.ts b/src/lib/patchMigrationDocumentData.ts index 434051ec..94673ab8 100644 --- a/src/lib/patchMigrationDocumentData.ts +++ b/src/lib/patchMigrationDocumentData.ts @@ -123,9 +123,9 @@ const documentToContentRelationship = < return { link_type: LinkType.Document, id: document.id, - uid: document.uid ?? undefined, + uid: document.uid || undefined, type: document.type, - tags: document.tags ?? [], + tags: document.tags || [], lang: document.lang, url: undefined, slug: undefined, @@ -390,17 +390,23 @@ const patchImage = ( if (Object.keys(thumbnails).length > 0) { for (const name in thumbnails) { - const { url, dimensions, edit, alt } = ( + const maybeThumbnail = ( thumbnails as Record )[name] - result[name] = { - id: asset.id, - url: inheritQueryParams(asset.url, url), - dimensions, - edit, - alt: alt || null, - copyright: asset.credits || null, + if (is.image(maybeThumbnail)) { + const { url, dimensions, edit, alt } = ( + thumbnails as Record + )[name] + + result[name] = { + id: asset.id, + url: inheritQueryParams(asset.url, url), + dimensions, + edit, + alt: alt || null, + copyright: asset.credits || null, + } } } } From 8048717c9978edc6193164446ff9c11f5660ef55 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 28 Aug 2024 21:22:38 +0200 Subject: [PATCH 10/61] fix: use `JSON.parse` instead of `JSON.stringify` --- src/BaseClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaseClient.ts b/src/BaseClient.ts index 0b807f7a..bc3ab0fb 100644 --- a/src/BaseClient.ts +++ b/src/BaseClient.ts @@ -318,7 +318,7 @@ export class BaseClient { } else { try { text = await res.text() - json = JSON.stringify(text) + json = JSON.parse(text) } catch { // noop } From 04e5043501356adbb9e2110d2d3db80b189f78b8 Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 29 Aug 2024 18:56:29 +0200 Subject: [PATCH 11/61] docs: document new code --- src/Migration.ts | 45 ++- src/WriteClient.ts | 379 +++++++++++++----- src/index.ts | 14 +- ...igrationIsField.ts => isMigrationField.ts} | 49 ++- src/lib/patchMigrationDocumentData.ts | 193 +++++---- src/lib/toField.ts | 106 +++++ src/types/api/asset/asset.ts | 110 ++++- src/types/api/asset/tag.ts | 37 ++ src/types/api/migration/document.ts | 30 ++ src/types/migration/asset.ts | 2 +- src/types/migration/document.ts | 84 +++- src/types/migration/fields.ts | 18 +- src/types/migration/richText.ts | 14 +- test/types/migration-document.types.ts | 44 +- test/types/migration.types.ts | 20 +- 15 files changed, 832 insertions(+), 313 deletions(-) rename src/lib/{migrationIsField.ts => isMigrationField.ts} (77%) create mode 100644 src/lib/toField.ts diff --git a/src/Migration.ts b/src/Migration.ts index 4da6aecd..49340407 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -1,16 +1,16 @@ -import * as is from "./lib/migrationIsField" +import * as is from "./lib/isMigrationField" import type { Asset } from "./types/api/asset/asset" import type { MigrationAsset } from "./types/migration/asset" import type { FieldsToMigrationFields, - MigrationPrismicDocument, - MigrationPrismicDocumentParams, + PrismicMigrationDocument, + PrismicMigrationDocumentParams, } from "./types/migration/document" import { + type ImageMigrationField, + type LinkToMediaMigrationField, MigrationFieldType, - type MigrationImageField, - type MigrationLinkToMediaField, } from "./types/migration/fields" import type { PrismicDocument } from "./types/value/document" import type { GroupField } from "./types/value/group" @@ -102,16 +102,16 @@ const discoverAssets = ( * @typeParam TType - Type(s) to match `TMigrationDocuments` against. */ type ExtractMigrationDocumentType< - TMigrationDocuments extends MigrationPrismicDocument, + TMigrationDocuments extends PrismicMigrationDocument, TType extends TMigrationDocuments["type"], > = Extract extends never ? TMigrationDocuments : Extract -type CreateAssetReturnType = MigrationImageField & { - image: MigrationImageField - linkToMedia: MigrationLinkToMediaField +type CreateAssetReturnType = ImageMigrationField & { + image: ImageMigrationField + linkToMedia: LinkToMediaMigrationField } const SINGLE_KEY = "__SINGLE__" @@ -125,14 +125,14 @@ const SINGLE_KEY = "__SINGLE__" export class Migration< TDocuments extends PrismicDocument = PrismicDocument, TMigrationDocuments extends - MigrationPrismicDocument = MigrationPrismicDocument, + PrismicMigrationDocument = PrismicMigrationDocument, > { /** * @internal */ documents: { document: TMigrationDocuments - params: MigrationPrismicDocumentParams + params: PrismicMigrationDocumentParams }[] = [] #indexedDocuments: Record> = {} @@ -194,7 +194,7 @@ export class Migration< asset = { id: fileOrAsset.id, - file: fileOrAsset.url, + file: url, filename, notes: undefined, credits, @@ -224,7 +224,22 @@ export class Migration< } } - this.assets.set(asset.id, asset) + const maybeAsset = this.assets.get(asset.id) + + if (maybeAsset) { + // Consolidate existing asset with new asset value if possible + this.assets.set(asset.id, { + ...maybeAsset, + notes: asset.notes || maybeAsset.notes, + credits: asset.credits || maybeAsset.credits, + alt: asset.alt || maybeAsset.alt, + tags: Array.from( + new Set([...(maybeAsset.tags || []), ...(asset.tags || [])]), + ), + }) + } else { + this.assets.set(asset.id, asset) + } return { migrationType: MigrationFieldType.Image, @@ -242,8 +257,8 @@ export class Migration< createDocument( document: ExtractMigrationDocumentType, - documentName: MigrationPrismicDocumentParams["documentName"], - params: Omit = {}, + documentName: PrismicMigrationDocumentParams["documentName"], + params: Omit = {}, ): ExtractMigrationDocumentType { this.documents.push({ document, diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 9a5e80d2..fe17c863 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -24,8 +24,8 @@ import { type PutDocumentParams, } from "./types/api/migration/document" import type { - MigrationPrismicDocument, - MigrationPrismicDocumentParams, + PrismicMigrationDocument, + PrismicMigrationDocumentParams, } from "./types/migration/document" import type { PrismicDocument } from "./types/value/document" @@ -35,6 +35,8 @@ import type { FetchParams, RequestInitLike } from "./BaseClient" import { Client } from "./Client" import type { ClientConfig } from "./Client" import type { Migration } from "./Migration" +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { createMigration } from "./createMigration" /** * Extracts one or more Prismic document types that match a given Prismic @@ -52,10 +54,29 @@ type ExtractDocumentType< ? TDocuments : Extract +/** + * A query response from the Prismic asset API. The response contains pagination + * metadata and a list of matching results for the query. + */ type GetAssetsReturnType = { + /** + * Found assets for the query. + */ results: Asset[] + + /** + * Total number of assets found for the query. + */ total_results_size: number + + /** + * IDs of assets that were not found when filtering by IDs. + */ missing_ids?: string[] + + /** + * A function to fectch the next page of assets if available. + */ next?: () => Promise } @@ -120,6 +141,15 @@ export class WriteClient< } } + /** + * Creates a migration release on the Prismic repository based on the provided + * prepared migration. + * + * @param migration - A migration prepared with {@link createMigration}. + * @param params - An event listener and additional fetch parameters. + * + * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + */ async migrate( migration: Migration, { @@ -147,6 +177,16 @@ export class WriteClient< ) } + /** + * Creates assets in the Prismic repository's media library. + * + * @param migration - A migration prepared with {@link createMigration}. + * @param params - An event listener and additional fetch parameters. + * + * @returns A map of assets available in the Prismic repository. + * + * @internal This method is one of the step performed by the {@link migrate} method. + */ private async migrateCreateAssets( migration: Migration, { @@ -225,6 +265,16 @@ export class WriteClient< return assets } + /** + * Creates documents in the Prismic repository's migration release. + * + * @param migration - A migration prepared with {@link createMigration}. + * @param params - An event listener and additional fetch parameters. + * + * @returns A map of documents available in the Prismic repository. + * + * @internal This method is one of the step performed by the {@link migrate} method. + */ private async migrateCreateDocuments( migration: Migration, { @@ -250,31 +300,28 @@ export class WriteClient< documents.set(document.id, document) } - const masterLocaleDocuments: { - document: MigrationPrismicDocument - params: MigrationPrismicDocumentParams - }[] = [] - const nonMasterLocaleDocuments: { - document: MigrationPrismicDocument - params: MigrationPrismicDocumentParams + const sortedDocuments: { + document: PrismicMigrationDocument + params: PrismicMigrationDocumentParams }[] = [] + // We create an array with non-master locale documents last because + // we need their master locale document to be created first. for (const { document, params } of migration.documents) { if (document.lang === masterLocale) { - masterLocaleDocuments.push({ document, params }) + sortedDocuments.unshift({ document, params }) } else { - nonMasterLocaleDocuments.push({ document, params }) + sortedDocuments.push({ document, params }) } } - // Create master locale documents first let i = 0 let created = 0 - if (masterLocaleDocuments.length) { - for (const { document, params } of masterLocaleDocuments) { + if (sortedDocuments.length) { + for (const { document, params } of sortedDocuments) { if (document.id && documents.has(document.id)) { reporter?.( - `Skipping existing master locale document \`${params.documentName}\` - ${++i}/${masterLocaleDocuments.length}`, + `Skipping existing document \`${params.documentName}\` - ${++i}/${sortedDocuments.length}`, ) // Index the migration document @@ -282,70 +329,30 @@ export class WriteClient< } else { created++ reporter?.( - `Creating master locale document \`${params.documentName}\` - ${++i}/${masterLocaleDocuments.length}`, + `Creating document \`${params.documentName}\` - ${++i}/${sortedDocuments.length}`, ) - const { id } = await this.createDocument( - // We'll upload docuements data later on. - { ...document, data: {} }, - params.documentName, - { - ...params, - ...fetchParams, - }, - ) - - // Index old ID for Prismic to Prismic migration - if (document.id) { - documents.set(document.id, { ...document, id }) - } - documents.set(document, { ...document, id }) - } - } - } - - if (created > 0) { - reporter?.(`Created ${created} master locale documents`) - } else { - reporter?.(`No master locale documents to create`) - } - - // Create non-master locale documents - i = 0 - created = 0 - if (nonMasterLocaleDocuments.length) { - for (const { document, params } of nonMasterLocaleDocuments) { - if (document.id && documents.has(document.id)) { - reporter?.( - `Skipping existing non-master locale document \`${params.documentName}\` - ${++i}/${nonMasterLocaleDocuments.length}`, - ) - - documents.set(document, documents.get(document.id)!) - } else { - created++ - reporter?.( - `Creating non-master locale document \`${params.documentName}\` - ${++i}/${nonMasterLocaleDocuments.length}`, - ) - - // Resolve master language document ID + // Resolve master language document ID for non-master locale documents let masterLanguageDocumentID: string | undefined - if (params.masterLanguageDocument) { - if (typeof params.masterLanguageDocument === "function") { - const masterLanguageDocument = - await params.masterLanguageDocument() - - if (masterLanguageDocument) { - masterLanguageDocumentID = documents.get( - masterLanguageDocument, - )?.id + if (document.lang !== masterLocale) { + if (params.masterLanguageDocument) { + if (typeof params.masterLanguageDocument === "function") { + const masterLanguageDocument = + await params.masterLanguageDocument() + + if (masterLanguageDocument) { + masterLanguageDocumentID = documents.get( + masterLanguageDocument, + )?.id + } + } else { + masterLanguageDocumentID = params.masterLanguageDocument.id } - } else { - masterLanguageDocumentID = params.masterLanguageDocument.id + } else if (document.alternate_languages) { + masterLanguageDocumentID = document.alternate_languages.find( + ({ lang }) => lang === masterLocale, + )?.id } - } else if (document.alternate_languages) { - masterLanguageDocumentID = document.alternate_languages.find( - ({ lang }) => lang === masterLocale, - )?.id } const { id } = await this.createDocument( @@ -358,20 +365,35 @@ export class WriteClient< }, ) + // Index old ID for Prismic to Prismic migration + if (document.id) { + documents.set(document.id, { ...document, id }) + } documents.set(document, { ...document, id }) } } } if (created > 0) { - reporter?.(`Created ${created} non-master locale documents`) + reporter?.(`Created ${created} documents`) } else { - reporter?.(`No non-master locale documents to create`) + reporter?.(`No documents to create`) } return documents } + /** + * Updates documents in the Prismic repository's migration release with their + * patched data. + * + * @param migration - A migration prepared with {@link createMigration}. + * @param assets - A map of assets available in the Prismic repository. + * @param documents - A map of documents available in the Prismic repository. + * @param params - An event listener and additional fetch parameters. + * + * @internal This method is one of the step performed by the {@link migrate} method. + */ private async migrateUpdateDocuments( migration: Migration, assets: AssetMap, @@ -404,6 +426,13 @@ export class WriteClient< } } + /** + * Queries assets from the Prismic repository's media library. + * + * @param params - Parameters to filter, sort, and paginate results. + * + * @returns A paginated response containing the result of the query. + */ async getAssets({ pageSize, cursor, @@ -451,7 +480,7 @@ export class WriteClient< cursor: nextCursor, } = await this.fetch( url.toString(), - this.buildWriteQueryParams({ params }), + this.buildAssetAPIQueryParams({ params }), ) return { @@ -473,6 +502,15 @@ export class WriteClient< } } + /** + * Creates an asset in the Prismic media library. + * + * @param file - The file to upload as an asset. + * @param filename - The filename of the asset. + * @param params - Additional asset data and fetch parameters. + * + * @returns The created asset. + */ private async createAsset( file: PostAssetParams["file"] | File, filename: string, @@ -513,7 +551,7 @@ export class WriteClient< const asset = await this.fetch( url.toString(), - this.buildWriteQueryParams({ + this.buildAssetAPIQueryParams({ method: "POST", body: formData, params, @@ -527,6 +565,14 @@ export class WriteClient< return asset } + /** + * Updates an asset in the Prismic media library. + * + * @param id - The ID of the asset to update. + * @param params - The asset data to update and additional fetch parameters. + * + * @returns The updated asset. + */ private async updateAsset( id: string, { @@ -550,7 +596,7 @@ export class WriteClient< return this.fetch( url.toString(), - this.buildWriteQueryParams({ + this.buildAssetAPIQueryParams({ method: "PATCH", body: { notes, @@ -567,6 +613,12 @@ export class WriteClient< // We don't want to expose those utilities for now, // and we don't have any internal use for them yet. + /** + * Deletes an asset from the Prismic media library. + * + * @param assetOrID - The asset or ID of the asset to delete. + * @param params - Additional fetch parameters. + */ // private async deleteAsset( // assetOrID: string | Asset, // params?: FetchParams, @@ -578,10 +630,16 @@ export class WriteClient< // await this.fetch( // url.toString(), - // this.buildWriteQueryParams({ method: "DELETE", params }), + // this.buildAssetAPIQueryParams({ method: "DELETE", params }), // ) // } + /** + * Deletes multiple assets from the Prismic media library. + * + * @param assetsOrIDs - An array of asset IDs or assets to delete. + * @param params - Additional fetch parameters. + */ // private async deleteAssets( // assetsOrIDs: (string | Asset)[], // params?: FetchParams, @@ -590,7 +648,7 @@ export class WriteClient< // await this.fetch( // url.toString(), - // this.buildWriteQueryParams({ + // this.buildAssetAPIQueryParams({ // method: "POST", // body: { // ids: assetsOrIDs.map((assetOrID) => @@ -602,6 +660,14 @@ export class WriteClient< // ) // } + /** + * Fetches a foreign asset from a URL. + * + * @param url - The URL of the asset to fetch. + * @param params - Additional fetch parameters. + * + * @returns A file representing the fetched asset. + */ private async fetchForeignAsset( url: string, params: FetchParams = {}, @@ -632,7 +698,20 @@ export class WriteClient< }) } + /** + * {@link resolveAssetTagIDs} rate limiter. + */ private _resolveAssetTagIDsLimit = pLimit({ limit: 1 }) + + /** + * Resolves asset tag IDs from tag names or IDs. + * + * @param tagNamesOrIDs - An array of tag names or IDs to resolve. + * @param params - Whether or not missing tags should be created and + * additional fetch parameters. + * + * @returns An array of resolved tag IDs. + */ private async resolveAssetTagIDs( tagNamesOrIDs: string[] = [], { createTags, ...params }: { createTags?: boolean } & FetchParams = {}, @@ -676,6 +755,17 @@ export class WriteClient< }) } + /** + * Creates a tag in the asset API. + * + * @remarks + * Tags should be at least 3 characters long and 20 characters at most. + * + * @param name - The name of the tag to create. + * @param params - Additional fetch parameters. + * + * @returns The created tag. + */ private async createAssetTag( name: string, params?: FetchParams, @@ -684,7 +774,7 @@ export class WriteClient< return this.fetch( url.toString(), - this.buildWriteQueryParams({ + this.buildAssetAPIQueryParams({ method: "POST", body: { name }, params, @@ -692,20 +782,42 @@ export class WriteClient< ) } + /** + * Queries existing tags from the asset API. + * + * @param params - Additional fetch parameters. + * + * @returns An array of existing tags. + */ private async getAssetTags(params?: FetchParams): Promise { const url = new URL("tags", this.assetAPIEndpoint) const { items } = await this.fetch( url.toString(), - this.buildWriteQueryParams({ params }), + this.buildAssetAPIQueryParams({ params }), ) return items } + /** + * Creates a document in the repository's migration release. + * + * @typeParam TType - Type of Prismic documents to create. + * + * @param document - The document data to create. + * @param documentName - The name of the document to create which will be + * displayed in the editor. + * @param params - Document master language document ID and additional fetch + * parameters. + * + * @returns The ID of the created document. + * + * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + */ private async createDocument( - document: MigrationPrismicDocument>, - documentName: MigrationPrismicDocumentParams["documentName"], + document: PrismicMigrationDocument>, + documentName: PrismicMigrationDocumentParams["documentName"], { masterLanguageDocumentID, ...params @@ -715,8 +827,7 @@ export class WriteClient< const result = await this.fetch( url.toString(), - this.buildWriteQueryParams({ - isMigrationAPI: true, + this.buildMigrationAPIQueryParams({ method: "POST", body: { title: documentName, @@ -734,13 +845,24 @@ export class WriteClient< return { id: result.id } } + /** + * Updates an existing document in the repository's migration release. + * + * @typeParam TType - Type of Prismic documents to update. + * + * @param id - The ID of the document to update. + * @param document - The document data to update. + * @param params - Additional fetch parameters. + * + * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + */ private async updateDocument( id: string, document: Pick< - MigrationPrismicDocument>, + PrismicMigrationDocument>, "uid" | "tags" | "data" > & { - documentName?: MigrationPrismicDocumentParams["documentName"] + documentName?: PrismicMigrationDocumentParams["documentName"] }, params?: FetchParams, ): Promise { @@ -748,8 +870,7 @@ export class WriteClient< await this.fetch( url.toString(), - this.buildWriteQueryParams({ - isMigrationAPI: true, + this.buildMigrationAPIQueryParams({ method: "PUT", body: { title: document.documentName, @@ -762,15 +883,71 @@ export class WriteClient< ) } - private buildWriteQueryParams>({ + /** + * Builds fetch parameters for the asset API. + * + * @typeParam TBody - Type of the body to send in the fetch request. + * + * @param params - Method, body, and additional fetch parameters. + * + * @returns An object that can be fetched to interact with the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ + private buildAssetAPIQueryParams>({ method, body, - isMigrationAPI, params, }: { method?: string body?: TBody - isMigrationAPI?: boolean + params?: FetchParams + }): FetchParams { + const headers: Record = { + ...params?.fetchOptions?.headers, + authorization: `Bearer ${this.writeToken}`, + repository: this.repositoryName, + } + + let _body: FormData | string | undefined + if (body instanceof FormData) { + _body = body + } else if (body) { + _body = JSON.stringify(body) + headers["content-type"] = "application/json" + } + + return { + ...params, + fetchOptions: { + ...params?.fetchOptions, + method, + body: _body, + headers, + }, + } + } + + /** + * Builds fetch parameters for the migration API. + * + * @typeParam TBody - Type of the body to send in the fetch request. + * + * @param params - Method, body, and additional fetch options. + * + * @returns An object that can be fetched to interact with the migration API. + * + * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + */ + private buildMigrationAPIQueryParams< + TBody extends PostDocumentParams | PutDocumentParams, + >({ + method, + body, + params, + }: { + method?: string + body: TBody params?: FetchParams }): FetchParams { return { @@ -778,19 +955,13 @@ export class WriteClient< fetchOptions: { ...params?.fetchOptions, method, - body: body - ? body instanceof FormData - ? body - : JSON.stringify(body) - : undefined, + body: JSON.stringify(body), headers: { ...params?.fetchOptions?.headers, - authorization: `Bearer ${this.writeToken}`, + "content-type": "application/json", repository: this.repositoryName, - ...(body instanceof FormData - ? {} - : { "content-type": "application/json" }), - ...(isMigrationAPI ? { "x-api-key": this.migrationAPIKey } : {}), + authorization: `Bearer ${this.writeToken}`, + "x-api-key": this.migrationAPIKey, }, }, } diff --git a/src/index.ts b/src/index.ts index e728600d..da47d444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -329,20 +329,20 @@ export type { export { MigrationFieldType } from "./types/migration/fields" export type { - MigrationPrismicDocument, + PrismicMigrationDocument, RichTextFieldToMigrationField, } from "./types/migration/document" export type { - MigrationImageField, - MigrationLinkToMediaField, - MigrationContentRelationshipField, - MigrationLinkField, + ImageMigrationField, + LinkToMediaMigrationField, + ContentRelationshipMigrationField, + LinkMigrationField, } from "./types/migration/fields" export type { - MigrationRTImageNode, - MigrationRTLinkNode, + RTImageMigrationNode, + RTLinkMigrationNode, } from "./types/migration/richText" // API - Types representing Prismic Rest API V2 responses. diff --git a/src/lib/migrationIsField.ts b/src/lib/isMigrationField.ts similarity index 77% rename from src/lib/migrationIsField.ts rename to src/lib/isMigrationField.ts index e9499ef4..9219a4ab 100644 --- a/src/lib/migrationIsField.ts +++ b/src/lib/isMigrationField.ts @@ -5,8 +5,8 @@ import type { SliceZoneToMigrationField, } from "../types/migration/document" import type { - MigrationImageField, - MigrationLinkField, + ImageMigrationField, + LinkMigrationField, } from "../types/migration/fields" import { MigrationFieldType } from "../types/migration/fields" import type { GroupField } from "../types/value/group" @@ -20,9 +20,14 @@ import type { AnyRegularField } from "../types/value/types" import * as isFilled from "../helpers/isFilled" /** - * Check if a field is a slice zone. + * Checks if a field is a slice zone. * - * @remarks + * @param field - Field to check. + * + * @returns `true` if `field` is a slice zone migration field, `false` + * otherwise. + * + * @internal * This is not an official helper function and it's only designed to work in the * case of migration fields. */ @@ -36,9 +41,13 @@ export const sliceZone = ( } /** - * Check if a field is a rich text field. + * Checks if a field is a rich text field. + * + * @param field - Field to check. * - * @remarks + * @returns `true` if `field` is a rich text migration field, `false` otherwise. + * + * @internal * This is not an official helper function and it's only designed to work in the * case of migration fields. */ @@ -57,9 +66,13 @@ export const richText = ( } /** - * Check if a field is a group field. + * Checks if a field is a group field. + * + * @param field - Field to check. * - * @remarks + * @returns `true` if `field` is a group migration field, `false` otherwise. + * + * @internal * This is not an official helper function and it's only designed to work in the * case of migration fields. */ @@ -70,15 +83,19 @@ export const group = ( } /** - * Check if a field is a link field. + * Checks if a field is a link field. + * + * @param field - Field to check. + * + * @returns `true` if `field` is a link migration field, `false` otherwise. * - * @remarks + * @internal * This is not an official helper function and it's only designed to work in the * case of migration fields. */ export const link = ( field: FieldToMigrationField, -): field is MigrationLinkField | LinkField => { +): field is LinkMigrationField | LinkField => { if (typeof field === "function") { // Lazy content relationship field return true @@ -112,15 +129,19 @@ export const link = ( } /** - * Check if a field is an image field. + * Checks if a field is an image field. + * + * @param field - Field to check. + * + * @returns `true` if `field` is an image migration field, `false` otherwise. * - * @remarks + * @internal * This is not an official helper function and it's only designed to work in the * case of migration fields. */ export const image = ( field: FieldToMigrationField, -): field is MigrationImageField | ImageField => { +): field is ImageMigrationField | ImageField => { if (field && typeof field === "object" && !("version" in field)) { if ( "migrationType" in field && diff --git a/src/lib/patchMigrationDocumentData.ts b/src/lib/patchMigrationDocumentData.ts index 94673ab8..6c1d520f 100644 --- a/src/lib/patchMigrationDocumentData.ts +++ b/src/lib/patchMigrationDocumentData.ts @@ -3,23 +3,21 @@ import type { MigrationAsset } from "../types/migration/asset" import type { FieldsToMigrationFields, GroupFieldToMigrationField, - MigrationPrismicDocument, + PrismicMigrationDocument, RichTextFieldToMigrationField, SliceZoneToMigrationField, } from "../types/migration/document" import type { - MigrationImageField, - MigrationLinkField, + ImageMigrationField, + LinkMigrationField, } from "../types/migration/fields" import { MigrationFieldType } from "../types/migration/fields" -import type { FilledContentRelationshipField } from "../types/value/contentRelationship" import type { PrismicDocument } from "../types/value/document" import type { GroupField, NestedGroupField } from "../types/value/group" import type { FilledImageFieldImage, ImageField } from "../types/value/image" import { LinkType } from "../types/value/link" import type { LinkField } from "../types/value/link" -import type { LinkToMediaField } from "../types/value/linkToMedia" -import type { RTImageNode, RTInlineNode } from "../types/value/richText" +import type { RTInlineNode } from "../types/value/richText" import { type RichTextField, RichTextNodeType } from "../types/value/richText" import type { SharedSlice } from "../types/value/sharedSlice" import type { Slice } from "../types/value/slice" @@ -28,7 +26,8 @@ import type { AnyRegularField } from "../types/value/types" import * as isFilled from "../helpers/isFilled" -import * as is from "./migrationIsField" +import * as is from "./isMigrationField" +import * as to from "./toField" /** * A map of asset IDs to asset used to resolve assets when patching migration @@ -43,99 +42,27 @@ export type AssetMap = Map * relationship field used to resolve content relationships when patching * migration Prismic documents. * + * @typeParam TDocuments - Type of Prismic documents in the repository. + * * @internal */ export type DocumentMap = Map< | string | TDocuments - | MigrationPrismicDocument - | MigrationPrismicDocument, + | PrismicMigrationDocument + | PrismicMigrationDocument, | PrismicDocument - | (Omit, "id"> & { id: string }) + | (Omit, "id"> & { id: string }) > /** - * Convert an asset to an image field. - */ -const assetToImageField = ({ - id, - url, - width, - height, - alt, - credits, -}: Asset): ImageField => { - return { - id, - url, - dimensions: { width: width!, height: height! }, - edit: { x: 0, y: 0, zoom: 0, background: "transparent" }, - alt: alt || null, - copyright: credits || null, - } -} - -/** - * Convert an asset to a link to media field. - */ -const assetToLinkToMediaField = ({ - id, - filename, - kind, - url, - size, - width, - height, -}: Asset): LinkToMediaField<"filled"> => { - return { - id, - link_type: LinkType.Media, - name: filename, - kind, - url, - size: `${size}`, - width: width ? `${width}` : undefined, - height: height ? `${height}` : undefined, - } -} - -/** - * Convert an asset to an RT image node. - */ -const assetToRTImageNode = (asset: Asset): RTImageNode => { - return { - ...assetToImageField(asset), - type: RichTextNodeType.image, - } -} - -/** - * Convert a document to a content relationship field. - */ -const documentToContentRelationship = < - TDocuments extends PrismicDocument = PrismicDocument, ->( - document: - | TDocuments - | (Omit & { id: string }), -): FilledContentRelationshipField => { - return { - link_type: LinkType.Document, - id: document.id, - uid: document.uid || undefined, - type: document.type, - tags: document.tags || [], - lang: document.lang, - url: undefined, - slug: undefined, - isBroken: false, - data: undefined, - } -} - -/** - * Inherit query parameters from an original URL to a new URL. + * Inherits query parameters from an original URL to a new URL. + * + * @param url - The new URL. + * @param original - The original URL to inherit query parameters from. + * + * @returns The new URL with query parameters inherited from the original URL. */ const inheritQueryParams = (url: string, original: string): string => { const queryParams = original.split("?")[1] || "" @@ -144,7 +71,15 @@ const inheritQueryParams = (url: string, original: string): string => { } /** - * Patch a slice zone. + * Patches references in a slice zone field. + * + * @typeParam TDocuments - Type of Prismic documents in the repository. + * + * @param sliceZone - The slice zone to patch. + * @param assets - A map of assets available in the Prismic repository. + * @param documents - A map of documents available in the Prismic repository. + * + * @returns The patched slice zone. */ const patchSliceZone = async < TDocuments extends PrismicDocument = PrismicDocument, @@ -177,7 +112,15 @@ const patchSliceZone = async < } /** - * Patch a rich text field. + * Patches references in a rich text field. + * + * @typeParam TDocuments - Type of Prismic documents in the repository. + * + * @param richText - The rich text field to patch. + * @param assets - A map of assets available in the Prismic repository. + * @param documents - A map of documents available in the Prismic repository. + * + * @returns The patched rich text field. */ const patchRichText = async < TDocuments extends PrismicDocument = PrismicDocument, @@ -234,7 +177,7 @@ const patchRichText = async < if (asset) { result.push({ - ...assetToRTImageNode(asset), + ...to.rtImageNode(asset), linkTo: isFilled.link(linkTo) ? linkTo : undefined, }) } @@ -245,7 +188,15 @@ const patchRichText = async < } /** - * Patch a group field. + * Patches references in a group field. + * + * @typeParam TDocuments - Type of Prismic documents in the repository. + * + * @param group - The group field to patch. + * @param assets - A map of assets available in the Prismic repository. + * @param documents - A map of documents available in the Prismic repository. + * + * @returns The patched group field. */ const patchGroup = async < TMigrationGroup extends GroupFieldToMigrationField< @@ -280,10 +231,18 @@ const patchGroup = async < } /** - * Patch a link field. + * Patches references in a link field. + * + * @typeParam TDocuments - Type of Prismic documents in the repository. + * + * @param link - The link field to patch. + * @param assets - A map of assets available in the Prismic repository. + * @param documents - A map of documents available in the Prismic repository. + * + * @returns The patched link field. */ const patchLink = async ( - link: MigrationLinkField | LinkField, + link: LinkMigrationField | LinkField, assets: AssetMap, documents: DocumentMap, ): Promise => { @@ -296,14 +255,14 @@ const patchLink = async ( const maybeRelationship = documents.get(resolved) if (maybeRelationship) { - return documentToContentRelationship(maybeRelationship) + return to.contentRelationship(maybeRelationship) } } } else if ("migrationType" in link) { // Migration link field const asset = assets.get(link.id) if (asset) { - return assetToLinkToMediaField(asset) + return to.linkToMediaField(asset) } } else if ("link_type" in link) { switch (link.link_type) { @@ -334,7 +293,7 @@ const patchLink = async ( const maybeRelationship = documents.get(link.id) if (maybeRelationship) { - return documentToContentRelationship(maybeRelationship) + return to.contentRelationship(maybeRelationship) } } } @@ -345,10 +304,15 @@ const patchLink = async ( } /** - * Patch an image field. + * Patches references in an image field. + * + * @param image - The image field to patch. + * @param assets - A map of assets available in the Prismic repository. + * + * @returns The patched image field. */ const patchImage = ( - image: MigrationImageField | ImageField, + image: ImageMigrationField | ImageField, assets: AssetMap, ): ImageField => { if (image) { @@ -359,7 +323,7 @@ const patchImage = ( // Migration image field const asset = assets.get(image.id) if (asset) { - return assetToImageField(asset) + return to.imageField(asset) } } else if ( "dimensions" in image && @@ -419,6 +383,18 @@ const patchImage = ( return {} } +/** + * Patches references in a record of Prismic field. + * + * @typeParam TFields - Type of the record of Prismic fields. + * @typeParam TDocuments - Type of Prismic documents in the repository. + * + * @param record - The link field to patch. + * @param assets - A map of assets available in the Prismic repository. + * @param documents - A map of documents available in the Prismic repository. + * + * @returns The patched record. + */ const patchRecord = async < TFields extends Record, TDocuments extends PrismicDocument = PrismicDocument, @@ -448,10 +424,21 @@ const patchRecord = async < return result as TFields } +/** + * Patches references in a document's data. + * + * @typeParam TDocuments - Type of Prismic documents in the repository. + * + * @param data - The document data to patch. + * @param assets - A map of assets available in the Prismic repository. + * @param documents - A map of documents available in the Prismic repository. + * + * @returns The patched document data. + */ export const patchMigrationDocumentData = async < TDocuments extends PrismicDocument = PrismicDocument, >( - data: MigrationPrismicDocument["data"], + data: PrismicMigrationDocument["data"], assets: AssetMap, documents: DocumentMap, ): Promise => { diff --git a/src/lib/toField.ts b/src/lib/toField.ts new file mode 100644 index 00000000..84035a43 --- /dev/null +++ b/src/lib/toField.ts @@ -0,0 +1,106 @@ +import type { Asset } from "../types/api/asset/asset" +import type { PrismicMigrationDocument } from "../types/migration/document" +import type { FilledContentRelationshipField } from "../types/value/contentRelationship" +import type { PrismicDocument } from "../types/value/document" +import type { ImageField } from "../types/value/image" +import { LinkType } from "../types/value/link" +import type { LinkToMediaField } from "../types/value/linkToMedia" +import type { RTImageNode } from "../types/value/richText" +import { RichTextNodeType } from "../types/value/richText" + +/** + * Converts an asset to an image field. + * + * @param asset - Asset to convert. + * + * @returns Equivalent image field. + */ +export const imageField = ({ + id, + url, + width, + height, + alt, + credits, +}: Asset): ImageField => { + return { + id, + url, + dimensions: { width: width!, height: height! }, + edit: { x: 0, y: 0, zoom: 0, background: "transparent" }, + alt: alt || null, + copyright: credits || null, + } +} + +/** + * Converts an asset to a link to media field. + * + * @param asset - Asset to convert. + * + * @returns Equivalent link to media field. + */ +export const linkToMediaField = ({ + id, + filename, + kind, + url, + size, + width, + height, +}: Asset): LinkToMediaField<"filled"> => { + return { + id, + link_type: LinkType.Media, + name: filename, + kind, + url, + size: `${size}`, + width: width ? `${width}` : undefined, + height: height ? `${height}` : undefined, + } +} + +/** + * Converts an asset to an RT image node. + * + * @param asset - Asset to convert. + * + * @returns Equivalent rich text image node. + */ +export const rtImageNode = (asset: Asset): RTImageNode => { + return { + ...imageField(asset), + type: RichTextNodeType.image, + } +} + +/** + * Converts a document to a content relationship field. + * + * @typeParam TDocuments - Type of Prismic documents in the repository. + * + * @param document - Document to convert. + * + * @returns Equivalent content relationship. + */ +export const contentRelationship = < + TDocuments extends PrismicDocument = PrismicDocument, +>( + document: + | TDocuments + | (Omit & { id: string }), +): FilledContentRelationshipField => { + return { + link_type: LinkType.Document, + id: document.id, + uid: document.uid || undefined, + type: document.type, + tags: document.tags || [], + lang: document.lang, + url: undefined, + slug: undefined, + isBroken: false, + data: undefined, + } +} diff --git a/src/types/api/asset/asset.ts b/src/types/api/asset/asset.ts index 77fac0ca..29c89435 100644 --- a/src/types/api/asset/asset.ts +++ b/src/types/api/asset/asset.ts @@ -1,7 +1,7 @@ import type { Tag } from "./tag" /** - * Asset types + * Asset types. */ export const AssetType = { All: "all", @@ -11,28 +11,83 @@ export const AssetType = { Video: "video", } as const +/** + * An object representing an asset returned by the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type Asset = { + /** + * Asset ID. + */ id: string + + /** + * Asset URL. + */ url: string + /** + * Asset creation date. + */ created_at: number + + /** + * Asset last modification date. + */ last_modified: number + /** + * Asset filename. + */ filename: string + + /** + * Asset extension. + */ extension: string + + /** + * Asset size in bytes. + */ size: number + + /** + * Asset kind. + */ kind: Exclude< (typeof AssetType)[keyof typeof AssetType], (typeof AssetType)["All"] > + /** + * Asset width in pixels. + */ width?: number + + /** + * Asset height in pixels. + */ height?: number + /** + * Asset notes. + */ notes?: string + + /** + * Asset credits. + */ credits?: string + + /** + * Asset alt text. + */ alt?: string + /** + * Asset tags. + */ tags?: Tag[] /** @@ -56,8 +111,16 @@ export type Asset = { } } +/** + * Available query parameters when querying assets from the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type GetAssetsParams = { // Pagination + /** + * Number of items to return. + */ pageSize?: number /** * @internal @@ -65,12 +128,32 @@ export type GetAssetsParams = { cursor?: string // Filtering + /** + * Asset type to filter by. + */ assetType?: (typeof AssetType)[keyof typeof AssetType] + + /** + * Search query. + */ keyword?: string + + /** + * Asset IDs to filter by. + */ ids?: string[] + + /** + * Asset tags to filter by. + */ tags?: string[] } +/** + * An object representing the result of querying assets from the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type GetAssetsResult = { items: Asset[] total: number @@ -79,6 +162,11 @@ export type GetAssetsResult = { is_opensearch_result: boolean } +/** + * Parameters for uploading an asset to the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type PostAssetParams = { file: BlobPart notes?: string @@ -86,8 +174,18 @@ export type PostAssetParams = { alt?: string } +/** + * Result of uploading an asset to the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type PostAssetResult = Asset +/** + * Parameters for updating an asset in the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type PatchAssetParams = { notes?: string credits?: string @@ -96,8 +194,18 @@ export type PatchAssetParams = { tags?: string[] } +/** + * Result of updating an asset in the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type PatchAssetResult = Asset +/** + * Parameters for deleting an asset from the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type BulkDeleteAssetsParams = { ids: string[] } diff --git a/src/types/api/asset/tag.ts b/src/types/api/asset/tag.ts index 80220e14..5cf9a84e 100644 --- a/src/types/api/asset/tag.ts +++ b/src/types/api/asset/tag.ts @@ -1,8 +1,27 @@ +/** + * An object representing an tag used by the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type Tag = { + /** + * Tag ID. + */ id: string + + /** + * Tag name. + */ name: string + /** + * Tag creation date. + */ created_at: number + + /** + * Tag last modification date. + */ last_modified: number /** @@ -10,13 +29,31 @@ export type Tag = { */ uploader_id?: string + /** + * Number of assets tagged with this tag. + */ count?: number } +/** + * An object representing the result of querying tags from the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type GetTagsResult = { items: Tag[] } +/** + * Parameters for creating a tag in the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type PostTagParams = { name: string } +/** + * Result of creating a tag in the asset API. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type PostTagResult = Tag diff --git a/src/types/api/migration/document.ts b/src/types/api/migration/document.ts index e164206e..e0ddf549 100644 --- a/src/types/api/migration/document.ts +++ b/src/types/api/migration/document.ts @@ -1,5 +1,13 @@ import type { PrismicDocument } from "../../value/document" +/** + * An object representing the parameters required when creating a document + * through the migration API. + * + * @typeParam TDocument - Type of the Prismic document to create. + * + * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + */ export type PostDocumentParams< TDocument extends PrismicDocument = PrismicDocument, > = @@ -17,6 +25,13 @@ export type PostDocumentParams< } : never +/** + * Result of creating a document with the migration API. + * + * @typeParam TDocument - Type of the created Prismic document. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type PostDocumentResult< TDocument extends PrismicDocument = PrismicDocument, > = @@ -32,6 +47,14 @@ export type PostDocumentResult< : Record) : never +/** + * An object representing the parameters required when updating a document + * through the migration API. + * + * @typeParam TDocument - Type of the Prismic document to update. + * + * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + */ export type PutDocumentParams< TDocument extends PrismicDocument = PrismicDocument, > = { @@ -45,6 +68,13 @@ export type PutDocumentParams< data: TDocument["data"] | Record } +/** + * Result of updating a document with the migration API. + * + * @typeParam TDocument - Type of the updated Prismic document. + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ export type PutDocumentResult< TDocument extends PrismicDocument = PrismicDocument, > = PostDocumentResult diff --git a/src/types/migration/asset.ts b/src/types/migration/asset.ts index 088ae5cc..f71ddd82 100644 --- a/src/types/migration/asset.ts +++ b/src/types/migration/asset.ts @@ -1,5 +1,5 @@ /** - * An asset to be uploaded to Prismic media library + * An asset to be uploaded to Prismic media library. */ export type MigrationAsset = { /** diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts index e9eebc54..91fae740 100644 --- a/src/types/migration/document.ts +++ b/src/types/migration/document.ts @@ -15,41 +15,69 @@ import type { SharedSlice } from "../value/sharedSlice" import type { Slice } from "../value/slice" import type { SliceZone } from "../value/sliceZone" -import type { MigrationImageField, MigrationLinkField } from "./fields" -import type { MigrationRTImageNode, MigrationRTLinkNode } from "./richText" +import type { ImageMigrationField, LinkMigrationField } from "./fields" +import type { RTImageMigrationNode, RTLinkMigrationNode } from "./richText" +/** + * A utility type that converts a rich text text node to a rich text text + * migration node. + * + * @typeParam TRTNode - Rich text text node type to convert. + */ type RichTextTextNodeToMigrationField = Omit< TRTNode, "spans" > & { - spans: (RTInlineNode | MigrationRTLinkNode)[] + spans: (RTInlineNode | RTLinkMigrationNode)[] } +/** + * A utility type that converts a rich text block node to a rich text block + * migration node. + * + * @typeParam TRTNode - Rich text block node type to convert. + */ type RichTextBlockNodeToMigrationField = TRTNode extends RTImageNode ? - | MigrationRTImageNode + | RTImageMigrationNode | (Omit & { - linkTo?: RTImageNode["linkTo"] | MigrationLinkField + linkTo?: RTImageNode["linkTo"] | LinkMigrationField }) : TRTNode extends RTTextNode ? RichTextTextNodeToMigrationField : TRTNode +/** + * A utility type that converts a rich text field to a rich text migration + * field. + * + * @typeParam TField - Rich text field type to convert. + */ export type RichTextFieldToMigrationField = { [Index in keyof TField]: RichTextBlockNodeToMigrationField } +/** + * A utility type that converts a regular field to a regular migration field. + * + * @typeParam TField - Regular field type to convert. + */ type RegularFieldToMigrationField = | (TField extends ImageField - ? MigrationImageField + ? ImageMigrationField : TField extends LinkField - ? MigrationLinkField + ? LinkMigrationField : TField extends RichTextField ? RichTextFieldToMigrationField : never) | TField +/** + * A utility type that converts a group field to a group migration field. + * + * @typeParam TField - Group field type to convert. + */ export type GroupFieldToMigrationField< TField extends GroupField | Slice["items"] | SharedSlice["items"], > = FieldsToMigrationFields[] @@ -62,9 +90,19 @@ type SliceToMigrationField = Omit< items: GroupFieldToMigrationField } +/** + * A utility type that converts a field to a migration field. + * + * @typeParam TField - Field type to convert. + */ export type SliceZoneToMigrationField = SliceToMigrationField[] +/** + * A utility type that converts a field to a migration field. + * + * @typeParam TField - Field type to convert. + */ export type FieldToMigrationField< TField extends AnyRegularField | GroupField | SliceZone, > = TField extends AnyRegularField @@ -75,6 +113,12 @@ export type FieldToMigrationField< ? SliceZoneToMigrationField : never +/** + * A utility type that converts a record of fields to a record of migration + * fields. + * + * @typeParam TFields - Type of the record of Prismic fields. + */ export type FieldsToMigrationFields< TFields extends Record, > = { @@ -82,7 +126,7 @@ export type FieldsToMigrationFields< } /** - * Makes the UID of `MigrationPrismicDocument` optional on custom types without + * Makes the UID of `PrismicMigrationDocument` optional on custom types without * UID. TypeScript fails to infer correct types if done with a type * intersection. * @@ -98,10 +142,10 @@ type MakeUIDOptional = * * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} */ -export type MigrationPrismicDocument< - TDocuments extends PrismicDocument = PrismicDocument, +export type PrismicMigrationDocument< + TDocument extends PrismicDocument = PrismicDocument, > = - TDocuments extends PrismicDocument + TDocument extends PrismicDocument ? MakeUIDOptional<{ /** * Type of the document. @@ -112,7 +156,7 @@ export type MigrationPrismicDocument< * The unique identifier for the document. Guaranteed to be unique among * all Prismic documents of the same type. */ - uid: TDocuments["uid"] + uid: TDocument["uid"] /** * Language of document. @@ -126,7 +170,7 @@ export type MigrationPrismicDocument< * @internal */ // Made optional compared to the original type. - id?: TDocuments["id"] + id?: TDocument["id"] /** * Alternate language documents from Prismic content API. Used as a @@ -136,13 +180,13 @@ export type MigrationPrismicDocument< * @internal */ // Made optional compared to the original type. - alternate_languages?: TDocuments["alternate_languages"] + alternate_languages?: TDocument["alternate_languages"] /** * Tags associated with document. */ // Made optional compared to the original type. - tags?: TDocuments["tags"] + tags?: TDocument["tags"] /** * Data contained in the document. @@ -156,7 +200,7 @@ export type MigrationPrismicDocument< * * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} */ -export type MigrationPrismicDocumentParams = { +export type PrismicMigrationDocumentParams = { /** * Name of the document displayed in the editor. */ @@ -165,15 +209,15 @@ export type MigrationPrismicDocumentParams = { /** * A link to the master language document. */ - // We're forced to inline `MigrationContentRelationshipField` here, otherwise + // We're forced to inline `ContentRelationshipMigrationField` here, otherwise // it creates a circular reference to itself which makes TypeScript unhappy. // (but I think it's weird and it doesn't make sense :thinking:) masterLanguageDocument?: | PrismicDocument - | MigrationPrismicDocument + | PrismicMigrationDocument | (() => - | Promise + | Promise | PrismicDocument - | MigrationPrismicDocument + | PrismicMigrationDocument | undefined) } diff --git a/src/types/migration/fields.ts b/src/types/migration/fields.ts index ca2b13e0..757e60c8 100644 --- a/src/types/migration/fields.ts +++ b/src/types/migration/fields.ts @@ -9,7 +9,7 @@ import type { LinkField } from "../value/link" import type { LinkToMediaField } from "../value/linkToMedia" import type { MigrationAsset } from "./asset" -import type { MigrationPrismicDocument } from "./document" +import type { PrismicMigrationDocument } from "./document" export const MigrationFieldType = { Image: "image", @@ -19,7 +19,7 @@ export const MigrationFieldType = { /** * An alternate version of the {@link ImageField} for use with the migration API. */ -export type MigrationImageField = +export type ImageMigrationField = | (MigrationAsset & { migrationType: typeof MigrationFieldType.Image }) @@ -29,7 +29,7 @@ export type MigrationImageField = * An alternate version of the {@link LinkToMediaField} for use with the * migration API. */ -export type MigrationLinkToMediaField = +export type LinkToMediaMigrationField = | (MigrationAsset & { migrationType: typeof MigrationFieldType.LinkToMedia }) @@ -39,18 +39,18 @@ export type MigrationLinkToMediaField = * An alternate version of the {@link ContentRelationshipField} for use with the * migration API. */ -export type MigrationContentRelationshipField = +export type ContentRelationshipMigrationField = | PrismicDocument | (() => - | Promise + | Promise | PrismicDocument - | MigrationPrismicDocument + | PrismicMigrationDocument | undefined) | undefined /** * An alternate version of the {@link LinkField} for use with the migration API. */ -export type MigrationLinkField = - | MigrationLinkToMediaField - | MigrationContentRelationshipField +export type LinkMigrationField = + | LinkToMediaMigrationField + | ContentRelationshipMigrationField diff --git a/src/types/migration/richText.ts b/src/types/migration/richText.ts index 18e86f7e..22ddc3d4 100644 --- a/src/types/migration/richText.ts +++ b/src/types/migration/richText.ts @@ -1,19 +1,19 @@ import type { RTImageNode, RTLinkNode } from "../value/richText" -import type { MigrationImageField, MigrationLinkField } from "./fields" +import type { ImageMigrationField, LinkMigrationField } from "./fields" /** * An alternate version of {@link RTLinkNode} that supports - * {@link MigrationLinkField} for use with the migration API. + * {@link LinkMigrationField} for use with the migration API. */ -export type MigrationRTLinkNode = Omit & { - data: MigrationLinkField +export type RTLinkMigrationNode = Omit & { + data: LinkMigrationField } /** * An alternate version of {@link RTImageNode} that supports - * {@link MigrationImageField} for use with the migration API. + * {@link ImageMigrationField} for use with the migration API. */ -export type MigrationRTImageNode = MigrationImageField & { - linkTo?: RTImageNode["linkTo"] | MigrationLinkField +export type RTImageMigrationNode = ImageMigrationField & { + linkTo?: RTImageNode["linkTo"] | LinkMigrationField } diff --git a/test/types/migration-document.types.ts b/test/types/migration-document.types.ts index 38d73793..0d48cd56 100644 --- a/test/types/migration-document.types.ts +++ b/test/types/migration-document.types.ts @@ -3,7 +3,7 @@ import { expectNever, expectType } from "ts-expect" import type * as prismic from "../../src" -;(value: prismic.MigrationPrismicDocument): true => { +;(value: prismic.PrismicMigrationDocument): true => { switch (typeof value) { case "object": { if (value === null) { @@ -19,7 +19,7 @@ import type * as prismic from "../../src" } } -expectType({ +expectType({ uid: "", type: "", lang: "", @@ -29,7 +29,7 @@ expectType({ /** * Supports any field when generic. */ -expectType({ +expectType({ uid: "", type: "", lang: "", @@ -39,12 +39,12 @@ expectType({ }) /** - * `PrismicDocument` is assignable to `MigrationPrismicDocument` with added + * `PrismicDocument` is assignable to `PrismicMigrationDocument` with added * `title`. */ expectType< TypeOf< - prismic.MigrationPrismicDocument, + prismic.PrismicMigrationDocument, prismic.PrismicDocument & { title: string } > >(true) @@ -55,7 +55,7 @@ type BarDocument = prismic.PrismicDocument<{ bar: prismic.KeyTextField }, "bar"> type BazDocument = prismic.PrismicDocument, "baz"> type Documents = FooDocument | BarDocument | BazDocument -type MigrationDocuments = prismic.MigrationPrismicDocument +type MigrationDocuments = prismic.PrismicMigrationDocument /** * Infers data type from document type. @@ -130,7 +130,7 @@ type SliceDocument = prismic.PrismicDocument< > type AdvancedDocuments = StaticDocument | GroupDocument | SliceDocument type MigrationAdvancedDocuments = - prismic.MigrationPrismicDocument + prismic.PrismicMigrationDocument // Static expectType({ @@ -139,11 +139,11 @@ expectType({ lang: "", data: { image: {} as prismic.ImageField, - migrationImage: {} as prismic.MigrationImageField, + migrationImage: {} as prismic.ImageMigrationField, link: {} as prismic.LinkField, - migrationLink: {} as prismic.MigrationLinkField, + migrationLink: {} as prismic.LinkMigrationField, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.ContentRelationshipField, }, @@ -158,11 +158,11 @@ expectType({ group: [ { image: {} as prismic.ImageField, - migrationImage: {} as prismic.MigrationImageField, + migrationImage: {} as prismic.ImageMigrationField, link: {} as prismic.LinkField, - migrationLink: {} as prismic.MigrationLinkField, + migrationLink: {} as prismic.LinkMigrationField, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.ContentRelationshipField, }, @@ -185,21 +185,21 @@ expectType({ version: "", primary: { image: {} as prismic.ImageField, - migrationImage: {} as prismic.MigrationImageField, + migrationImage: {} as prismic.ImageMigrationField, link: {} as prismic.LinkField, - migrationLink: {} as prismic.MigrationLinkField, + migrationLink: {} as prismic.LinkMigrationField, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.ContentRelationshipField, group: [ { image: {} as prismic.ImageField, - migrationImage: {} as prismic.MigrationImageField, + migrationImage: {} as prismic.ImageMigrationField, link: {} as prismic.LinkField, - migrationLink: {} as prismic.MigrationLinkField, + migrationLink: {} as prismic.LinkMigrationField, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.ContentRelationshipField, @@ -209,11 +209,11 @@ expectType({ items: [ { image: {} as prismic.ImageField, - migrationImage: {} as prismic.MigrationImageField, + migrationImage: {} as prismic.ImageMigrationField, link: {} as prismic.LinkField, - migrationLink: {} as prismic.MigrationLinkField, + migrationLink: {} as prismic.LinkMigrationField, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.MigrationLinkToMediaField, + migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.ContentRelationshipField, diff --git a/test/types/migration.types.ts b/test/types/migration.types.ts index 8e8a0d8b..91542ac2 100644 --- a/test/types/migration.types.ts +++ b/test/types/migration.types.ts @@ -41,36 +41,36 @@ expectType>(false) // Default const defaultCreateAsset = defaultMigration.createAsset("url", "name") -expectType>(true) +expectType>(true) expectType< - TypeEqual + TypeEqual >(true) expectType< TypeEqual< typeof defaultCreateAsset.linkToMedia, - prismic.MigrationLinkToMediaField + prismic.LinkToMediaMigrationField > >(true) expectType< - TypeOf + TypeOf >(true) // Documents const documentsCreateAsset = defaultMigration.createAsset("url", "name") -expectType>( +expectType>( true, ) expectType< - TypeEqual + TypeEqual >(true) expectType< TypeEqual< typeof documentsCreateAsset.linkToMedia, - prismic.MigrationLinkToMediaField + prismic.LinkToMediaMigrationField > >(true) expectType< - TypeOf + TypeOf >(true) /** @@ -88,7 +88,7 @@ const defaultCreateDocument = defaultMigration.createDocument( "", ) expectType< - TypeEqual + TypeEqual >(true) // Documents @@ -104,6 +104,6 @@ const documentsCreateDocument = documentsMigration.createDocument( expectType< TypeEqual< typeof documentsCreateDocument, - prismic.MigrationPrismicDocument + prismic.PrismicMigrationDocument > >(true) From a123e8c6acc9f06e7ff5f6e3751dc4b463275ce3 Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 30 Aug 2024 12:16:57 +0200 Subject: [PATCH 12/61] docs: document new warning messages --- messages/avoid-write-client-in-browser.md | 24 +++++++++++++++ messages/prefer-repository-name.md | 37 ++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 messages/avoid-write-client-in-browser.md diff --git a/messages/avoid-write-client-in-browser.md b/messages/avoid-write-client-in-browser.md new file mode 100644 index 00000000..c20834ce --- /dev/null +++ b/messages/avoid-write-client-in-browser.md @@ -0,0 +1,24 @@ +# Avoid write client in browser + +`@prismicio/client`'s write client uses credentials to authenticate write queries to a Prismic repository. + +The repository write token and migration API key must be provided when creating a `@prismicio/client` write client like the following: + +```typescript +import * as prismic from "@prismicio/client"; + +const writeClient = prismic.createWriteClient("example-prismic-repo", { + writeToken: "xxx" + migrationAPIKey: "yyy" +}) +``` + +If the write client gets exposed to the browser, its credentials also do. This potentially exposes them to malicious actors. + +When no write actions are to be performed, using a `@prismicio/client` regular client should be preferred. This client only has read access to a Prismic repository. + +```typescript +import * as prismic from "@prismicio/client"; + +const client = prismic.createClient("example-prismic-repo") +``` diff --git a/messages/prefer-repository-name.md b/messages/prefer-repository-name.md index 0b2b52fa..b759bcb5 100644 --- a/messages/prefer-repository-name.md +++ b/messages/prefer-repository-name.md @@ -1,3 +1,38 @@ # Prefer repository name -TODO +`@prismicio/client` uses either a Prismic repository name or a Prismic Rest API v2 repository endpoint to query content from Prismic. + +The repository name or repository endpoint must be provided when creating a `@prismicio/client` like the following: + +```typescript +import * as prismic from "@prismicio/client"; + +// Using the repository name +const client = prismic.createClient("example-prismic-repo") + +// Using the repository endpoint +const client = prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2") +``` + +When proxying a Prismic API v2 repository endpoint (not recommended), the `apiEndpoint` option can be used to specify that endpoint explicitly. When doing so, if a repository endpoint is also provided as the first parameter, it should match the provided `apiEndpoint`. + +```typescript +import * as prismic from "@prismicio/client" + +// ✅ Correct +const client = prismic.createClient("my-repo-name", { + apiEndpoint: "https://example.com/my-repo-name/prismic" +}) + +// ✅ Correct +const client = prismic.createClient("https://example.com/my-repo-name/prismic", { + apiEndpoint: "https://example.com/my-repo-name/prismic" +}) + +// ❌ Incorrect +const client = prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2", { + apiEndpoint: "https://example.com/my-repo-name/prismic" +}) +``` + +Proxying a Prismic API v2 repository endpoint can have unexpected side-effects and cause performance issues when querying Prismic. From 23e011eb76ae9d732e6971955d029118eabc5768 Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 30 Aug 2024 12:17:14 +0200 Subject: [PATCH 13/61] feat: validate assets metadata --- src/Migration.ts | 4 ++ src/WriteClient.ts | 144 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 134 insertions(+), 14 deletions(-) diff --git a/src/Migration.ts b/src/Migration.ts index 49340407..ae7af12b 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -23,6 +23,8 @@ import type { AnyRegularField } from "./types/value/types" import * as isFilled from "./helpers/isFilled" +import { validateAssetMetadata } from "./WriteClient" + /** * Discover assets in a record of Prismic fields. * @@ -224,6 +226,8 @@ export class Migration< } } + validateAssetMetadata(asset) + const maybeAsset = this.assets.get(asset.id) if (maybeAsset) { diff --git a/src/WriteClient.ts b/src/WriteClient.ts index fe17c863..84b57f2f 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -1,3 +1,4 @@ +import { devMsg } from "./lib/devMsg" import { pLimit } from "./lib/pLimit" import type { AssetMap, DocumentMap } from "./lib/patchMigrationDocumentData" import { patchMigrationDocumentData } from "./lib/patchMigrationDocumentData" @@ -80,6 +81,115 @@ type GetAssetsReturnType = { next?: () => Promise } +/** + * Additional parameters for creating an asset in the Prismic media library. + */ +type CreateAssetParams = { + /** + * Asset notes. + */ + notes?: string + + /** + * Asset credits. + */ + credits?: string + + /** + * Asset alt text. + */ + alt?: string + + /** + * Asset tags. + */ + tags?: string[] +} + +/** + * Max length for asset notes accepted by the API. + */ +const ASSET_NOTES_MAX_LENGTH = 500 + +/** + * Max length for asset credits accepted by the API. + */ +const ASSET_CREDITS_MAX_LENGTH = 500 + +/** + * Max length for asset alt text accepted by the API. + */ +const ASSET_ALT_MAX_LENGTH = 500 + +/** + * Checks if a string is an asset tag ID. + * + * @param maybeAssetTagID - A string that's maybe an asset tag ID. + * + * @returns `true` if the string is an asset tag ID, `false` otherwise. + */ +const isAssetTagID = (maybeAssetTagID: string): boolean => { + // Taken from @sinclair/typebox which is the uuid type checker of the asset API + // See: https://github.com/sinclairzx81/typebox/blob/e36f5658e3a56d8c32a711aa616ec8bb34ca14b4/test/runtime/compiler/validate.ts#L15 + // Tag is already a tag ID + return /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test( + maybeAssetTagID, + ) +} + +/** + * Validates an asset's metadata, throwing an error if any of the metadata are + * invalid. + * + * @param assetMetadata - The asset metadata to validate. + * + * @internal + */ +export const validateAssetMetadata = ({ + notes, + credits, + alt, + tags, +}: CreateAssetParams): void => { + const errors: string[] = [] + + if (notes && notes.length > ASSET_NOTES_MAX_LENGTH) { + errors.push( + `\`notes\` must be at most ${ASSET_NOTES_MAX_LENGTH} characters`, + ) + } + + if (credits && credits.length > ASSET_CREDITS_MAX_LENGTH) { + errors.push( + `\`credits\` must be at most ${ASSET_CREDITS_MAX_LENGTH} characters`, + ) + } + + if (alt && alt.length > ASSET_ALT_MAX_LENGTH) { + errors.push(`\`alt\` must be at most ${ASSET_ALT_MAX_LENGTH} characters`) + } + + if ( + tags && + tags.length && + tags.some( + (tag) => !isAssetTagID(tag) && (tag.length < 3 || tag.length > 20), + ) + ) { + errors.push( + `all \`tags\`'s tag must be at least 3 characters long and 20 characters at most`, + ) + } + + if (errors.length) { + throw new PrismicError( + `Errors validating asset metadata: ${errors.join(", ")}`, + undefined, + { notes, credits, alt, tags }, + ) + } +} + /** * Configuration for clients that determine how content is queried. */ @@ -129,6 +239,12 @@ export class WriteClient< constructor(repositoryName: string, options: WriteClientConfig) { super(repositoryName, options) + if (typeof window !== "undefined") { + console.warn( + `[@prismicio/client] Prismic write client appears to be running in a browser environment. This is not recommended as it exposes your write token and migration API key. Consider using Prismic write client in a server environment only, preferring the regular client for browser environement. For more details, see ${devMsg("avoid-write-client-in-browser")}`, + ) + } + this.writeToken = options.writeToken this.migrationAPIKey = options.migrationAPIKey @@ -520,13 +636,10 @@ export class WriteClient< alt, tags, ...params - }: { - notes?: string - credits?: string - alt?: string - tags?: string[] - } & FetchParams = {}, + }: CreateAssetParams & FetchParams = {}, ): Promise { + validateAssetMetadata({ notes, credits, alt, tags }) + const url = new URL("assets", this.assetAPIEndpoint) const formData = new FormData() @@ -584,6 +697,8 @@ export class WriteClient< ...params }: PatchAssetParams & FetchParams = {}, ): Promise { + validateAssetMetadata({ notes, credits, alt, tags }) + const url = new URL(`assets/${id}`, this.assetAPIEndpoint) // Resolve tags if any and create missing ones @@ -725,14 +840,7 @@ export class WriteClient< const resolvedTagIDs = [] for (const tagNameOrID of tagNamesOrIDs) { - // Taken from @sinclair/typebox which is the uuid type checker of the asset API - // See: https://github.com/sinclairzx81/typebox/blob/e36f5658e3a56d8c32a711aa616ec8bb34ca14b4/test/runtime/compiler/validate.ts#L15 - // Tag is already a tag ID - if ( - /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test( - tagNameOrID, - ) - ) { + if (isAssetTagID(tagNameOrID)) { resolvedTagIDs.push(tagNameOrID) continue } @@ -770,6 +878,14 @@ export class WriteClient< name: string, params?: FetchParams, ): Promise { + if (name.length < 3 || name.length > 20) { + throw new PrismicError( + "Asset tag name must be at least 3 characters long and 20 characters at most", + undefined, + { name }, + ) + } + const url = new URL("tags", this.assetAPIEndpoint) return this.fetch( From f7a886803e0ad2acb5cc23e54484473ea8d6f353 Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 30 Aug 2024 12:17:31 +0200 Subject: [PATCH 14/61] refactor: skip initial delay on `pLimit` when possible --- src/lib/pLimit.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/pLimit.ts b/src/lib/pLimit.ts index 4a0539c0..a5365e51 100644 --- a/src/lib/pLimit.ts +++ b/src/lib/pLimit.ts @@ -40,6 +40,7 @@ export const pLimit = ({ }): LimitFunction => { const queue: AnyFunction[] = [] let activeCount = 0 + let lastCompletion = 0 const resumeNext = () => { if (activeCount < limit && queue.length > 0) { @@ -60,8 +61,10 @@ export const pLimit = ({ resolve: (value: unknown) => void, arguments_: unknown[], ) => { - if (interval) { - await sleep(interval) + const timeSinceLastCompletion = Date.now() - lastCompletion + + if (interval && timeSinceLastCompletion < interval) { + await sleep(interval - timeSinceLastCompletion) } const result = (async () => function_(...arguments_))() @@ -71,6 +74,8 @@ export const pLimit = ({ await result } catch {} + lastCompletion = Date.now() + next() } From 96bce5f4c37d622732c8c357b298a438ce2d30c6 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 2 Sep 2024 11:18:20 +0200 Subject: [PATCH 15/61] test: `Client` updated constructor --- messages/prefer-repository-name.md | 6 +- src/getRepositoryName.ts | 21 +++-- test/client.test.ts | 121 +++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 11 deletions(-) diff --git a/messages/prefer-repository-name.md b/messages/prefer-repository-name.md index b759bcb5..a90b8bf5 100644 --- a/messages/prefer-repository-name.md +++ b/messages/prefer-repository-name.md @@ -14,7 +14,7 @@ const client = prismic.createClient("example-prismic-repo") const client = prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2") ``` -When proxying a Prismic API v2 repository endpoint (not recommended), the `apiEndpoint` option can be used to specify that endpoint explicitly. When doing so, if a repository endpoint is also provided as the first parameter, it should match the provided `apiEndpoint`. +When proxying a Prismic API v2 repository endpoint (not recommended), the `apiEndpoint` option can be used to specify that endpoint. When doing so, the repository name should be used as the first argument when creating the client because a repository name can't be inferred from a proxied endpoint. ```typescript import * as prismic from "@prismicio/client" @@ -24,12 +24,12 @@ const client = prismic.createClient("my-repo-name", { apiEndpoint: "https://example.com/my-repo-name/prismic" }) -// ✅ Correct +// ❌ Incorrect, repository name can't be inferred from a proxied endpoint const client = prismic.createClient("https://example.com/my-repo-name/prismic", { apiEndpoint: "https://example.com/my-repo-name/prismic" }) -// ❌ Incorrect +// ❌ Incorrect, the endpoint does not match the `apiEndpoint` option. const client = prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2", { apiEndpoint: "https://example.com/my-repo-name/prismic" }) diff --git a/src/getRepositoryName.ts b/src/getRepositoryName.ts index 08f16998..a78c19f9 100644 --- a/src/getRepositoryName.ts +++ b/src/getRepositoryName.ts @@ -16,12 +16,17 @@ import { PrismicError } from "./errors/PrismicError" */ export const getRepositoryName = (repositoryEndpoint: string): string => { try { - return new URL(repositoryEndpoint).hostname.split(".")[0] - } catch { - throw new PrismicError( - `An invalid Prismic Rest API V2 endpoint was provided: ${repositoryEndpoint}`, - undefined, - undefined, - ) - } + const parts = new URL(repositoryEndpoint).hostname.split(".") + + // [subdomain, domain, tld] + if (parts.length > 2) { + return parts[0] + } + } catch {} + + throw new PrismicError( + `An invalid Prismic Rest API V2 endpoint was provided: ${repositoryEndpoint}`, + undefined, + undefined, + ) } diff --git a/test/client.test.ts b/test/client.test.ts index 1f3a7d77..69b08e13 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -149,6 +149,82 @@ it("constructor warns if a non-.cdn prismic.io endpoint is given", () => { process.env.NODE_ENV = originalNodeEnv }) +it("contructor warns if an endpoint is given along the `apiEndpoint` option and they do not match", () => { + const fetch = vi.fn() + + const originalNodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = "development" + + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => void 0) + + prismic.createClient("https://example.com/my-repo-name", { + apiEndpoint: "https://example.com/my-repo-name/prismic", + fetch, + }) + expect(consoleWarnSpy).toHaveBeenNthCalledWith( + 2, + expect.stringMatching(/prefer-repository-name/i), + ) + consoleWarnSpy.mockClear() + + prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2", { + apiEndpoint: "https://example.com/my-repo-name/prismic", + fetch, + }) + expect(consoleWarnSpy).toHaveBeenNthCalledWith( + 1, + expect.stringMatching(/prefer-repository-name/i), + ) + consoleWarnSpy.mockClear() + + prismic.createClient("https://example.com/my-repo-name/prismic", { + apiEndpoint: "https://example.com/my-repo-name/prismic", + fetch, + }) + expect(consoleWarnSpy).toHaveBeenNthCalledWith( + 1, + expect.stringMatching(/prefer-repository-name/i), + ) + consoleWarnSpy.mockClear() + + prismic.createClient("my-repo-name", { + apiEndpoint: "https://example.com/my-repo-name/prismic", + fetch, + }) + expect(consoleWarnSpy).not.toHaveBeenCalledWith( + expect.stringMatching(/prefer-repository-name/i), + ) + + consoleWarnSpy.mockRestore() + + process.env.NODE_ENV = originalNodeEnv +}) + +it("contructor warns if an endpoint is given and the repository name could not be inferred", () => { + const fetch = vi.fn() + + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => void 0) + + prismic.createClient("https://example.com/my-repo-name/prismic", { fetch }) + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringMatching(/prefer-repository-name/i), + ) + consoleWarnSpy.mockClear() + + prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2", { + fetch, + }) + expect(consoleWarnSpy).not.toHaveBeenCalledWith( + expect.stringMatching(/prefer-repository-name/i), + ) + + consoleWarnSpy.mockRestore() +}) + it("constructor throws if fetch is unavailable", () => { const endpoint = prismic.getRepositoryEndpoint("qwerty") @@ -197,6 +273,51 @@ it("constructor throws if provided fetch is not a function", () => { globalThis.fetch = originalFetch }) +it("throws is `repositoryName` is not available but accessed", () => { + const fetch = vi.fn() + + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => void 0) + + const client = prismic.createClient( + "https://example.com/my-repo-name/prismic", + { fetch }, + ) + + expect(() => { + client.repositoryName + }).toThrowError(/prefer-repository-name/i) + + const client2 = prismic.createClient("my-repo-name", { + fetch, + apiEndpoint: "https://example.com/my-repo-name/prismic", + }) + + expect(() => { + client2.repositoryName + }).not.toThrowError() + + consoleWarnSpy.mockRestore() +}) + +// TODO: Remove when alias gets removed +it("aliases `endpoint` (deprecated) to `apiEndpoint`", () => { + const fetch = vi.fn() + + const client = prismic.createClient( + "https://example-prismic-repo.cdn.prismic.io/api/v2", + { fetch }, + ) + + expect(client.apiEndpoint).toBe(client.endpoint) + + const otherEndpoint = "https://other-prismic-repo.cdn.prismic.io/api/v2" + client.endpoint = otherEndpoint + + expect(client.apiEndpoint).toBe(otherEndpoint) +}) + it("uses globalThis.fetch if available", async () => { const endpoint = prismic.getRepositoryEndpoint("qwerty") const responseBody = { foo: "bar" } From 7d45a0a5792ef9fc6d855382135afe917c5777e0 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 2 Sep 2024 15:29:03 +0200 Subject: [PATCH 16/61] test: `Migration` class --- src/Migration.ts | 6 +- test/migration-createAsset.test.ts | 273 ++++++++++++++++++++++++ test/migration-createDocument.test.ts | 296 ++++++++++++++++++++++++++ test/migration-getByUID.test.ts | 37 ++++ test/migration-getSingle.test.ts | 33 +++ 5 files changed, 642 insertions(+), 3 deletions(-) create mode 100644 test/migration-createAsset.test.ts create mode 100644 test/migration-createDocument.test.ts create mode 100644 test/migration-getByUID.test.ts create mode 100644 test/migration-getSingle.test.ts diff --git a/src/Migration.ts b/src/Migration.ts index ae7af12b..fa03d4b3 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -26,7 +26,7 @@ import * as isFilled from "./helpers/isFilled" import { validateAssetMetadata } from "./WriteClient" /** - * Discover assets in a record of Prismic fields. + * Discovers assets in a record of Prismic fields. * * @param record - Record of Prismic fields to loook for assets in. * @param onAsset - Callback that is called for each asset found. @@ -184,8 +184,8 @@ export class Migration< const filename = "name" in fileOrAsset ? fileOrAsset.name - : url.split("_").pop() || - fileOrAsset.alt?.slice(0, 500) || + : url.split("/").pop()?.split("_").pop() || + fileOrAsset.alt || "unknown" const credits = "copyright" in fileOrAsset && fileOrAsset.copyright diff --git a/test/migration-createAsset.test.ts b/test/migration-createAsset.test.ts new file mode 100644 index 00000000..7cc8e41a --- /dev/null +++ b/test/migration-createAsset.test.ts @@ -0,0 +1,273 @@ +import { expect, it } from "vitest" + +import * as prismic from "../src" +import type { Asset } from "../src/types/api/asset/asset" +import { AssetType } from "../src/types/api/asset/asset" + +it("creates an asset from a url", () => { + const migration = prismic.createMigration() + + const filename = "foo.jpg" + const file = "https://example.com/foo.jpg" + + migration.createAsset(file, filename) + + expect(migration.assets.get(file)).toEqual({ + id: file, + file, + filename, + }) +}) + +it("creates an asset from a file", () => { + const migration = prismic.createMigration() + + const filename = "foo.jpg" + const file = new File(["data"], filename) + + migration.createAsset(file, filename) + + expect(migration.assets.get(file)).toEqual({ + id: file, + file, + filename, + }) +}) + +it("creates an asset from an existing asset", () => { + const migration = prismic.createMigration() + + const asset: Asset = { + id: "foo", + url: "https://example.com/foo.jpg", + created_at: 0, + last_modified: 0, + filename: "foo.jpg", + extension: "jpg", + size: 1, + kind: AssetType.Image, + width: 1, + height: 1, + notes: "notes", + credits: "credits", + alt: "alt", + tags: [ + { + name: "tag", + id: "id", + created_at: 0, + last_modified: 0, + uploader_id: "uploader_id", + }, + ], + } + + migration.createAsset(asset) + + expect(migration.assets.get(asset.id)).toStrictEqual({ + id: asset.id, + file: asset.url, + filename: asset.filename, + alt: asset.alt, + credits: asset.credits, + notes: asset.notes, + tags: asset.tags?.map(({ name }) => name), + }) +}) + +it("creates an asset from an image field", () => { + const migration = prismic.createMigration() + + const image: prismic.FilledImageFieldImage = { + id: "foo", + url: "https://example.com/foo.jpg", + alt: "alt", + copyright: "copyright", + dimensions: { width: 1, height: 1 }, + edit: { x: 0, y: 0, zoom: 1, background: "transparent" }, + } + + migration.createAsset(image) + + expect(migration.assets.get(image.id)).toEqual({ + id: image.id, + file: image.url, + filename: image.url.split("/").pop(), + alt: image.alt, + credits: image.copyright, + }) + + const image2: prismic.FilledImageFieldImage = { + ...image, + id: "bar", + url: "https://example.com/", + } + + migration.createAsset(image2) + + expect( + migration.assets.get(image2.id), + "uses alt as filename fallback when filename cannot be inferred from URL", + ).toEqual({ + id: image2.id, + file: image2.url, + filename: image2.alt, + alt: image2.alt, + credits: image2.copyright, + }) + + const image3: prismic.FilledImageFieldImage = { + ...image, + id: "qux", + alt: "", + url: "https://example.com/", + } + + migration.createAsset(image3) + + expect( + migration.assets.get(image3.id), + "uses default filename when filename cannot be inferred from URL and `alt` is not available", + ).toEqual({ + id: image3.id, + file: image3.url, + filename: "unknown", + credits: image3.copyright, + }) +}) + +it("creates an asset from a link to media field", () => { + const migration = prismic.createMigration() + + const link: prismic.FilledLinkToMediaField = { + id: "foo", + url: "https://example.com/foo.jpg", + link_type: prismic.LinkType.Media, + kind: AssetType.Image, + name: "foo.jpg", + size: "1", + height: "1", + width: "1", + } + + migration.createAsset(link) + + expect(migration.assets.get(link.id)).toEqual({ + id: link.id, + file: link.url, + filename: link.name, + }) +}) + +it("throws if asset has invalid metadata", () => { + const migration = prismic.createMigration() + + const filename = "foo.jpg" + const file = "https://example.com/foo.jpg" + + expect(() => { + migration.createAsset(file, filename, { notes: "0".repeat(501) }) + }, "notes").toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`notes\` must be at most 500 characters]`, + ) + + expect(() => { + migration.createAsset(file, filename, { credits: "0".repeat(501) }) + }, "credits").toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`credits\` must be at most 500 characters]`, + ) + + expect(() => { + migration.createAsset(file, filename, { alt: "0".repeat(501) }) + }, "alt").toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`alt\` must be at most 500 characters]`, + ) + + expect(() => { + migration.createAsset(file, filename, { tags: ["0"] }) + }, "tags").toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, + ) + + expect(() => { + migration.createAsset(file, filename, { tags: ["0".repeat(21)] }) + }, "tags").toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, + ) + + expect(() => { + migration.createAsset(file, filename, { + tags: [ + // Tag name + "012", + // Tag ID + "123e4567-e89b-12d3-a456-426614174000", + ], + }) + }, "tags").not.toThrowError() +}) + +it("consolidates existing assets with additional metadata", () => { + const migration = prismic.createMigration() + + const filename = "foo.jpg" + const file = "https://example.com/foo.jpg" + + migration.createAsset(file, filename) + + expect(migration.assets.get(file)).toStrictEqual({ + id: file, + file, + filename, + notes: undefined, + alt: undefined, + credits: undefined, + tags: undefined, + }) + + migration.createAsset(file, filename, { + notes: "notes", + alt: "alt", + credits: "credits", + tags: ["tag"], + }) + + expect(migration.assets.get(file)).toStrictEqual({ + id: file, + file, + filename, + notes: "notes", + alt: "alt", + credits: "credits", + tags: ["tag"], + }) + + migration.createAsset(file, filename, { + notes: "notes 2", + alt: "alt 2", + credits: "credits 2", + tags: ["tag", "tag 2"], + }) + + expect(migration.assets.get(file)).toStrictEqual({ + id: file, + file, + filename, + notes: "notes 2", + alt: "alt 2", + credits: "credits 2", + tags: ["tag", "tag 2"], + }) + + migration.createAsset(file, filename) + + expect(migration.assets.get(file)).toStrictEqual({ + id: file, + file, + filename, + notes: "notes 2", + alt: "alt 2", + credits: "credits 2", + tags: ["tag", "tag 2"], + }) +}) diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts new file mode 100644 index 00000000..bc2cd363 --- /dev/null +++ b/test/migration-createDocument.test.ts @@ -0,0 +1,296 @@ +import { describe, expect, it } from "vitest" + +import type { MockFactory } from "@prismicio/mock" + +import * as prismic from "../src" +import type { MigrationAsset } from "../src/types/migration/asset" + +it("creates a document", () => { + const migration = prismic.createMigration() + + const document: prismic.PrismicMigrationDocument = { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + } + const documentName = "documentName" + + migration.createDocument(document, documentName) + + expect(migration.documents[0]).toStrictEqual({ + document, + params: { documentName }, + }) +}) + +it("creates a document from an existing Prismic document", () => { + const migration = prismic.createMigration() + + const document: prismic.PrismicDocument = { + id: "id", + type: "type", + uid: "uid", + lang: "lang", + url: "url", + href: "href", + slugs: [], + tags: [], + linked_documents: [], + first_publication_date: "0-0-0T0:0:0+0", + last_publication_date: "0-0-0T0:0:0+0", + alternate_languages: [], + data: {}, + } + const documentName = "documentName" + + migration.createDocument(document, documentName) + + expect(migration.documents[0]).toStrictEqual({ + document, + params: { documentName }, + }) +}) + +describe.each<{ + fieldType: string + getField: (mock: MockFactory) => { + id: string + field: + | prismic.FilledImageFieldImage + | prismic.FilledLinkToMediaField + | prismic.RichTextField<"filled"> + expected: MigrationAsset + } +}>([ + { + fieldType: "image", + getField: (mock) => { + const image = mock.value.image({ state: "filled" }) + + return { + id: image.id, + field: image, + expected: { + alt: image.alt ?? undefined, + credits: image.copyright ?? undefined, + file: image.url.split("?")[0], + filename: image.url.split("/").pop()!.split("?")[0], + id: image.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, + { + fieldType: "link to media", + getField: (mock) => { + const linkToMedia = mock.value.linkToMedia({ state: "filled" }) + + return { + id: linkToMedia.id, + field: linkToMedia, + expected: { + alt: undefined, + credits: undefined, + file: linkToMedia.url.split("?")[0], + filename: linkToMedia.name, + id: linkToMedia.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, + { + fieldType: "rich text (image)", + getField: (mock) => { + const image = mock.value.image({ state: "filled" }) + const richText: prismic.RichTextField<"filled"> = [ + { + type: prismic.RichTextNodeType.image, + ...image, + }, + ] + + return { + id: image.id, + field: richText, + expected: { + alt: image.alt ?? undefined, + credits: image.copyright ?? undefined, + file: image.url.split("?")[0], + filename: image.url.split("/").pop()!.split("?")[0], + id: image.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, + { + fieldType: "rich text (link to media span)", + getField: (mock) => { + const linkToMedia = mock.value.linkToMedia({ state: "filled" }) + const richText = mock.value.richText({ + state: "filled", + }) as prismic.RichTextField<"filled"> + + richText.push({ + type: prismic.RichTextNodeType.paragraph, + text: "lorem", + spans: [ + { + start: 0, + end: 5, + type: prismic.RichTextNodeType.hyperlink, + data: linkToMedia, + }, + ], + }) + + return { + id: linkToMedia.id, + field: richText, + expected: { + alt: undefined, + credits: undefined, + file: linkToMedia.url.split("?")[0], + filename: linkToMedia.name, + id: linkToMedia.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, + { + fieldType: "rich text (image's link to media)", + getField: (mock) => { + const image = mock.value.image({ state: "filled" }) + const linkToMedia = mock.value.linkToMedia({ state: "filled" }) + const richText: prismic.RichTextField<"filled"> = [ + { + type: prismic.RichTextNodeType.image, + linkTo: linkToMedia, + ...image, + }, + ] + + return { + id: linkToMedia.id, + field: richText, + expected: { + alt: undefined, + credits: undefined, + file: linkToMedia.url.split("?")[0], + filename: linkToMedia.name, + id: linkToMedia.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, +])("extracts assets from image fields ($fieldType)", ({ getField }) => { + it("regular fields", ({ mock }) => { + const migration = prismic.createMigration() + + const { id, field, expected } = getField(mock) + + const document: prismic.PrismicMigrationDocument = { + type: "type", + uid: "uid", + lang: "lang", + data: { field }, + } + const documentName = "documentName" + + expect(migration.assets.size).toBe(0) + + migration.createDocument(document, documentName) + + expect(migration.assets.get(id)).toStrictEqual(expected) + }) + + it("group fields", ({ mock }) => { + const migration = prismic.createMigration() + + const { id, field, expected } = getField(mock) + + const document: prismic.PrismicMigrationDocument = { + type: "type", + uid: "uid", + lang: "lang", + data: { group: [{ field }] }, + } + const documentName = "documentName" + + expect(migration.assets.size).toBe(0) + + migration.createDocument(document, documentName) + + expect(migration.assets.get(id)).toStrictEqual(expected) + }) + + it("slice's primary zone", ({ mock }) => { + const migration = prismic.createMigration() + + const { id, field, expected } = getField(mock) + + const slices: prismic.SliceZone = [ + { + id: "id", + slice_type: "slice_type", + slice_label: "slice_label", + primary: { field }, + items: [], + }, + ] + + const document: prismic.PrismicMigrationDocument = { + type: "type", + uid: "uid", + lang: "lang", + data: { slices }, + } + const documentName = "documentName" + + expect(migration.assets.size).toBe(0) + + migration.createDocument(document, documentName) + + expect(migration.assets.get(id)).toStrictEqual(expected) + }) + + it("slice's repeatable zone", ({ mock }) => { + const migration = prismic.createMigration() + + const { id, field, expected } = getField(mock) + + const slices: prismic.SliceZone = [ + { + id: "id", + slice_type: "slice_type", + slice_label: "slice_label", + primary: {}, + items: [{ field }], + }, + ] + + const document: prismic.PrismicMigrationDocument = { + type: "type", + uid: "uid", + lang: "lang", + data: { slices }, + } + const documentName = "documentName" + + expect(migration.assets.size).toBe(0) + + migration.createDocument(document, documentName) + + expect(migration.assets.get(id)).toStrictEqual(expected) + }) +}) diff --git a/test/migration-getByUID.test.ts b/test/migration-getByUID.test.ts new file mode 100644 index 00000000..015d7dce --- /dev/null +++ b/test/migration-getByUID.test.ts @@ -0,0 +1,37 @@ +import { expect, it } from "vitest" + +import * as prismic from "../src" + +it("returns indexed document", () => { + const migration = prismic.createMigration() + + const document = { + type: "type", + uid: "foo", + lang: "lang", + data: {}, + } + const documentName = "documentName" + + migration.createDocument(document, documentName) + + expect(migration.getByUID(document.type, document.uid)).toStrictEqual( + document, + ) +}) + +it("returns `undefined` is document is not found", () => { + const migration = prismic.createMigration() + + const document = { + type: "type", + uid: "foo", + lang: "lang", + data: {}, + } + const documentName = "documentName" + + migration.createDocument(document, documentName) + + expect(migration.getByUID(document.type, "bar")).toStrictEqual(undefined) +}) diff --git a/test/migration-getSingle.test.ts b/test/migration-getSingle.test.ts new file mode 100644 index 00000000..19b301bc --- /dev/null +++ b/test/migration-getSingle.test.ts @@ -0,0 +1,33 @@ +import { expect, it } from "vitest" + +import * as prismic from "../src" + +it("returns indexed document", () => { + const migration = prismic.createMigration() + + const document = { + type: "foo", + lang: "lang", + data: {}, + } + const documentName = "documentName" + + migration.createDocument(document, documentName) + + expect(migration.getSingle(document.type)).toStrictEqual(document) +}) + +it("returns `undefined` is document is not found", () => { + const migration = prismic.createMigration() + + const document = { + type: "foo", + lang: "lang", + data: {}, + } + const documentName = "documentName" + + migration.createDocument(document, documentName) + + expect(migration.getSingle("bar")).toStrictEqual(undefined) +}) From 7f75b6495bc24170c238e9549a6cbe21120a8efa Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 2 Sep 2024 17:21:26 +0200 Subject: [PATCH 17/61] test: `WriteClient` constructor --- src/WriteClient.ts | 22 ++++++++--------- test/writeClient.test.ts | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 test/writeClient.test.ts diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 84b57f2f..0024bc26 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -13,10 +13,10 @@ import type { PostAssetResult, } from "./types/api/asset/asset" import type { - GetTagsResult, - PostTagParams, - PostTagResult, - Tag, + AssetTag, + GetAssetTagsResult, + PostAssetTagParams, + PostAssetTagResult, } from "./types/api/asset/tag" import type { PutDocumentResult } from "./types/api/migration/document" import { @@ -239,7 +239,7 @@ export class WriteClient< constructor(repositoryName: string, options: WriteClientConfig) { super(repositoryName, options) - if (typeof window !== "undefined") { + if (typeof globalThis.window !== "undefined") { console.warn( `[@prismicio/client] Prismic write client appears to be running in a browser environment. This is not recommended as it exposes your write token and migration API key. Consider using Prismic write client in a server environment only, preferring the regular client for browser environement. For more details, see ${devMsg("avoid-write-client-in-browser")}`, ) @@ -833,7 +833,7 @@ export class WriteClient< ): Promise { return this._resolveAssetTagIDsLimit(async () => { const existingTags = await this.getAssetTags(params) - const existingTagMap: Record = {} + const existingTagMap: Record = {} for (const tag of existingTags) { existingTagMap[tag.name] = tag } @@ -877,7 +877,7 @@ export class WriteClient< private async createAssetTag( name: string, params?: FetchParams, - ): Promise { + ): Promise { if (name.length < 3 || name.length > 20) { throw new PrismicError( "Asset tag name must be at least 3 characters long and 20 characters at most", @@ -888,9 +888,9 @@ export class WriteClient< const url = new URL("tags", this.assetAPIEndpoint) - return this.fetch( + return this.fetch( url.toString(), - this.buildAssetAPIQueryParams({ + this.buildAssetAPIQueryParams({ method: "POST", body: { name }, params, @@ -905,10 +905,10 @@ export class WriteClient< * * @returns An array of existing tags. */ - private async getAssetTags(params?: FetchParams): Promise { + private async getAssetTags(params?: FetchParams): Promise { const url = new URL("tags", this.assetAPIEndpoint) - const { items } = await this.fetch( + const { items } = await this.fetch( url.toString(), this.buildAssetAPIQueryParams({ params }), ) diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts new file mode 100644 index 00000000..ef82d67f --- /dev/null +++ b/test/writeClient.test.ts @@ -0,0 +1,53 @@ +import { expect, it, vi } from "vitest" + +import * as prismic from "../src" + +it("createWriteClient creates a write client", () => { + const client = prismic.createWriteClient("qwerty", { + writeToken: "xxx", + migrationAPIKey: "yyy", + }) + + expect(client).toBeInstanceOf(prismic.WriteClient) +}) + +it("constructor warns if running in a browser-like environment", () => { + const originalWindow = globalThis.window + globalThis.window = {} as Window & typeof globalThis + + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => void 0) + + prismic.createWriteClient("qwerty", { + writeToken: "xxx", + migrationAPIKey: "yyy", + }) + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringMatching(/avoid-write-client-in-browser/i), + ) + + consoleWarnSpy.mockRestore() + + globalThis.window = originalWindow +}) + +it("uses provided asset API endpoint and adds `/` suffix", () => { + const client = prismic.createWriteClient("qwerty", { + assetAPIEndpoint: "https://example.com", + writeToken: "xxx", + migrationAPIKey: "yyy", + }) + + expect(client.assetAPIEndpoint).toBe("https://example.com/") +}) + +it("uses provided migration API endpoint and adds `/` suffix", () => { + const client = prismic.createWriteClient("qwerty", { + migrationAPIEndpoint: "https://example.com", + writeToken: "xxx", + migrationAPIKey: "yyy", + }) + + expect(client.migrationAPIEndpoint).toBe("https://example.com/") +}) From 69f3c2f9b9278e7c1a2a4a5324db384aa9c3e933 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 2 Sep 2024 17:21:39 +0200 Subject: [PATCH 18/61] test: setup mocks for asset API and migration API --- src/types/api/asset/asset.ts | 4 +- src/types/api/asset/tag.ts | 8 +- src/types/api/migration/document.ts | 2 +- test/__testutils__/mockPrismicAssetAPI.ts | 151 ++++++++++++++++++ test/__testutils__/mockPrismicMigrationAPI.ts | 81 ++++++++++ test/writeClient-migrate.test.ts | 12 ++ 6 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 test/__testutils__/mockPrismicAssetAPI.ts create mode 100644 test/__testutils__/mockPrismicMigrationAPI.ts create mode 100644 test/writeClient-migrate.test.ts diff --git a/src/types/api/asset/asset.ts b/src/types/api/asset/asset.ts index 29c89435..24ca8b4d 100644 --- a/src/types/api/asset/asset.ts +++ b/src/types/api/asset/asset.ts @@ -1,4 +1,4 @@ -import type { Tag } from "./tag" +import type { AssetTag } from "./tag" /** * Asset types. @@ -88,7 +88,7 @@ export type Asset = { /** * Asset tags. */ - tags?: Tag[] + tags?: AssetTag[] /** * @internal diff --git a/src/types/api/asset/tag.ts b/src/types/api/asset/tag.ts index 5cf9a84e..23f8995b 100644 --- a/src/types/api/asset/tag.ts +++ b/src/types/api/asset/tag.ts @@ -3,7 +3,7 @@ * * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ -export type Tag = { +export type AssetTag = { /** * Tag ID. */ @@ -40,14 +40,14 @@ export type Tag = { * * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ -export type GetTagsResult = { items: Tag[] } +export type GetAssetTagsResult = { items: AssetTag[] } /** * Parameters for creating a tag in the asset API. * * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ -export type PostTagParams = { +export type PostAssetTagParams = { name: string } @@ -56,4 +56,4 @@ export type PostTagParams = { * * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ -export type PostTagResult = Tag +export type PostAssetTagResult = AssetTag diff --git a/src/types/api/migration/document.ts b/src/types/api/migration/document.ts index e0ddf549..e8662236 100644 --- a/src/types/api/migration/document.ts +++ b/src/types/api/migration/document.ts @@ -44,7 +44,7 @@ export type PostDocumentResult< lang: TLang } & (TDocument["uid"] extends string ? { uid: TDocument["uid"] } - : Record) + : { uid?: TDocument["uid"] }) : never /** diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts new file mode 100644 index 00000000..b4e86c70 --- /dev/null +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -0,0 +1,151 @@ +import type { TestContext } from "vitest" + +import { rest } from "msw" + +import { createRepositoryName } from "./createRepositoryName" + +import type { + Asset, + GetAssetsResult, + PostAssetResult, +} from "../../src/types/api/asset/asset" +import type { + AssetTag, + GetAssetTagsResult, + PostAssetTagParams, + PostAssetTagResult, +} from "../../src/types/api/asset/tag" + +type MockPrismicMigrationAPIV2Args = { + ctx: TestContext + writeToken: string + expectedAsset?: Asset + expectedAssets?: Asset[] + expectedTag?: AssetTag + expectedTags?: AssetTag[] +} + +const DEFAULT_TAG: AssetTag = { + id: "091daea1-4954-46c9-a819-faabb69464ea", + name: "fooTag", + uploader_id: "uploaded_id", + created_at: 0, + last_modified: 0, + count: 0, +} + +const DEFAULT_ASSET: Asset = { + id: "Yz7kzxAAAB0AREK7", + uploader_id: "uploader_id", + created_at: 0, + last_modified: 0, + kind: "image", + filename: "foo.jpg", + extension: "jpg", + url: "https://example.com/foo.jpg", + width: 1, + height: 1, + size: 1, + notes: "notes", + credits: "credits", + alt: "alt", + origin_url: "origin_url", + search_highlight: { filename: [], notes: [], credits: [], alt: [] }, + tags: [], +} + +export const mockPrismicRestAPIV2 = ( + args: MockPrismicMigrationAPIV2Args, +): void => { + const repositoryName = createRepositoryName() + const assetAPIEndpoint = `https://asset-api.prismic.io` + + args.ctx.server.use( + rest.get(`${assetAPIEndpoint}/assets`, async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401)) + } + + const items: Asset[] = args.expectedAssets || [DEFAULT_ASSET] + + const response: GetAssetsResult = { + total: items.length, + items, + is_opensearch_result: false, + cursor: undefined, + missing_ids: [], + } + + return res(ctx.json(response)) + }), + ) + + args.ctx.server.use( + rest.post(`${assetAPIEndpoint}/assets`, async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401)) + } + + const response: PostAssetResult = args.expectedAsset || DEFAULT_ASSET + + return res(ctx.json(response)) + }), + ) + + args.ctx.server.use( + rest.patch(`${assetAPIEndpoint}/assets/:id`, async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401)) + } + + const response: PostAssetResult = args.expectedAsset || DEFAULT_ASSET + + return res(ctx.json(response)) + }), + ) + + args.ctx.server.use( + rest.get(`${assetAPIEndpoint}/tags`, async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401)) + } + + const items: AssetTag[] = args.expectedTags || [DEFAULT_TAG] + + const response: GetAssetTagsResult = { items } + + return res(ctx.json(response)) + }), + ) + + args.ctx.server.use( + rest.post(`${assetAPIEndpoint}/tags`, async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401)) + } + + const body = await req.json() + const response: PostAssetTagResult = args.expectedTag || { + ...DEFAULT_TAG, + name: body.name, + } + + return res(ctx.json(201), ctx.json(response)) + }), + ) +} diff --git a/test/__testutils__/mockPrismicMigrationAPI.ts b/test/__testutils__/mockPrismicMigrationAPI.ts new file mode 100644 index 00000000..a619ad15 --- /dev/null +++ b/test/__testutils__/mockPrismicMigrationAPI.ts @@ -0,0 +1,81 @@ +import type { TestContext } from "vitest" + +import { rest } from "msw" + +import { createRepositoryName } from "./createRepositoryName" + +import type { + PostDocumentParams, + PostDocumentResult, + PutDocumentParams, + PutDocumentResult, +} from "../../src/types/api/migration/document" + +type MockPrismicMigrationAPIV2Args = { + ctx: TestContext + writeToken: string + migrationAPIKey: string + expectedID?: string +} + +export const mockPrismicRestAPIV2 = ( + args: MockPrismicMigrationAPIV2Args, +): void => { + const repositoryName = createRepositoryName() + const migrationAPIEndpoint = `https://migration.prismic.io` + + args.ctx.server.use( + rest.post(`${migrationAPIEndpoint}/documents`, async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("x-apy-key") !== args.migrationAPIKey || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401)) + } + + const id = args.expectedID || args.ctx.mock.value.document().id + const body = await req.json() + + const response: PostDocumentResult = { + title: body.title, + id, + lang: body.lang, + type: body.type, + uid: body.uid, + } + + return res(ctx.status(201), ctx.json(response)) + }), + ) + + args.ctx.server.use( + rest.put(`${migrationAPIEndpoint}/documents/:id`, async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("x-apy-key") !== args.migrationAPIKey || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401)) + } + + if (req.params.id !== args.expectedID) { + return res(ctx.status(404)) + } + + const document = args.ctx.mock.value.document() + const id = args.expectedID || document.id + const body = await req.json() + + const response: PutDocumentResult = { + title: body.title || args.ctx.mock.value.keyText({ state: "filled" }), + id, + lang: document.lang, + type: document.type, + uid: body.uid, + } + + return res(ctx.json(response)) + }), + ) +} diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts new file mode 100644 index 00000000..22a760fb --- /dev/null +++ b/test/writeClient-migrate.test.ts @@ -0,0 +1,12 @@ +import { expect, it, vi } from "vitest" + +import * as prismic from "../src" + +it("createWriteClient creates a write client", () => { + const client = prismic.createWriteClient("qwerty", { + writeToken: "xxx", + migrationAPIKey: "yyy", + }) + + expect(client).toBeInstanceOf(prismic.WriteClient) +}) From 55cb47607bf911647ed6e0a5ac46f7f725fc7dc2 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 2 Sep 2024 17:25:15 +0200 Subject: [PATCH 19/61] style: lint --- test/writeClient-migrate.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index 22a760fb..164f97a5 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -1,4 +1,4 @@ -import { expect, it, vi } from "vitest" +import { expect, it } from "vitest" import * as prismic from "../src" From e3fb63df120e49d27e7d5b16ddb6259be9affe59 Mon Sep 17 00:00:00 2001 From: lihbr Date: Tue, 3 Sep 2024 18:17:13 +0200 Subject: [PATCH 20/61] test: `WriteClient` core methods --- src/BaseClient.ts | 23 +- src/WriteClient.ts | 2 +- src/lib/pLimit.ts | 11 +- test/__testutils__/createRepositoryName.ts | 5 +- test/__testutils__/createWriteClient.ts | 30 +++ test/__testutils__/mockPrismicAssetAPI.ts | 95 ++++--- test/__testutils__/mockPrismicMigrationAPI.ts | 39 ++- test/writeClient-createAsset.test.ts | 224 ++++++++++++++++ test/writeClient-createAssetTag.test.ts | 100 ++++++++ test/writeClient-createDocument.test.ts | 102 ++++++++ test/writeClient-getAssets.test.ts | 241 ++++++++++++++++++ test/writeClient-migrate.test.ts | 20 +- test/writeClient-updateDocument.test.ts | 104 ++++++++ test/writeClient.test.ts | 2 +- 14 files changed, 904 insertions(+), 94 deletions(-) create mode 100644 test/__testutils__/createWriteClient.ts create mode 100644 test/writeClient-createAsset.test.ts create mode 100644 test/writeClient-createAssetTag.test.ts create mode 100644 test/writeClient-createDocument.test.ts create mode 100644 test/writeClient-getAssets.test.ts create mode 100644 test/writeClient-updateDocument.test.ts diff --git a/src/BaseClient.ts b/src/BaseClient.ts index bc3ab0fb..329d0e63 100644 --- a/src/BaseClient.ts +++ b/src/BaseClient.ts @@ -2,6 +2,13 @@ import { type LimitFunction, pLimit } from "./lib/pLimit" import { PrismicError } from "./errors/PrismicError" +/** + * The default delay used with APIs not providing rate limit headers. + * + * @internal + */ +export const UNKNOWN_RATE_LIMIT_DELAY = 1500 + /** * A universal API to make network requests. A subset of the `fetch()` API. * @@ -241,25 +248,13 @@ export class BaseClient { if (!this.queuedFetchJobs[hostname]) { this.queuedFetchJobs[hostname] = pLimit({ - limit: 1, - interval: 1500, + interval: UNKNOWN_RATE_LIMIT_DELAY, }) } - const job = this.queuedFetchJobs[hostname](() => + return this.queuedFetchJobs[hostname](() => this.createFetchJob(url, requestInit), ) - - job.finally(() => { - if ( - this.queuedFetchJobs[hostname] && - this.queuedFetchJobs[hostname].queueSize === 0 - ) { - delete this.queuedFetchJobs[hostname] - } - }) - - return job } private dedupeFetch( diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 0024bc26..a0cc41a3 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -816,7 +816,7 @@ export class WriteClient< /** * {@link resolveAssetTagIDs} rate limiter. */ - private _resolveAssetTagIDsLimit = pLimit({ limit: 1 }) + private _resolveAssetTagIDsLimit = pLimit() /** * Resolves asset tag IDs from tag names or IDs. diff --git a/src/lib/pLimit.ts b/src/lib/pLimit.ts index a5365e51..0f929623 100644 --- a/src/lib/pLimit.ts +++ b/src/lib/pLimit.ts @@ -32,20 +32,15 @@ export type LimitFunction = { } export const pLimit = ({ - limit, interval, -}: { - limit: number - interval?: number -}): LimitFunction => { +}: { interval?: number } = {}): LimitFunction => { const queue: AnyFunction[] = [] let activeCount = 0 let lastCompletion = 0 const resumeNext = () => { - if (activeCount < limit && queue.length > 0) { + if (activeCount === 0 && queue.length > 0) { queue.shift()?.() - // Since `pendingCount` has been decreased by one, increase `activeCount` by one. activeCount++ } } @@ -96,7 +91,7 @@ export const pLimit = ({ // needs to happen asynchronously as well to get an up-to-date value for `activeCount`. await Promise.resolve() - if (activeCount < limit) { + if (activeCount === 0) { resumeNext() } })() diff --git a/test/__testutils__/createRepositoryName.ts b/test/__testutils__/createRepositoryName.ts index b049e632..1eb8880e 100644 --- a/test/__testutils__/createRepositoryName.ts +++ b/test/__testutils__/createRepositoryName.ts @@ -1,9 +1,10 @@ +import type { TaskContext } from "vitest" import { expect } from "vitest" import * as crypto from "node:crypto" -export const createRepositoryName = (): string => { - const seed = expect.getState().currentTestName +export const createRepositoryName = (ctx?: TaskContext): string => { + const seed = ctx?.task.name || expect.getState().currentTestName if (!seed) { throw new Error( `createRepositoryName() can only be called within a Vitest test.`, diff --git a/test/__testutils__/createWriteClient.ts b/test/__testutils__/createWriteClient.ts new file mode 100644 index 00000000..962d8ceb --- /dev/null +++ b/test/__testutils__/createWriteClient.ts @@ -0,0 +1,30 @@ +import type { TaskContext } from "vitest" + +import fetch from "node-fetch" + +import { createRepositoryName } from "./createRepositoryName" + +import * as prismic from "../../src" + +type CreateTestWriteClientArgs = { + repositoryName?: string +} & { + ctx: TaskContext + clientConfig?: Partial +} + +export const createTestWriteClient = ( + args: CreateTestWriteClientArgs, +): prismic.WriteClient => { + const repositoryName = args.repositoryName || createRepositoryName(args.ctx) + + return prismic.createWriteClient(repositoryName, { + fetch, + writeToken: "xxx", + migrationAPIKey: "yyy", + // We create unique endpoints so we can run tests concurrently + assetAPIEndpoint: `https://${repositoryName}.asset-api.prismic.io`, + migrationAPIEndpoint: `https://${repositoryName}.migration.prismic.io`, + ...args.clientConfig, + }) +} diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index b4e86c70..46b23e5b 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -1,12 +1,12 @@ -import type { TestContext } from "vitest" +import { type TestContext } from "vitest" import { rest } from "msw" -import { createRepositoryName } from "./createRepositoryName" - +import type { WriteClient } from "../../src" import type { Asset, GetAssetsResult, + PatchAssetParams, PostAssetResult, } from "../../src/types/api/asset/asset" import type { @@ -16,17 +16,20 @@ import type { PostAssetTagResult, } from "../../src/types/api/asset/tag" -type MockPrismicMigrationAPIV2Args = { +type MockPrismicAssetAPIArgs = { ctx: TestContext - writeToken: string + client: WriteClient + writeToken?: string expectedAsset?: Asset expectedAssets?: Asset[] expectedTag?: AssetTag expectedTags?: AssetTag[] + expectedCursor?: string + getRequiredParams?: Record } const DEFAULT_TAG: AssetTag = { - id: "091daea1-4954-46c9-a819-faabb69464ea", + id: "88888888-4444-4444-4444-121212121212", name: "fooTag", uploader_id: "uploaded_id", created_at: 0, @@ -54,19 +57,30 @@ const DEFAULT_ASSET: Asset = { tags: [], } -export const mockPrismicRestAPIV2 = ( - args: MockPrismicMigrationAPIV2Args, -): void => { - const repositoryName = createRepositoryName() - const assetAPIEndpoint = `https://asset-api.prismic.io` +export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { + const repositoryName = args.client.repositoryName + const assetAPIEndpoint = args.client.assetAPIEndpoint + const writeToken = args.writeToken || args.client.writeToken args.ctx.server.use( - rest.get(`${assetAPIEndpoint}/assets`, async (req, res, ctx) => { + rest.get(`${assetAPIEndpoint}assets`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) + } + + if (args.getRequiredParams) { + for (const paramKey in args.getRequiredParams) { + const requiredValue = args.getRequiredParams[paramKey] + + args.ctx + .expect(req.url.searchParams.getAll(paramKey)) + .toStrictEqual( + Array.isArray(requiredValue) ? requiredValue : [requiredValue], + ) + } } const items: Asset[] = args.expectedAssets || [DEFAULT_ASSET] @@ -75,51 +89,49 @@ export const mockPrismicRestAPIV2 = ( total: items.length, items, is_opensearch_result: false, - cursor: undefined, + cursor: args.expectedCursor, missing_ids: [], } return res(ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.post(`${assetAPIEndpoint}/assets`, async (req, res, ctx) => { + rest.post(`${assetAPIEndpoint}assets`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } const response: PostAssetResult = args.expectedAsset || DEFAULT_ASSET return res(ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.patch(`${assetAPIEndpoint}/assets/:id`, async (req, res, ctx) => { + rest.patch(`${assetAPIEndpoint}assets/:id`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) + } + const { tags, ...body } = await req.json() + + const response: PostAssetResult = { + ...(args.expectedAsset || DEFAULT_ASSET), + ...body, + tags: tags?.length + ? tags.map((id) => ({ ...DEFAULT_TAG, id })) + : (args.expectedAsset || DEFAULT_ASSET).tags, } - - const response: PostAssetResult = args.expectedAsset || DEFAULT_ASSET return res(ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.get(`${assetAPIEndpoint}/tags`, async (req, res, ctx) => { + rest.get(`${assetAPIEndpoint}tags`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } const items: AssetTag[] = args.expectedTags || [DEFAULT_TAG] @@ -128,24 +140,23 @@ export const mockPrismicRestAPIV2 = ( return res(ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.post(`${assetAPIEndpoint}/tags`, async (req, res, ctx) => { + rest.post(`${assetAPIEndpoint}tags`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } const body = await req.json() + const response: PostAssetTagResult = args.expectedTag || { ...DEFAULT_TAG, + id: `${`${Date.now()}`.slice(-8)}-4954-46c9-a819-faabb69464ea`, name: body.name, } - return res(ctx.json(201), ctx.json(response)) + return res(ctx.status(201), ctx.json(response)) }), ) } diff --git a/test/__testutils__/mockPrismicMigrationAPI.ts b/test/__testutils__/mockPrismicMigrationAPI.ts index a619ad15..6a31d997 100644 --- a/test/__testutils__/mockPrismicMigrationAPI.ts +++ b/test/__testutils__/mockPrismicMigrationAPI.ts @@ -2,8 +2,7 @@ import type { TestContext } from "vitest" import { rest } from "msw" -import { createRepositoryName } from "./createRepositoryName" - +import type { WriteClient } from "../../src" import type { PostDocumentParams, PostDocumentResult, @@ -11,27 +10,30 @@ import type { PutDocumentResult, } from "../../src/types/api/migration/document" -type MockPrismicMigrationAPIV2Args = { +type MockPrismicMigrationAPIArgs = { ctx: TestContext - writeToken: string - migrationAPIKey: string + client: WriteClient + writeToken?: string + migrationAPIKey?: string expectedID?: string } -export const mockPrismicRestAPIV2 = ( - args: MockPrismicMigrationAPIV2Args, +export const mockPrismicMigrationAPI = ( + args: MockPrismicMigrationAPIArgs, ): void => { - const repositoryName = createRepositoryName() - const migrationAPIEndpoint = `https://migration.prismic.io` + const repositoryName = args.client.repositoryName + const migrationAPIEndpoint = args.client.migrationAPIEndpoint + const writeToken = args.writeToken || args.client.writeToken + const migrationAPIKey = args.migrationAPIKey || args.client.migrationAPIKey args.ctx.server.use( - rest.post(`${migrationAPIEndpoint}/documents`, async (req, res, ctx) => { + rest.post(`${migrationAPIEndpoint}documents`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || - req.headers.get("x-apy-key") !== args.migrationAPIKey || + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("x-api-key") !== migrationAPIKey || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(403), ctx.json({ Message: "forbidden" })) } const id = args.expectedID || args.ctx.mock.value.document().id @@ -47,16 +49,13 @@ export const mockPrismicRestAPIV2 = ( return res(ctx.status(201), ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.put(`${migrationAPIEndpoint}/documents/:id`, async (req, res, ctx) => { + rest.put(`${migrationAPIEndpoint}documents/:id`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || - req.headers.get("x-apy-key") !== args.migrationAPIKey || + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("x-api-key") !== migrationAPIKey || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(403), ctx.json({ Message: "forbidden" })) } if (req.params.id !== args.expectedID) { diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts new file mode 100644 index 00000000..3fa8d41a --- /dev/null +++ b/test/writeClient-createAsset.test.ts @@ -0,0 +1,224 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" + +import { ForbiddenError } from "../src" +import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" +import type { AssetTag } from "../src/types/api/asset/tag" + +it.concurrent("creates an asset from string content", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg") + + expect(asset.id).toBeTypeOf("string") +}) + +it.concurrent("creates an asset from file content", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset( + new File(["file"], "foo.jpg"), + "foo.jpg", + ) + + expect(asset.id).toBeTypeOf("string") +}) + +it.concurrent("creates an asset with metadata", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + alt: "foo", + credits: "bar", + notes: "baz", + }) + + expect(asset.id).toBeTypeOf("string") +}) + +it.concurrent("creates an asset with an existing tag ID", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const tag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ ctx, client, expectedTags: [tag] }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + tags: [tag.id], + }) + + expect(asset.tags?.[0].id).toEqual(tag.id) +}) + +it.concurrent("creates an asset with an existing tag name", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const tag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ ctx, client, expectedTags: [tag] }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + tags: [tag.name], + }) + + expect(asset.tags?.[0].id).toEqual(tag.id) +}) + +it.concurrent("creates an asset with a new tag name", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const tag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ ctx, client, expectedTag: tag }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + tags: ["foo"], + }) + + expect(asset.tags?.[0].id).toEqual(tag.id) +}) + +it.concurrent("throws if asset has invalid metadata", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const filename = "foo.jpg" + const file = "https://example.com/foo.jpg" + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { notes: "0".repeat(501) }), + "notes", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`notes\` must be at most 500 characters]`, + ) + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { credits: "0".repeat(501) }), + "credits", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`credits\` must be at most 500 characters]`, + ) + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { alt: "0".repeat(501) }), + "alt", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`alt\` must be at most 500 characters]`, + ) + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { tags: ["0"] }), + "tags", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, + ) + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { tags: ["0".repeat(21)] }), + "tags", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, + ) + + await expect( + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { + tags: [ + // Tag name + "012", + // Tag ID + "00000000-4444-4444-4444-121212121212", + ], + }), + "tags", + ).resolves.not.toBeUndefined() +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAsset("file", "foo.jpg"), + ).rejects.toThrow(ForbiddenError) +}) + +// It seems like p-limit and node-fetch are not happy friends :/ +// https://github.com/sindresorhus/p-limit/issues/64 +it.skip("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + + mockPrismicAssetAPI({ ctx, client }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAsset("file", "foo.jpg", { + fetchOptions: { signal: controller.signal }, + }), + ).rejects.toThrow(/aborted/i) +}) + +it.concurrent("respects unknown rate limit", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const args = ["file", "foo.jpg"] + + const start = Date.now() + + // @ts-expect-error - testing purposes + await client.createAsset(...args) + + expect(Date.now() - start).toBeLessThan(UNKNOWN_RATE_LIMIT_DELAY) + + // @ts-expect-error - testing purposes + await client.createAsset(...args) + + expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) +}) diff --git a/test/writeClient-createAssetTag.test.ts b/test/writeClient-createAssetTag.test.ts new file mode 100644 index 00000000..7c1d6d7e --- /dev/null +++ b/test/writeClient-createAssetTag.test.ts @@ -0,0 +1,100 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" + +import { ForbiddenError } from "../src" +import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" +import type { AssetTag } from "../src/types/api/asset/tag" + +it.concurrent("creates a tag", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedTag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ ctx, client, expectedTag }) + + // @ts-expect-error - testing purposes + const tag = await client.createAssetTag(expectedTag.name) + + expect(tag).toStrictEqual(expectedTag) +}) + +it.concurrent("throws if tag is invalid", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("0"), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Asset tag name must be at least 3 characters long and 20 characters at most]`, + ) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("0".repeat(21)), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Asset tag name must be at least 3 characters long and 20 characters at most]`, + ) + + await expect( + // @ts-expect-error - testing purposes + client.createAssetTag("valid"), + ).resolves.not.toBeUndefined() +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("foo"), + ).rejects.toThrow(ForbiddenError) +}) + +// It seems like p-limit and node-fetch are not happy friends :/ +// https://github.com/sindresorhus/p-limit/issues/64 +it.skip("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + + mockPrismicAssetAPI({ ctx, client }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("foo", { + fetchOptions: { signal: controller.signal }, + }), + ).rejects.toThrow(/aborted/i) +}) + +it.concurrent("respects unknown rate limit", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const args = ["foo"] + + const start = Date.now() + + // @ts-expect-error - testing purposes + await client.createAssetTag(...args) + + expect(Date.now() - start).toBeLessThan(UNKNOWN_RATE_LIMIT_DELAY) + + // @ts-expect-error - testing purposes + await client.createAssetTag(...args) + + expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) +}) diff --git a/test/writeClient-createDocument.test.ts b/test/writeClient-createDocument.test.ts new file mode 100644 index 00000000..82a0904c --- /dev/null +++ b/test/writeClient-createDocument.test.ts @@ -0,0 +1,102 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" + +import { ForbiddenError } from "../src" +import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" + +it.concurrent("creates a document", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + // @ts-expect-error - testing purposes + const { id } = await client.createDocument( + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + ) + + expect(id).toBe(expectedID) +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicMigrationAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createDocument( + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + ), + ).rejects.toThrow(ForbiddenError) +}) + +// It seems like p-limit and node-fetch are not happy friends :/ +// https://github.com/sindresorhus/p-limit/issues/64 +it.skip("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + + mockPrismicMigrationAPI({ ctx, client, expectedID: "foo" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createDocument( + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + { fetchOptions: { signal: controller.signal } }, + ), + ).rejects.toThrow(/aborted/i) +}) + +it.concurrent("respects unknown rate limit", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + const args = [ + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + ] + + const start = Date.now() + + // @ts-expect-error - testing purposes + await client.createDocument(...args) + + expect(Date.now() - start).toBeLessThan(UNKNOWN_RATE_LIMIT_DELAY) + + // @ts-expect-error - testing purposes + await client.createDocument(...args) + + expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) +}) diff --git a/test/writeClient-getAssets.test.ts b/test/writeClient-getAssets.test.ts new file mode 100644 index 00000000..f206e706 --- /dev/null +++ b/test/writeClient-getAssets.test.ts @@ -0,0 +1,241 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" + +import { ForbiddenError } from "../src" +import { AssetType } from "../src/types/api/asset/asset" + +it.concurrent("get assets", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const { results } = await client.getAssets() + + expect(results).toBeInstanceOf(Array) +}) + +it.concurrent("supports `pageSize` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ + ctx, + client, + getRequiredParams: { + pageSize: "10", + }, + }) + + const { results } = await client.getAssets({ + pageSize: 10, + }) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `cursor` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + cursor: "foo", + } + + mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `assetType` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + assetType: AssetType.Image, + } + + mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `keyword` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + keyword: "foo", + } + + mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `ids` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + ids: ["foo", "bar"], + } + + mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `tags` parameter (id)", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + tags: [ + "00000000-4444-4444-4444-121212121212", + "10000000-4444-4444-4444-121212121212", + ], + } + + mockPrismicAssetAPI({ + ctx, + client, + expectedTags: [ + { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + }, + { + id: "10000000-4444-4444-4444-121212121212", + name: "bar", + created_at: 0, + last_modified: 0, + }, + ], + getRequiredParams, + }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `tags` parameter (name)", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + tags: [ + "00000000-4444-4444-4444-121212121212", + "10000000-4444-4444-4444-121212121212", + ], + } + + mockPrismicAssetAPI({ + ctx, + client, + expectedTags: [ + { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + }, + { + id: "10000000-4444-4444-4444-121212121212", + name: "bar", + created_at: 0, + last_modified: 0, + }, + ], + getRequiredParams, + }) + + const { results } = await client.getAssets({ tags: ["foo", "bar"] }) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `tags` parameter (missing)", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + tags: ["00000000-4444-4444-4444-121212121212"], + } + + mockPrismicAssetAPI({ + ctx, + client, + expectedTags: [ + { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + }, + ], + getRequiredParams, + }) + + const { results } = await client.getAssets({ tags: ["foo", "bar"] }) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("returns `next` when next `cursor` is available", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const { next: next1 } = await client.getAssets() + + ctx.expect(next1).toBeUndefined() + + const cursor = "foo" + mockPrismicAssetAPI({ ctx, client, expectedCursor: cursor }) + + const { next: next2 } = await client.getAssets() + + ctx.expect(next2).toBeInstanceOf(Function) + + mockPrismicAssetAPI({ ctx, client, getRequiredParams: { cursor } }) + + const { results } = await next2!() + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(4) +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => client.getAssets()).rejects.toThrow(ForbiddenError) +}) + +it.concurrent("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + + mockPrismicAssetAPI({ ctx, client }) + + await expect(() => + client.getAssets({ + fetchOptions: { signal: controller.signal }, + }), + ).rejects.toThrow(/aborted/i) +}) diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index 164f97a5..dce47ec8 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -1,12 +1,20 @@ import { expect, it } from "vitest" +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" +import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" + import * as prismic from "../src" -it("createWriteClient creates a write client", () => { - const client = prismic.createWriteClient("qwerty", { - writeToken: "xxx", - migrationAPIKey: "yyy", - }) +it("migrates nothing when migration is empty", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() - expect(client).toBeInstanceOf(prismic.WriteClient) + await expect(client.migrate(migration)).resolves.toBeUndefined() }) diff --git a/test/writeClient-updateDocument.test.ts b/test/writeClient-updateDocument.test.ts new file mode 100644 index 00000000..0b744be0 --- /dev/null +++ b/test/writeClient-updateDocument.test.ts @@ -0,0 +1,104 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" + +import { ForbiddenError, NotFoundError } from "../src" +import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" + +it.concurrent("updates a document", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + await expect( + // @ts-expect-error - testing purposes + client.updateDocument(expectedID, { + uid: "uid", + data: {}, + }), + ).resolves.toBeUndefined() +}) + +it.concurrent("throws not found error on not found ID", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicMigrationAPI({ ctx, client, expectedID: "not-found" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.updateDocument("foo", { + uid: "uid", + data: {}, + }), + ).rejects.toThrow(NotFoundError) +}) + +it.concurrent("respects unknown rate limit", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + const args = [ + expectedID, + { + uid: "uid", + data: {}, + }, + ] + + const start = Date.now() + + // @ts-expect-error - testing purposes + await client.updateDocument(...args) + + expect(Date.now() - start).toBeLessThan(UNKNOWN_RATE_LIMIT_DELAY) + + // @ts-expect-error - testing purposes + await client.updateDocument(...args) + + expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicMigrationAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.updateDocument("foo", { + uid: "uid", + data: {}, + }), + ).rejects.toThrow(ForbiddenError) +}) + +// It seems like p-limit and node-fetch are not happy friends :/ +// https://github.com/sindresorhus/p-limit/issues/64 +it.skip("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + const controller = new AbortController() + controller.abort() + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + await expect(() => + // @ts-expect-error - testing purposes + client.updateDocument( + expectedID, + { + uid: "uid", + data: {}, + }, + { fetchOptions: { signal: controller.signal } }, + ), + ).rejects.toThrow(/aborted/i) +}) diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts index ef82d67f..bb8ea86b 100644 --- a/test/writeClient.test.ts +++ b/test/writeClient.test.ts @@ -2,7 +2,7 @@ import { expect, it, vi } from "vitest" import * as prismic from "../src" -it("createWriteClient creates a write client", () => { +it("`createWriteClient` creates a write client", () => { const client = prismic.createWriteClient("qwerty", { writeToken: "xxx", migrationAPIKey: "yyy", From 97a9e1397273981ad2a3f99ee435f7901ce8707e Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 4 Sep 2024 11:20:27 +0200 Subject: [PATCH 21/61] chore(deps): bump `@prismicio/mock` --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf16c921..ed54e589 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "imgix-url-builder": "^0.0.5" }, "devDependencies": { - "@prismicio/mock": "^0.3.7", + "@prismicio/mock": "^0.3.8", "@prismicio/types-internal": "^2.6.0", "@size-limit/preset-small-lib": "^11.1.4", "@trivago/prettier-plugin-sort-imports": "^4.3.0", @@ -1212,9 +1212,9 @@ } }, "node_modules/@prismicio/mock": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@prismicio/mock/-/mock-0.3.7.tgz", - "integrity": "sha512-aXNSWMVTSanVSVxgIw0q+1YHj33Yv9GjVX9MuWBIQddf0dmDSMSiLEmDG1LSCgWq2cpukLMf/4eSo3P4dwG/kQ==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@prismicio/mock/-/mock-0.3.8.tgz", + "integrity": "sha512-kc7MrxxyD2uzl8kAzc51CNHPrJTfKDxtJ0FLO0AcoODQUnd12JxumLvNY+O6ydbH6Zg8A56FGt22SH3yIxtcAQ==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index e049548b..d4f49a80 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "imgix-url-builder": "^0.0.5" }, "devDependencies": { - "@prismicio/mock": "^0.3.7", + "@prismicio/mock": "^0.3.8", "@prismicio/types-internal": "^2.6.0", "@size-limit/preset-small-lib": "^11.1.4", "@trivago/prettier-plugin-sort-imports": "^4.3.0", From 58f17656456c897ff2b20431bfedb03039b45c15 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 4 Sep 2024 11:30:16 +0200 Subject: [PATCH 22/61] fix tests on old node version --- test/migration-createAsset.test.ts | 6 +++++- test/writeClient-createAsset.test.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/migration-createAsset.test.ts b/test/migration-createAsset.test.ts index 7cc8e41a..eade34de 100644 --- a/test/migration-createAsset.test.ts +++ b/test/migration-createAsset.test.ts @@ -1,9 +1,13 @@ -import { expect, it } from "vitest" +import { it as _it, expect } from "vitest" import * as prismic from "../src" import type { Asset } from "../src/types/api/asset/asset" import { AssetType } from "../src/types/api/asset/asset" +// Skip test on Node 16 and 18 +const hasFileConstructor = typeof File === "function" +const it = _it.skipIf(!hasFileConstructor) + it("creates an asset from a url", () => { const migration = prismic.createMigration() diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts index 3fa8d41a..34eaabf8 100644 --- a/test/writeClient-createAsset.test.ts +++ b/test/writeClient-createAsset.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest" +import { it as _it, expect } from "vitest" import { createTestWriteClient } from "./__testutils__/createWriteClient" import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" @@ -7,6 +7,10 @@ import { ForbiddenError } from "../src" import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" import type { AssetTag } from "../src/types/api/asset/tag" +// Skip test on Node 16 and 18 +const hasFileConstructor = typeof File === "function" +const it = _it.skipIf(!hasFileConstructor) + it.concurrent("creates an asset from string content", async (ctx) => { const client = createTestWriteClient({ ctx }) From 84ed67b6e54249fcb2a65e161f0917d8c813d4a8 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 4 Sep 2024 13:43:22 +0200 Subject: [PATCH 23/61] test: skip tests on specific node version --- test/migration-createAsset.test.ts | 7 ++++--- test/writeClient-createAsset.test.ts | 7 ++++--- test/writeClient-createAssetTag.test.ts | 6 +++++- test/writeClient-getAssets.test.ts | 6 +++++- test/writeClient-migrate.test.ts | 24 +++++++++++++++++++++--- test/writeClient.test.ts | 4 ++++ 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/test/migration-createAsset.test.ts b/test/migration-createAsset.test.ts index eade34de..e22da05d 100644 --- a/test/migration-createAsset.test.ts +++ b/test/migration-createAsset.test.ts @@ -4,9 +4,10 @@ import * as prismic from "../src" import type { Asset } from "../src/types/api/asset/asset" import { AssetType } from "../src/types/api/asset/asset" -// Skip test on Node 16 and 18 -const hasFileConstructor = typeof File === "function" -const it = _it.skipIf(!hasFileConstructor) +// Skip test on Node 16 and 18 (File and FormData support) +const isNode16 = process.version.startsWith("v16") +const isNode18 = process.version.startsWith("v18") +const it = _it.skipIf(isNode16 || isNode18) it("creates an asset from a url", () => { const migration = prismic.createMigration() diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts index 34eaabf8..11748113 100644 --- a/test/writeClient-createAsset.test.ts +++ b/test/writeClient-createAsset.test.ts @@ -7,9 +7,10 @@ import { ForbiddenError } from "../src" import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" import type { AssetTag } from "../src/types/api/asset/tag" -// Skip test on Node 16 and 18 -const hasFileConstructor = typeof File === "function" -const it = _it.skipIf(!hasFileConstructor) +// Skip test on Node 16 and 18 (File and FormData support) +const isNode16 = process.version.startsWith("v16") +const isNode18 = process.version.startsWith("v18") +const it = _it.skipIf(isNode16 || isNode18) it.concurrent("creates an asset from string content", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-createAssetTag.test.ts b/test/writeClient-createAssetTag.test.ts index 7c1d6d7e..d153ecc1 100644 --- a/test/writeClient-createAssetTag.test.ts +++ b/test/writeClient-createAssetTag.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest" +import { it as _it, expect } from "vitest" import { createTestWriteClient } from "./__testutils__/createWriteClient" import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" @@ -7,6 +7,10 @@ import { ForbiddenError } from "../src" import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" import type { AssetTag } from "../src/types/api/asset/tag" +// Skip test on Node 16 (FormData support) +const isNode16 = process.version.startsWith("v16") +const it = _it.skipIf(isNode16) + it.concurrent("creates a tag", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-getAssets.test.ts b/test/writeClient-getAssets.test.ts index f206e706..e3c694bd 100644 --- a/test/writeClient-getAssets.test.ts +++ b/test/writeClient-getAssets.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest" +import { it as _it, expect } from "vitest" import { createTestWriteClient } from "./__testutils__/createWriteClient" import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" @@ -6,6 +6,10 @@ import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" import { ForbiddenError } from "../src" import { AssetType } from "../src/types/api/asset/asset" +// Skip test on Node 16 (FormData support) +const isNode16 = process.version.startsWith("v16") +const it = _it.skipIf(isNode16) + it.concurrent("get assets", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index dce47ec8..342032a7 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -1,4 +1,4 @@ -import { expect, it } from "vitest" +import { it as _it, expect, vi } from "vitest" import { createTestWriteClient } from "./__testutils__/createWriteClient" import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" @@ -7,14 +7,32 @@ import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" import * as prismic from "../src" +// Skip test on Node 16 and 18 (File and FormData support) +const isNode16 = process.version.startsWith("v16") +const isNode18 = process.version.startsWith("v18") +const it = _it.skipIf(isNode16 || isNode18) + it("migrates nothing when migration is empty", async (ctx) => { const client = createTestWriteClient({ ctx }) mockPrismicRestAPIV2({ ctx }) mockPrismicAssetAPI({ ctx, client }) - mockPrismicMigrationAPI({ ctx, client }) + // Not mocking migration API to test we're not calling it + // mockPrismicMigrationAPI({ ctx, client }) const migration = prismic.createMigration() - await expect(client.migrate(migration)).resolves.toBeUndefined() + const reporter = vi.fn() + + await expect(client.migrate(migration, { reporter })).resolves.toBeUndefined() + + expect(reporter).toHaveBeenCalledWith({ + type: "end", + data: { + migrated: { + documents: 0, + assets: 0, + }, + }, + }) }) diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts index bb8ea86b..b2ba0466 100644 --- a/test/writeClient.test.ts +++ b/test/writeClient.test.ts @@ -4,6 +4,7 @@ import * as prismic from "../src" it("`createWriteClient` creates a write client", () => { const client = prismic.createWriteClient("qwerty", { + fetch: vi.fn(), writeToken: "xxx", migrationAPIKey: "yyy", }) @@ -20,6 +21,7 @@ it("constructor warns if running in a browser-like environment", () => { .mockImplementation(() => void 0) prismic.createWriteClient("qwerty", { + fetch: vi.fn(), writeToken: "xxx", migrationAPIKey: "yyy", }) @@ -34,6 +36,7 @@ it("constructor warns if running in a browser-like environment", () => { it("uses provided asset API endpoint and adds `/` suffix", () => { const client = prismic.createWriteClient("qwerty", { + fetch: vi.fn(), assetAPIEndpoint: "https://example.com", writeToken: "xxx", migrationAPIKey: "yyy", @@ -44,6 +47,7 @@ it("uses provided asset API endpoint and adds `/` suffix", () => { it("uses provided migration API endpoint and adds `/` suffix", () => { const client = prismic.createWriteClient("qwerty", { + fetch: vi.fn(), migrationAPIEndpoint: "https://example.com", writeToken: "xxx", migrationAPIKey: "yyy", From 4116f8c3407f27aa0e9f71a5b6890595ad2aeb96 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 4 Sep 2024 13:45:58 +0200 Subject: [PATCH 24/61] style: unused variable --- test/writeClient-migrate.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index 342032a7..1d14a22b 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -2,7 +2,6 @@ import { it as _it, expect, vi } from "vitest" import { createTestWriteClient } from "./__testutils__/createWriteClient" import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" -import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" import * as prismic from "../src" From c87934e69f2363a4bba3f008cb0a0d0e2b43f170 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 4 Sep 2024 13:48:36 +0200 Subject: [PATCH 25/61] refactor: `WriteClient.migrate` reporter --- src/WriteClient.ts | 298 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 238 insertions(+), 60 deletions(-) diff --git a/src/WriteClient.ts b/src/WriteClient.ts index a0cc41a3..db3b29d7 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -24,6 +24,7 @@ import { type PostDocumentResult, type PutDocumentParams, } from "./types/api/migration/document" +import type { MigrationAsset } from "./types/migration/asset" import type { PrismicMigrationDocument, PrismicMigrationDocumentParams, @@ -55,6 +56,101 @@ type ExtractDocumentType< ? TDocuments : Extract +/** + * Utility type to construct events reported by the migration process. + */ +type ReporterEvent = TData extends never + ? { type: TType } + : { + type: TType + data: TData + } + +/** + * A map of event types and their data reported by the migration process. + */ +type ReporterEventMap = { + start: { + pending: { + documents: number + assets: number + } + } + end: { + migrated: { + documents: number + assets: number + } + } + "assets:existing": { + existing: number + } + "assets:skipping": { + reason: string + index: number + remaining: number + total: number + asset: MigrationAsset + } + "assets:creating": { + index: number + remaining: number + total: number + asset: MigrationAsset + } + "assets:created": { + created: number + assets: AssetMap + } + "documents:masterLocale": { + masterLocale: string + } + "documents:existing": { + existing: number + } + "documents:skipping": { + reason: string + index: number + remaining: number + total: number + document: PrismicMigrationDocument + documentParams: PrismicMigrationDocumentParams + } + "documents:creating": { + index: number + remaining: number + total: number + document: PrismicMigrationDocument + documentParams: PrismicMigrationDocumentParams + } + "documents:created": { + created: number + documents: DocumentMap + } + "documents:updating": { + index: number + remaining: number + total: number + document: PrismicMigrationDocument + documentParams: PrismicMigrationDocumentParams + } + "documents:updated": { + updated: number + } +} + +/** + * Available event types reported by the migration process. + */ +type ReporterEventTypes = keyof ReporterEventMap + +/** + * All events reported by the migration process. + */ +type ReporterEvents = { + [K in ReporterEventTypes]: ReporterEvent +}[ReporterEventTypes] + /** * A query response from the Prismic asset API. The response contains pagination * metadata and a list of matching results for the query. @@ -271,26 +367,33 @@ export class WriteClient< { reporter, ...params - }: { reporter?: (message: string) => void } & FetchParams = {}, + }: { reporter?: (event: ReporterEvents) => void } & FetchParams = {}, ): Promise { - const assets = await this.migrateCreateAssets(migration, { - reporter: (message) => reporter?.(`01_createAssets - ${message}`), - ...params, + reporter?.({ + type: "start", + data: { + pending: { + documents: migration.documents.length, + assets: migration.assets.size, + }, + }, }) - const documents = await this.migrateCreateDocuments(migration, { - reporter: (message) => reporter?.(`02_createDocuments - ${message}`), - ...params, - }) + const assets = await this.migrateCreateAssets(migration, params) - await this.migrateUpdateDocuments(migration, assets, documents, { - reporter: (message) => reporter?.(`03_updateDocuments - ${message}`), - ...params, - }) + const documents = await this.migrateCreateDocuments(migration, params) - reporter?.( - `Migration complete, migrated ${migration.documents.length} documents and ${migration.assets.size} assets`, - ) + await this.migrateUpdateDocuments(migration, assets, documents, params) + + reporter?.({ + type: "end", + data: { + migrated: { + documents: migration.documents.length, + assets: migration.assets.size, + }, + }, + }) } /** @@ -308,7 +411,7 @@ export class WriteClient< { reporter, ...fetchParams - }: { reporter?: (message: string) => void } & FetchParams = {}, + }: { reporter?: (event: ReporterEvents) => void } & FetchParams = {}, ): Promise { const assets: AssetMap = new Map() @@ -329,15 +432,46 @@ export class WriteClient< } } while (getAssetsResult?.next) - reporter?.(`Found ${assets.size} existing assets`) + reporter?.({ + type: "assets:existing", + data: { + existing: assets.size, + }, + }) // Create assets + let i = 0 + let created = 0 if (migration.assets.size) { - let i = 0 - for (const [_, { id, file, filename, ...params }] of migration.assets) { - reporter?.(`Creating asset - ${++i}/${migration.assets.size}`) + for (const [_, migrationAsset] of migration.assets) { + if ( + typeof migrationAsset.id === "string" && + assets.has(migrationAsset.id) + ) { + reporter?.({ + type: "assets:skipping", + data: { + reason: "already exists", + index: ++i, + remaining: migration.assets.size - i, + total: migration.assets.size, + asset: migrationAsset, + }, + }) + } else { + created++ + reporter?.({ + type: "assets:creating", + data: { + index: ++i, + remaining: migration.assets.size - i, + total: migration.assets.size, + asset: migrationAsset, + }, + }) + + const { id, file, filename, ...params } = migrationAsset - if (typeof id !== "string" || !assets.has(id)) { let resolvedFile: PostAssetParams["file"] | File if (typeof file === "string") { let url: URL | undefined @@ -372,12 +506,16 @@ export class WriteClient< assets.set(id, asset) } } - - reporter?.(`Created ${i} assets`) - } else { - reporter?.(`No assets to create`) } + reporter?.({ + type: "assets:created", + data: { + created, + assets, + }, + }) + return assets } @@ -396,18 +534,27 @@ export class WriteClient< { reporter, ...fetchParams - }: { reporter?: (message: string) => void } & FetchParams = {}, + }: { reporter?: (event: ReporterEvents) => void } & FetchParams = {}, ): Promise> { // Should this be a function of `Client`? // Resolve master locale const repository = await this.getRepository(fetchParams) const masterLocale = repository.languages.find((lang) => lang.is_master)!.id - reporter?.(`Resolved master locale \`${masterLocale}\``) + reporter?.({ + type: "documents:masterLocale", + data: { + masterLocale, + }, + }) // Get all existing documents const existingDocuments = await this.dangerouslyGetAll(fetchParams) - - reporter?.(`Found ${existingDocuments.length} existing documents`) + reporter?.({ + type: "documents:existing", + data: { + existing: existingDocuments.length, + }, + }) const documents: DocumentMap = new Map() for (const document of existingDocuments) { @@ -436,17 +583,32 @@ export class WriteClient< if (sortedDocuments.length) { for (const { document, params } of sortedDocuments) { if (document.id && documents.has(document.id)) { - reporter?.( - `Skipping existing document \`${params.documentName}\` - ${++i}/${sortedDocuments.length}`, - ) + reporter?.({ + type: "documents:skipping", + data: { + reason: "already exists", + index: ++i, + remaining: sortedDocuments.length - i, + total: sortedDocuments.length, + document, + documentParams: params, + }, + }) // Index the migration document documents.set(document, documents.get(document.id)!) } else { created++ - reporter?.( - `Creating document \`${params.documentName}\` - ${++i}/${sortedDocuments.length}`, - ) + reporter?.({ + type: "documents:creating", + data: { + index: ++i, + remaining: sortedDocuments.length - i, + total: sortedDocuments.length, + document, + documentParams: params, + }, + }) // Resolve master language document ID for non-master locale documents let masterLanguageDocumentID: string | undefined @@ -490,11 +652,13 @@ export class WriteClient< } } - if (created > 0) { - reporter?.(`Created ${created} documents`) - } else { - reporter?.(`No documents to create`) - } + reporter?.({ + type: "documents:created", + data: { + created, + documents, + }, + }) return documents } @@ -517,29 +681,43 @@ export class WriteClient< { reporter, ...fetchParams - }: { reporter?: (message: string) => void } & FetchParams = {}, + }: { reporter?: (event: ReporterEvents) => void } & FetchParams = {}, ): Promise { - if (migration.documents.length) { - let i = 0 - for (const { document, params } of migration.documents) { - reporter?.( - `Updating document \`${params.documentName}\` - ${++i}/${migration.documents.length}`, - ) - - const { id, uid } = documents.get(document)! - const data = await patchMigrationDocumentData( - document.data, - assets, - documents, - ) - - await this.updateDocument(id, { uid, data }, fetchParams) - } + let i = 0 + for (const { document, params } of migration.documents) { + reporter?.({ + type: "documents:updating", + data: { + index: ++i, + remaining: migration.documents.length - i, + total: migration.documents.length, + document, + documentParams: params, + }, + }) + + const { id, uid } = documents.get(document)! + const data = await patchMigrationDocumentData( + document.data, + assets, + documents, + ) - reporter?.(`Updated ${i} documents`) - } else { - reporter?.(`No documents to update`) + await this.updateDocument( + id, + // We need to forward again document name and tags to update them + // in case the document already existed during the previous step. + { documentName: params.documentName, uid, tags: document.tags, data }, + fetchParams, + ) } + + reporter?.({ + type: "documents:updated", + data: { + updated: migration.documents.length, + }, + }) } /** From 0103db9bb959ff365d268baa2121088a572e61de Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 4 Sep 2024 16:58:34 +0200 Subject: [PATCH 26/61] refactor: use more predictable asset API in tests --- src/WriteClient.ts | 38 ++++++----- src/index.ts | 2 +- test/__testutils__/mockPrismicAssetAPI.ts | 69 +++++++++++++------- test/writeClient-createAsset.test.ts | 6 +- test/writeClient-createAssetTag.test.ts | 8 +-- test/writeClient-getAssets.test.ts | 11 ++-- test/writeClient-migrate.test.ts | 12 +++- test/writeClient-migrateCreateAssets.test.ts | 44 +++++++++++++ 8 files changed, 135 insertions(+), 55 deletions(-) create mode 100644 test/writeClient-migrateCreateAssets.test.ts diff --git a/src/WriteClient.ts b/src/WriteClient.ts index db3b29d7..7dd10249 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -59,7 +59,10 @@ type ExtractDocumentType< /** * Utility type to construct events reported by the migration process. */ -type ReporterEvent = TData extends never +type MigrateReporterEvent< + TType extends string, + TData = never, +> = TData extends never ? { type: TType } : { type: TType @@ -69,7 +72,7 @@ type ReporterEvent = TData extends never /** * A map of event types and their data reported by the migration process. */ -type ReporterEventMap = { +type MigrateReporterEventMap = { start: { pending: { documents: number @@ -142,14 +145,18 @@ type ReporterEventMap = { /** * Available event types reported by the migration process. */ -type ReporterEventTypes = keyof ReporterEventMap +type MigrateReporterEventTypes = keyof MigrateReporterEventMap /** - * All events reported by the migration process. + * All events reported by the migration process. Events can be listened to by + * providing a `reporter` function to the `migrate` method. */ -type ReporterEvents = { - [K in ReporterEventTypes]: ReporterEvent -}[ReporterEventTypes] +export type MigrateReporterEvents = { + [K in MigrateReporterEventTypes]: MigrateReporterEvent< + K, + MigrateReporterEventMap[K] + > +}[MigrateReporterEventTypes] /** * A query response from the Prismic asset API. The response contains pagination @@ -364,12 +371,11 @@ export class WriteClient< */ async migrate( migration: Migration, - { - reporter, - ...params - }: { reporter?: (event: ReporterEvents) => void } & FetchParams = {}, + params: { + reporter?: (event: MigrateReporterEvents) => void + } & FetchParams = {}, ): Promise { - reporter?.({ + params.reporter?.({ type: "start", data: { pending: { @@ -385,7 +391,7 @@ export class WriteClient< await this.migrateUpdateDocuments(migration, assets, documents, params) - reporter?.({ + params.reporter?.({ type: "end", data: { migrated: { @@ -411,7 +417,7 @@ export class WriteClient< { reporter, ...fetchParams - }: { reporter?: (event: ReporterEvents) => void } & FetchParams = {}, + }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise { const assets: AssetMap = new Map() @@ -534,7 +540,7 @@ export class WriteClient< { reporter, ...fetchParams - }: { reporter?: (event: ReporterEvents) => void } & FetchParams = {}, + }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise> { // Should this be a function of `Client`? // Resolve master locale @@ -681,7 +687,7 @@ export class WriteClient< { reporter, ...fetchParams - }: { reporter?: (event: ReporterEvents) => void } & FetchParams = {}, + }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise { let i = 0 for (const { document, params } of migration.documents) { diff --git a/src/index.ts b/src/index.ts index da47d444..1ff3342e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ export * as cookie from "./cookie" export type { CreateClient } from "./createClient" export type { ClientConfig } from "./Client" export type { CreateWriteClient } from "./createWriteClient" -export type { WriteClientConfig } from "./WriteClient" +export type { WriteClientConfig, MigrateReporterEvents } from "./WriteClient" export type { AbortSignalLike, FetchLike, diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index 46b23e5b..48a43f3d 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -20,21 +20,11 @@ type MockPrismicAssetAPIArgs = { ctx: TestContext client: WriteClient writeToken?: string - expectedAsset?: Asset - expectedAssets?: Asset[] - expectedTag?: AssetTag - expectedTags?: AssetTag[] - expectedCursor?: string getRequiredParams?: Record -} - -const DEFAULT_TAG: AssetTag = { - id: "88888888-4444-4444-4444-121212121212", - name: "fooTag", - uploader_id: "uploaded_id", - created_at: 0, - last_modified: 0, - count: 0, + existingAssets?: Asset[][] + newAssets?: Asset[] + existingTags?: AssetTag[] + newTags?: AssetTag[] } const DEFAULT_ASSET: Asset = { @@ -62,6 +52,9 @@ export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { const assetAPIEndpoint = args.client.assetAPIEndpoint const writeToken = args.writeToken || args.client.writeToken + const assetsDatabase: Asset[][] = args.existingAssets || [] + const tagsDatabase: AssetTag[] = args.existingTags || [] + args.ctx.server.use( rest.get(`${assetAPIEndpoint}assets`, async (req, res, ctx) => { if ( @@ -83,13 +76,14 @@ export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { } } - const items: Asset[] = args.expectedAssets || [DEFAULT_ASSET] + const index = Number.parseInt(req.url.searchParams.get("cursor") ?? "0") + const items: Asset[] = assetsDatabase[index] || [] const response: GetAssetsResult = { total: items.length, items, is_opensearch_result: false, - cursor: args.expectedCursor, + cursor: assetsDatabase[index + 1] ? `${index + 1}` : undefined, missing_ids: [], } @@ -103,7 +97,10 @@ export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } - const response: PostAssetResult = args.expectedAsset || DEFAULT_ASSET + const response: PostAssetResult = args.newAssets?.pop() ?? DEFAULT_ASSET + + // Save the asset in DB + assetsDatabase.push([response]) return res(ctx.json(response)) }), @@ -116,12 +113,29 @@ export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { } const { tags, ...body } = await req.json() + const asset = assetsDatabase + .flat() + .find((asset) => asset.id === req.params.id) + + if (!asset) { + return res(ctx.status(404), ctx.json({ error: "not found" })) + } + const response: PostAssetResult = { - ...(args.expectedAsset || DEFAULT_ASSET), + ...asset, ...body, tags: tags?.length - ? tags.map((id) => ({ ...DEFAULT_TAG, id })) - : (args.expectedAsset || DEFAULT_ASSET).tags, + ? tagsDatabase.filter((tag) => tags.includes(tag.id)) + : asset.tags, + } + + // Update asset in DB + for (const cursor in assetsDatabase) { + for (const asset in assetsDatabase[cursor]) { + if (assetsDatabase[cursor][asset].id === req.params.id) { + assetsDatabase[cursor][asset] = response + } + } } return res(ctx.json(response)) @@ -134,7 +148,7 @@ export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } - const items: AssetTag[] = args.expectedTags || [DEFAULT_TAG] + const items: AssetTag[] = tagsDatabase const response: GetAssetTagsResult = { items } @@ -150,12 +164,19 @@ export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { const body = await req.json() - const response: PostAssetTagResult = args.expectedTag || { - ...DEFAULT_TAG, - id: `${`${Date.now()}`.slice(-8)}-4954-46c9-a819-faabb69464ea`, + const tag: AssetTag = { + id: `${`${Date.now()}`.slice(-8)}-4444-4444-4444-121212121212`, + created_at: Date.now(), + last_modified: Date.now(), name: body.name, + ...args.newTags?.find((tag) => tag.name === body.name), } + // Save the tag in DB + tagsDatabase.push(tag) + + const response: PostAssetTagResult = tag + return res(ctx.status(201), ctx.json(response)) }), ) diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts index 11748113..d6cee5c5 100644 --- a/test/writeClient-createAsset.test.ts +++ b/test/writeClient-createAsset.test.ts @@ -62,7 +62,7 @@ it.concurrent("creates an asset with an existing tag ID", async (ctx) => { last_modified: 0, } - mockPrismicAssetAPI({ ctx, client, expectedTags: [tag] }) + mockPrismicAssetAPI({ ctx, client, existingTags: [tag] }) // @ts-expect-error - testing purposes const asset = await client.createAsset("file", "foo.jpg", { @@ -82,7 +82,7 @@ it.concurrent("creates an asset with an existing tag name", async (ctx) => { last_modified: 0, } - mockPrismicAssetAPI({ ctx, client, expectedTags: [tag] }) + mockPrismicAssetAPI({ ctx, client, existingTags: [tag] }) // @ts-expect-error - testing purposes const asset = await client.createAsset("file", "foo.jpg", { @@ -102,7 +102,7 @@ it.concurrent("creates an asset with a new tag name", async (ctx) => { last_modified: 0, } - mockPrismicAssetAPI({ ctx, client, expectedTag: tag }) + mockPrismicAssetAPI({ ctx, client, newTags: [tag] }) // @ts-expect-error - testing purposes const asset = await client.createAsset("file", "foo.jpg", { diff --git a/test/writeClient-createAssetTag.test.ts b/test/writeClient-createAssetTag.test.ts index d153ecc1..bd4386f1 100644 --- a/test/writeClient-createAssetTag.test.ts +++ b/test/writeClient-createAssetTag.test.ts @@ -14,19 +14,19 @@ const it = _it.skipIf(isNode16) it.concurrent("creates a tag", async (ctx) => { const client = createTestWriteClient({ ctx }) - const expectedTag: AssetTag = { + const newTag: AssetTag = { id: "00000000-4444-4444-4444-121212121212", name: "foo", created_at: 0, last_modified: 0, } - mockPrismicAssetAPI({ ctx, client, expectedTag }) + mockPrismicAssetAPI({ ctx, client, newTags: [newTag] }) // @ts-expect-error - testing purposes - const tag = await client.createAssetTag(expectedTag.name) + const tag = await client.createAssetTag(newTag.name) - expect(tag).toStrictEqual(expectedTag) + expect(tag).toStrictEqual(newTag) }) it.concurrent("throws if tag is invalid", async (ctx) => { diff --git a/test/writeClient-getAssets.test.ts b/test/writeClient-getAssets.test.ts index e3c694bd..1cfdc9e1 100644 --- a/test/writeClient-getAssets.test.ts +++ b/test/writeClient-getAssets.test.ts @@ -112,7 +112,7 @@ it.concurrent("supports `tags` parameter (id)", async (ctx) => { mockPrismicAssetAPI({ ctx, client, - expectedTags: [ + existingTags: [ { id: "00000000-4444-4444-4444-121212121212", name: "foo", @@ -148,7 +148,7 @@ it.concurrent("supports `tags` parameter (name)", async (ctx) => { mockPrismicAssetAPI({ ctx, client, - expectedTags: [ + existingTags: [ { id: "00000000-4444-4444-4444-121212121212", name: "foo", @@ -181,7 +181,7 @@ it.concurrent("supports `tags` parameter (missing)", async (ctx) => { mockPrismicAssetAPI({ ctx, client, - expectedTags: [ + existingTags: [ { id: "00000000-4444-4444-4444-121212121212", name: "foo", @@ -207,14 +207,13 @@ it.concurrent("returns `next` when next `cursor` is available", async (ctx) => { ctx.expect(next1).toBeUndefined() - const cursor = "foo" - mockPrismicAssetAPI({ ctx, client, expectedCursor: cursor }) + mockPrismicAssetAPI({ ctx, client, existingAssets: [[], []] }) const { next: next2 } = await client.getAssets() ctx.expect(next2).toBeInstanceOf(Function) - mockPrismicAssetAPI({ ctx, client, getRequiredParams: { cursor } }) + mockPrismicAssetAPI({ ctx, client, getRequiredParams: { cursor: "1" } }) const { results } = await next2!() ctx.expect(results).toBeInstanceOf(Array) diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index 1d14a22b..c635eab1 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -23,7 +23,17 @@ it("migrates nothing when migration is empty", async (ctx) => { const reporter = vi.fn() - await expect(client.migrate(migration, { reporter })).resolves.toBeUndefined() + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "start", + data: { + pending: { + documents: 0, + assets: 0, + }, + }, + }) expect(reporter).toHaveBeenCalledWith({ type: "end", diff --git a/test/writeClient-migrateCreateAssets.test.ts b/test/writeClient-migrateCreateAssets.test.ts new file mode 100644 index 00000000..84e2596f --- /dev/null +++ b/test/writeClient-migrateCreateAssets.test.ts @@ -0,0 +1,44 @@ +import { it as _it, expect, vi } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" +import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" + +import * as prismic from "../src" + +// Skip test on Node 16 and 18 (File and FormData support) +const isNode16 = process.version.startsWith("v16") +const isNode18 = process.version.startsWith("v18") +const it = _it.skipIf(isNode16 || isNode18) + +it("discovers existing assets", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "start", + data: { + pending: { + documents: 0, + assets: 0, + }, + }, + }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:existing", + data: { + existing: 0, + }, + }) +}) From 6325d221e3a99648972a9ad736dd001571deb59b Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 5 Sep 2024 15:38:13 +0200 Subject: [PATCH 27/61] test: `WriteClient.migrateCreateAssets` --- src/WriteClient.ts | 39 ++- src/createWriteClient.ts | 3 + test/writeClient-migrate.test.ts | 25 +- test/writeClient-migrateCreateAssets.test.ts | 330 ++++++++++++++++++- 4 files changed, 372 insertions(+), 25 deletions(-) diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 7dd10249..9ad3c29f 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -90,13 +90,13 @@ type MigrateReporterEventMap = { } "assets:skipping": { reason: string - index: number + current: number remaining: number total: number asset: MigrationAsset } "assets:creating": { - index: number + current: number remaining: number total: number asset: MigrationAsset @@ -113,14 +113,14 @@ type MigrateReporterEventMap = { } "documents:skipping": { reason: string - index: number + current: number remaining: number total: number document: PrismicMigrationDocument documentParams: PrismicMigrationDocumentParams } "documents:creating": { - index: number + current: number remaining: number total: number document: PrismicMigrationDocument @@ -131,7 +131,7 @@ type MigrateReporterEventMap = { documents: DocumentMap } "documents:updating": { - index: number + current: number remaining: number total: number document: PrismicMigrationDocument @@ -458,7 +458,7 @@ export class WriteClient< type: "assets:skipping", data: { reason: "already exists", - index: ++i, + current: ++i, remaining: migration.assets.size - i, total: migration.assets.size, asset: migrationAsset, @@ -469,7 +469,7 @@ export class WriteClient< reporter?.({ type: "assets:creating", data: { - index: ++i, + current: ++i, remaining: migration.assets.size - i, total: migration.assets.size, asset: migrationAsset, @@ -564,8 +564,7 @@ export class WriteClient< const documents: DocumentMap = new Map() for (const document of existingDocuments) { - // Index on document and document ID - documents.set(document, document) + // Index on document ID documents.set(document.id, document) } @@ -593,7 +592,7 @@ export class WriteClient< type: "documents:skipping", data: { reason: "already exists", - index: ++i, + current: ++i, remaining: sortedDocuments.length - i, total: sortedDocuments.length, document, @@ -608,7 +607,7 @@ export class WriteClient< reporter?.({ type: "documents:creating", data: { - index: ++i, + current: ++i, remaining: sortedDocuments.length - i, total: sortedDocuments.length, document, @@ -625,9 +624,19 @@ export class WriteClient< await params.masterLanguageDocument() if (masterLanguageDocument) { - masterLanguageDocumentID = documents.get( - masterLanguageDocument, - )?.id + // `masterLanguageDocument` is an existing document + if (masterLanguageDocument.id) { + masterLanguageDocumentID = documents.get( + masterLanguageDocument.id, + )?.id + } + + // `masterLanguageDocument` is a new document + if (!masterLanguageDocumentID) { + masterLanguageDocumentID = documents.get( + masterLanguageDocument, + )?.id + } } } else { masterLanguageDocumentID = params.masterLanguageDocument.id @@ -694,7 +703,7 @@ export class WriteClient< reporter?.({ type: "documents:updating", data: { - index: ++i, + current: ++i, remaining: migration.documents.length - i, total: migration.documents.length, document, diff --git a/src/createWriteClient.ts b/src/createWriteClient.ts index b2bee037..bd6d7190 100644 --- a/src/createWriteClient.ts +++ b/src/createWriteClient.ts @@ -17,6 +17,9 @@ export interface CreateWriteClient { * Creates a Prismic client that can be used to query and write content to a * repository. * + * @remarks + * This client only works with Node 20 or later. + * * @example * * ```ts diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index c635eab1..b05f4287 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -11,7 +11,7 @@ const isNode16 = process.version.startsWith("v16") const isNode18 = process.version.startsWith("v18") const it = _it.skipIf(isNode16 || isNode18) -it("migrates nothing when migration is empty", async (ctx) => { +it.concurrent("migrates nothing when migration is empty", async (ctx) => { const client = createTestWriteClient({ ctx }) mockPrismicRestAPIV2({ ctx }) @@ -35,6 +35,29 @@ it("migrates nothing when migration is empty", async (ctx) => { }, }) + expect(reporter).toHaveBeenCalledWith({ + type: "assets:created", + data: { + created: 0, + assets: expect.any(Map), + }, + }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:created", + data: { + created: 0, + documents: expect.any(Map), + }, + }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:updated", + data: { + updated: 0, + }, + }) + expect(reporter).toHaveBeenCalledWith({ type: "end", data: { diff --git a/test/writeClient-migrateCreateAssets.test.ts b/test/writeClient-migrateCreateAssets.test.ts index 84e2596f..064d6c81 100644 --- a/test/writeClient-migrateCreateAssets.test.ts +++ b/test/writeClient-migrateCreateAssets.test.ts @@ -1,7 +1,12 @@ import { it as _it, expect, vi } from "vitest" +import { rest } from "msw" + import { createTestWriteClient } from "./__testutils__/createWriteClient" -import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" +import { + mockAsset, + mockPrismicAssetAPI, +} from "./__testutils__/mockPrismicAssetAPI" import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" @@ -12,11 +17,11 @@ const isNode16 = process.version.startsWith("v16") const isNode18 = process.version.startsWith("v18") const it = _it.skipIf(isNode16 || isNode18) -it("discovers existing assets", async (ctx) => { +it.concurrent("discovers existing assets", async (ctx) => { const client = createTestWriteClient({ ctx }) mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client }) + mockPrismicAssetAPI({ ctx, client, existingAssets: [2] }) mockPrismicMigrationAPI({ ctx, client }) const migration = prismic.createMigration() @@ -26,19 +31,326 @@ it("discovers existing assets", async (ctx) => { await client.migrate(migration, { reporter }) expect(reporter).toHaveBeenCalledWith({ - type: "start", + type: "assets:existing", data: { - pending: { - documents: 0, - assets: 0, + existing: 2, + }, + }) +}) + +it.concurrent( + "discovers existing assets and crawl pages if any", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client, existingAssets: [2, 3] }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:existing", + data: { + existing: 5, }, + }) + }, +) + +it.concurrent("skips creating existing assets", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicRestAPIV2({ ctx }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [1], + }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() + + const asset = assetsDatabase[0][0] + migration.createAsset(asset) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:skipping", + data: { + reason: "already exists", + current: 1, + remaining: 0, + total: 1, + asset: expect.objectContaining({ id: asset.id, file: asset.url }), }, }) +}) + +it.concurrent("creates new asset from string file data", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const asset = mockAsset(ctx) + const dummyFileData = "foo" + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() + migration.createAsset(dummyFileData, asset.filename) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) expect(reporter).toHaveBeenCalledWith({ - type: "assets:existing", + type: "assets:creating", + data: { + current: 1, + remaining: 0, + total: 1, + asset: expect.objectContaining({ + file: dummyFileData, + filename: asset.filename, + }), + }, + }) +}) + +it.concurrent("creates new asset from a File instance", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const asset = mockAsset(ctx) + const dummyFile = new File(["foo"], asset.filename) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() + migration.createAsset(dummyFile, asset.filename) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:creating", data: { - existing: 0, + current: 1, + remaining: 0, + total: 1, + asset: expect.objectContaining({ + file: dummyFile, + filename: asset.filename, + }), }, }) }) + +it.concurrent( + "creates new asset from a file URL with content type", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const asset = mockAsset(ctx) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + mockPrismicMigrationAPI({ ctx, client }) + + ctx.server.use( + rest.get(asset.url.split("?")[0], (_req, res, ctx) => { + return res(ctx.set("content-type", "image/jpg"), ctx.text("foo")) + }), + ) + + const migration = prismic.createMigration() + migration.createAsset(new URL(asset.url), asset.filename) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:creating", + data: { + current: 1, + remaining: 0, + total: 1, + asset: expect.objectContaining({ + file: new URL(asset.url), + filename: asset.filename, + }), + }, + }) + }, +) + +it.concurrent( + "creates new asset from a file string URL with content type", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const asset = mockAsset(ctx) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + mockPrismicMigrationAPI({ ctx, client }) + + ctx.server.use( + rest.get(asset.url.split("?")[0], (_req, res, ctx) => { + return res(ctx.set("content-type", "image/jpg"), ctx.text("foo")) + }), + ) + + const migration = prismic.createMigration() + migration.createAsset(asset.url, asset.filename) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:creating", + data: { + current: 1, + remaining: 0, + total: 1, + asset: expect.objectContaining({ + file: asset.url, + filename: asset.filename, + }), + }, + }) + }, +) + +it.concurrent( + "creates new asset from a file URL with no content type", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const asset = mockAsset(ctx) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + mockPrismicMigrationAPI({ ctx, client }) + + ctx.server.use( + rest.get(asset.url.split("?")[0], (_req, res, ctx) => { + return res(ctx.set("content-type", ""), ctx.text("foo")) + }), + ) + + const migration = prismic.createMigration() + migration.createAsset(new URL(asset.url), asset.filename) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:creating", + data: { + current: 1, + remaining: 0, + total: 1, + asset: expect.objectContaining({ + file: new URL(asset.url), + filename: asset.filename, + }), + }, + }) + }, +) + +it.concurrent( + "creates new asset from a file string URL with no content type", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const asset = mockAsset(ctx) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + mockPrismicMigrationAPI({ ctx, client }) + + ctx.server.use( + rest.get(asset.url.split("?")[0], (_req, res, ctx) => { + return res(ctx.set("content-type", ""), ctx.text("foo")) + }), + ) + + const migration = prismic.createMigration() + migration.createAsset(asset.url, asset.filename) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:creating", + data: { + current: 1, + remaining: 0, + total: 1, + asset: expect.objectContaining({ + file: asset.url, + filename: asset.filename, + }), + }, + }) + }, +) + +it.concurrent( + "throws on fetch error when creating a new asset from a file URL", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const asset = mockAsset(ctx) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + mockPrismicMigrationAPI({ ctx, client }) + + ctx.server.use( + rest.get(asset.url.split("?")[0], (_req, res, ctx) => { + return res(ctx.status(429)) + }), + ) + + const migration = prismic.createMigration() + migration.createAsset(asset.url, asset.filename) + + const reporter = vi.fn() + + await expect(() => + client.migrate(migration, { reporter }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Could not fetch foreign asset]`, + ) + + expect(reporter).toHaveBeenLastCalledWith({ + type: "assets:creating", + data: { + current: 1, + remaining: 0, + total: 1, + asset: expect.objectContaining({ + file: asset.url, + filename: asset.filename, + }), + }, + }) + }, +) From e824a33a8c423f3306d9115e1b9448a73185e5ad Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 5 Sep 2024 15:38:32 +0200 Subject: [PATCH 28/61] test: `WriteClient.migrateCreateDocuments` --- test/writeClient-createDocument.test.ts | 12 +- ...writeClient-migrateCreateDocuments.test.ts | 413 ++++++++++++++++++ test/writeClient-updateDocument.test.ts | 34 +- 3 files changed, 441 insertions(+), 18 deletions(-) create mode 100644 test/writeClient-migrateCreateDocuments.test.ts diff --git a/test/writeClient-createDocument.test.ts b/test/writeClient-createDocument.test.ts index 82a0904c..b6205a8d 100644 --- a/test/writeClient-createDocument.test.ts +++ b/test/writeClient-createDocument.test.ts @@ -9,9 +9,9 @@ import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" it.concurrent("creates a document", async (ctx) => { const client = createTestWriteClient({ ctx }) - const expectedID = "foo" + const newDocument = { id: "foo" } - mockPrismicMigrationAPI({ ctx, client, expectedID }) + mockPrismicMigrationAPI({ ctx, client, newDocuments: [newDocument] }) // @ts-expect-error - testing purposes const { id } = await client.createDocument( @@ -24,7 +24,7 @@ it.concurrent("creates a document", async (ctx) => { "Foo", ) - expect(id).toBe(expectedID) + expect(id).toBe(newDocument.id) }) it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { @@ -54,7 +54,7 @@ it.skip("supports abort controller", async (ctx) => { const controller = new AbortController() controller.abort() - mockPrismicMigrationAPI({ ctx, client, expectedID: "foo" }) + mockPrismicMigrationAPI({ ctx, client }) await expect(() => // @ts-expect-error - testing purposes @@ -74,9 +74,7 @@ it.skip("supports abort controller", async (ctx) => { it.concurrent("respects unknown rate limit", async (ctx) => { const client = createTestWriteClient({ ctx }) - const expectedID = "foo" - - mockPrismicMigrationAPI({ ctx, client, expectedID }) + mockPrismicMigrationAPI({ ctx, client }) const args = [ { diff --git a/test/writeClient-migrateCreateDocuments.test.ts b/test/writeClient-migrateCreateDocuments.test.ts new file mode 100644 index 00000000..3220a8de --- /dev/null +++ b/test/writeClient-migrateCreateDocuments.test.ts @@ -0,0 +1,413 @@ +import type { TestContext } from "vitest" +import { it as _it, expect, vi } from "vitest" + +import { createPagedQueryResponses } from "./__testutils__/createPagedQueryResponses" +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" +import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" + +import * as prismic from "../src" +import type { DocumentMap } from "../src/lib/patchMigrationDocumentData" + +// Skip test on Node 16 and 18 (File and FormData support) +const isNode16 = process.version.startsWith("v16") +const isNode18 = process.version.startsWith("v18") +const it = _it.skipIf(isNode16 || isNode18) + +const createRepository = ( + ctx: TestContext, + masterLocale = "en-us", +): { + repository: prismic.Repository + masterLocale: string +} => { + const repository = ctx.mock.api.repository() + repository.languages[0].id = masterLocale + repository.languages[0].is_master = true + + return { repository, masterLocale } +} + +it.concurrent("infers master locale", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const { repository, masterLocale } = createRepository(ctx) + + mockPrismicRestAPIV2({ ctx, repositoryResponse: repository }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:masterLocale", + data: { masterLocale }, + }) +}) + +it.concurrent("discovers existing documents", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const queryResponse = createPagedQueryResponses({ + ctx, + pages: 2, + pageSize: 1, + }) + + mockPrismicRestAPIV2({ ctx, queryResponse }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:existing", + data: { + existing: 2, + }, + }) +}) + +it.concurrent("skips creating existing documents", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const queryResponse = createPagedQueryResponses({ + ctx, + pages: 1, + pageSize: 1, + }) + const document = queryResponse[0].results[0] + + mockPrismicRestAPIV2({ ctx, queryResponse }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, existingDocuments: [document] }) + + const migration = prismic.createMigration() + + migration.createDocument(document, "foo") + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:skipping", + data: { + reason: "already exists", + current: 1, + remaining: 0, + total: 1, + document, + documentParams: { + documentName: "foo", + }, + }, + }) + expect(reporter).toHaveBeenCalledWith({ + type: "documents:created", + data: { + created: 0, + documents: expect.anything(), + }, + }) +}) + +it.concurrent("creates new documents", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const document = ctx.mock.value.document() + const newDocument = { id: "foo" } + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ + ctx, + client, + newDocuments: [newDocument], + }) + + const migration = prismic.createMigration() + + migration.createDocument(document, "foo") + + let documents: DocumentMap | undefined + const reporter = vi.fn((event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }) + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:creating", + data: { + current: 1, + remaining: 0, + total: 1, + document, + documentParams: { + documentName: "foo", + }, + }, + }) + expect(documents?.get(document)?.id).toBe(newDocument.id) +}) + +it.concurrent( + "creates new non-master locale document with direct reference", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const masterLanguageDocument = ctx.mock.value.document() + const document = ctx.mock.value.document() + const newDocument = { + id: "foo", + masterLanguageDocumentID: masterLanguageDocument.id, + } + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, newDocuments: [newDocument] }) + + const migration = prismic.createMigration() + + migration.createDocument(document, "foo", { masterLanguageDocument }) + + let documents: DocumentMap | undefined + const reporter = vi.fn((event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }) + + await client.migrate(migration, { reporter }) + + ctx.expect(reporter).toHaveBeenCalledWith({ + type: "documents:creating", + data: { + current: 1, + remaining: 0, + total: 1, + document, + documentParams: { + documentName: "foo", + masterLanguageDocument, + }, + }, + }) + ctx.expect(documents?.get(document)?.id).toBe(newDocument.id) + ctx.expect.assertions(3) + }, +) + +it.concurrent( + "creates new non-master locale document with lazy reference (existing document)", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const queryResponse = createPagedQueryResponses({ + ctx, + pages: 1, + pageSize: 1, + }) + + const masterLanguageDocument = queryResponse[0].results[0] + const document = ctx.mock.value.document() + const newDocument = { + id: "foo", + masterLanguageDocumentID: masterLanguageDocument.id, + } + + mockPrismicRestAPIV2({ ctx, queryResponse }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, newDocuments: [newDocument] }) + + const migration = prismic.createMigration() + + migration.createDocument(document, "foo", { + masterLanguageDocument: () => masterLanguageDocument, + }) + + let documents: DocumentMap | undefined + const reporter = vi.fn((event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }) + + await client.migrate(migration, { reporter }) + + ctx.expect(reporter).toHaveBeenCalledWith({ + type: "documents:creating", + data: { + current: 1, + remaining: 0, + total: 1, + document, + documentParams: { + documentName: "foo", + masterLanguageDocument: expect.any(Function), + }, + }, + }) + ctx.expect(documents?.get(document)?.id).toBe(newDocument.id) + ctx.expect.assertions(3) + }, +) + +it.concurrent( + "creates new non-master locale document with lazy reference (new document)", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const masterLanguageDocument = + ctx.mock.value.document() as prismic.PrismicMigrationDocument + const document = + ctx.mock.value.document() as prismic.PrismicMigrationDocument + const newDocuments = [ + { + id: masterLanguageDocument.id!, + }, + { + id: document.id!, + masterLanguageDocumentID: masterLanguageDocument.id!, + }, + ] + + delete masterLanguageDocument.id + delete document.id + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, newDocuments: [...newDocuments] }) + + const migration = prismic.createMigration() + + migration.createDocument(masterLanguageDocument, "foo") + migration.createDocument(document, "bar", { + masterLanguageDocument: () => masterLanguageDocument, + }) + + let documents: DocumentMap | undefined + const reporter = vi.fn((event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }) + + await client.migrate(migration, { reporter }) + + ctx + .expect(documents?.get(masterLanguageDocument)?.id) + .toBe(newDocuments[0].id) + ctx.expect(documents?.get(document)?.id).toBe(newDocuments[1].id) + ctx.expect.assertions(3) + }, +) + +it.concurrent( + "creates new non-master locale document with alternate language", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const { repository, masterLocale } = createRepository(ctx) + const queryResponse = createPagedQueryResponses({ + ctx, + pages: 1, + pageSize: 1, + }) + + const masterLanguageDocument = queryResponse[0].results[0] + masterLanguageDocument.lang = masterLocale + const document = ctx.mock.value.document({ + alternateLanguages: [masterLanguageDocument], + }) + const newDocument = { + id: "foo", + masterLanguageDocumentID: masterLanguageDocument.id, + } + + mockPrismicRestAPIV2({ ctx, repositoryResponse: repository, queryResponse }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, newDocuments: [newDocument] }) + + const migration = prismic.createMigration() + + migration.createDocument(document, "foo") + + let documents: DocumentMap | undefined + const reporter = vi.fn((event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }) + + await client.migrate(migration, { reporter }) + + ctx.expect(reporter).toHaveBeenCalledWith({ + type: "documents:creating", + data: { + current: 1, + remaining: 0, + total: 1, + document, + documentParams: { + documentName: "foo", + }, + }, + }) + ctx.expect(documents?.get(document)?.id).toBe(newDocument.id) + ctx.expect.assertions(3) + }, +) + +it.concurrent("creates master locale documents first", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const { repository, masterLocale } = createRepository(ctx) + + const masterLanguageDocument = ctx.mock.value.document() + masterLanguageDocument.lang = masterLocale + const document = ctx.mock.value.document() + const newDocuments = [ + { + id: masterLanguageDocument.id, + }, + { + id: document.id, + }, + ] + + mockPrismicRestAPIV2({ ctx, repositoryResponse: repository }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, newDocuments: [...newDocuments] }) + + const migration = prismic.createMigration() + + migration.createDocument(document, "bar") + migration.createDocument(masterLanguageDocument, "foo") + + let documents: DocumentMap | undefined + const reporter = vi.fn((event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }) + + await client.migrate(migration, { reporter }) + + ctx + .expect(documents?.get(masterLanguageDocument)?.id) + .toBe(newDocuments[0].id) + ctx.expect(documents?.get(document)?.id).toBe(newDocuments[1].id) +}) diff --git a/test/writeClient-updateDocument.test.ts b/test/writeClient-updateDocument.test.ts index 0b744be0..4b72883b 100644 --- a/test/writeClient-updateDocument.test.ts +++ b/test/writeClient-updateDocument.test.ts @@ -9,13 +9,17 @@ import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" it.concurrent("updates a document", async (ctx) => { const client = createTestWriteClient({ ctx }) - const expectedID = "foo" + const { documentsDatabase } = mockPrismicMigrationAPI({ + ctx, + client, + existingDocuments: 1, + }) - mockPrismicMigrationAPI({ ctx, client, expectedID }) + const document = Object.values(documentsDatabase)[0] await expect( // @ts-expect-error - testing purposes - client.updateDocument(expectedID, { + client.updateDocument(document.id, { uid: "uid", data: {}, }), @@ -25,7 +29,7 @@ it.concurrent("updates a document", async (ctx) => { it.concurrent("throws not found error on not found ID", async (ctx) => { const client = createTestWriteClient({ ctx }) - mockPrismicMigrationAPI({ ctx, client, expectedID: "not-found" }) + mockPrismicMigrationAPI({ ctx, client }) await expect(() => // @ts-expect-error - testing purposes @@ -39,12 +43,16 @@ it.concurrent("throws not found error on not found ID", async (ctx) => { it.concurrent("respects unknown rate limit", async (ctx) => { const client = createTestWriteClient({ ctx }) - const expectedID = "foo" + const { documentsDatabase } = mockPrismicMigrationAPI({ + ctx, + client, + existingDocuments: 1, + }) - mockPrismicMigrationAPI({ ctx, client, expectedID }) + const document = Object.values(documentsDatabase)[0] const args = [ - expectedID, + document.id, { uid: "uid", data: {}, @@ -83,17 +91,21 @@ it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { it.skip("supports abort controller", async (ctx) => { const client = createTestWriteClient({ ctx }) - const expectedID = "foo" - const controller = new AbortController() controller.abort() - mockPrismicMigrationAPI({ ctx, client, expectedID }) + const { documentsDatabase } = mockPrismicMigrationAPI({ + ctx, + client, + existingDocuments: 1, + }) + + const document = Object.values(documentsDatabase)[0] await expect(() => // @ts-expect-error - testing purposes client.updateDocument( - expectedID, + document.id, { uid: "uid", data: {}, From 4bc610dd02981ef8666b9daab670f3fa81ea2fd4 Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 5 Sep 2024 15:38:44 +0200 Subject: [PATCH 29/61] test: ensure unique repository name for all tests --- test/__testutils__/createClient.ts | 7 +- test/__testutils__/createRepositoryName.ts | 5 +- test/__testutils__/mockPrismicAssetAPI.ts | 44 ++++++++++-- test/__testutils__/mockPrismicMigrationAPI.ts | 70 ++++++++++++++++--- test/__testutils__/mockPrismicRestAPIV2.ts | 6 +- test/__testutils__/testAbortableMethod.ts | 4 +- test/__testutils__/testAnyGetMethod.ts | 1 + test/__testutils__/testConcurrentMethod.ts | 4 +- test/__testutils__/testFetchOptions.ts | 2 + test/__testutils__/testGetTTL.ts | 4 +- test/client-buildQueryURL.test.ts | 8 +-- test/client-dangerouslyGetAll.test.ts | 4 +- test/client-get.test.ts | 2 +- test/client-getFirst.test.ts | 2 +- test/client-getMasterRef.test.ts | 2 +- test/client-getRefByID.test.ts | 4 +- test/client-getRefByLabel.test.ts | 4 +- test/client-getRefs.test.ts | 2 +- test/client-getReleaseById.test.ts | 4 +- test/client-getReleaseByLabel.test.ts | 4 +- test/client-getReleases.test.ts | 2 +- test/client-getRepository.test.ts | 4 +- test/client-getTags.test.ts | 6 +- test/client-graphQLFetch.test.ts | 28 ++++---- test/client-queryContentFromRef.test.ts | 2 +- ...client-queryContentFromReleaseById.test.ts | 2 +- ...ent-queryContentFromReleaseByLabel.test.ts | 2 +- test/client-resolvePreviewUrl.test.ts | 24 +++---- test/client.test.ts | 59 ++++++++-------- 29 files changed, 204 insertions(+), 108 deletions(-) diff --git a/test/__testutils__/createClient.ts b/test/__testutils__/createClient.ts index e77d14f5..164e5a82 100644 --- a/test/__testutils__/createClient.ts +++ b/test/__testutils__/createClient.ts @@ -1,3 +1,5 @@ +import type { TaskContext } from "vitest" + import fetch from "node-fetch" import { createRepositoryName } from "./createRepositoryName" @@ -14,13 +16,14 @@ type CreateTestClientArgs = ( apiEndpoint?: string } ) & { + ctx: TaskContext clientConfig?: prismic.ClientConfig } export const createTestClient = ( - args: CreateTestClientArgs = {}, + args: CreateTestClientArgs, ): prismic.Client => { - const repositoryName = args.repositoryName || createRepositoryName() + const repositoryName = args.repositoryName || createRepositoryName(args.ctx) return prismic.createClient(args.apiEndpoint || repositoryName, { fetch, diff --git a/test/__testutils__/createRepositoryName.ts b/test/__testutils__/createRepositoryName.ts index 1eb8880e..2a85c262 100644 --- a/test/__testutils__/createRepositoryName.ts +++ b/test/__testutils__/createRepositoryName.ts @@ -1,10 +1,9 @@ import type { TaskContext } from "vitest" -import { expect } from "vitest" import * as crypto from "node:crypto" -export const createRepositoryName = (ctx?: TaskContext): string => { - const seed = ctx?.task.name || expect.getState().currentTestName +export const createRepositoryName = (ctx: TaskContext): string => { + const seed = ctx.task.name if (!seed) { throw new Error( `createRepositoryName() can only be called within a Vitest test.`, diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index 48a43f3d..d3136ff3 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -21,12 +21,17 @@ type MockPrismicAssetAPIArgs = { client: WriteClient writeToken?: string getRequiredParams?: Record - existingAssets?: Asset[][] + existingAssets?: Asset[][] | number[] newAssets?: Asset[] existingTags?: AssetTag[] newTags?: AssetTag[] } +type MockPrismicAssetAPIReturnType = { + assetsDatabase: Asset[][] + tagsDatabase: AssetTag[] +} + const DEFAULT_ASSET: Asset = { id: "Yz7kzxAAAB0AREK7", uploader_id: "uploader_id", @@ -47,12 +52,37 @@ const DEFAULT_ASSET: Asset = { tags: [], } -export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { +export const mockAsset = (ctx: TestContext): Asset => { + const { id, url, alt, copyright } = ctx.mock.value.image({ + state: "filled", + }) + + return { + ...DEFAULT_ASSET, + id, + url, + alt: alt ?? undefined, + credits: copyright ?? undefined, + } +} + +export const mockPrismicAssetAPI = ( + args: MockPrismicAssetAPIArgs, +): MockPrismicAssetAPIReturnType => { const repositoryName = args.client.repositoryName const assetAPIEndpoint = args.client.assetAPIEndpoint const writeToken = args.writeToken || args.client.writeToken - const assetsDatabase: Asset[][] = args.existingAssets || [] + const assetsDatabase: Asset[][] = + args.existingAssets?.map((assets) => { + if (typeof assets === "number") { + return Array(assets) + .fill(1) + .map(() => mockAsset(args.ctx)) + } + + return assets + }) || [] const tagsDatabase: AssetTag[] = args.existingTags || [] args.ctx.server.use( @@ -97,7 +127,8 @@ export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } - const response: PostAssetResult = args.newAssets?.pop() ?? DEFAULT_ASSET + const response: PostAssetResult = + args.newAssets?.shift() ?? mockAsset(args.ctx) // Save the asset in DB assetsDatabase.push([response]) @@ -180,4 +211,9 @@ export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { return res(ctx.status(201), ctx.json(response)) }), ) + + return { + assetsDatabase, + tagsDatabase, + } } diff --git a/test/__testutils__/mockPrismicMigrationAPI.ts b/test/__testutils__/mockPrismicMigrationAPI.ts index 6a31d997..a610338e 100644 --- a/test/__testutils__/mockPrismicMigrationAPI.ts +++ b/test/__testutils__/mockPrismicMigrationAPI.ts @@ -2,7 +2,7 @@ import type { TestContext } from "vitest" import { rest } from "msw" -import type { WriteClient } from "../../src" +import type { PrismicDocument, WriteClient } from "../../src" import type { PostDocumentParams, PostDocumentResult, @@ -15,17 +15,53 @@ type MockPrismicMigrationAPIArgs = { client: WriteClient writeToken?: string migrationAPIKey?: string - expectedID?: string + existingDocuments?: (PostDocumentResult | PrismicDocument)[] | number + newDocuments?: { id: string; masterLanguageDocumentID?: string }[] +} + +type MockPrismicMigrationAPIReturnType = { + documentsDatabase: Record } export const mockPrismicMigrationAPI = ( args: MockPrismicMigrationAPIArgs, -): void => { +): MockPrismicMigrationAPIReturnType => { const repositoryName = args.client.repositoryName const migrationAPIEndpoint = args.client.migrationAPIEndpoint const writeToken = args.writeToken || args.client.writeToken const migrationAPIKey = args.migrationAPIKey || args.client.migrationAPIKey + const documentsDatabase: Record = {} + + if (args.existingDocuments) { + if (typeof args.existingDocuments === "number") { + for (let i = 0; i < args.existingDocuments; i++) { + const document = args.ctx.mock.value.document() + documentsDatabase[document.id] = { + title: args.ctx.mock.value.keyText({ state: "filled" }), + id: document.id, + lang: document.lang, + type: document.type, + uid: document.uid, + } + } + } else { + for (const document of args.existingDocuments) { + if ("title" in document) { + documentsDatabase[document.id] = document + } else { + documentsDatabase[document.id] = { + title: args.ctx.mock.value.keyText({ state: "filled" }), + id: document.id, + lang: document.lang, + type: document.type, + uid: document.uid, + } + } + } + } + } + args.ctx.server.use( rest.post(`${migrationAPIEndpoint}documents`, async (req, res, ctx) => { if ( @@ -36,9 +72,17 @@ export const mockPrismicMigrationAPI = ( return res(ctx.status(403), ctx.json({ Message: "forbidden" })) } - const id = args.expectedID || args.ctx.mock.value.document().id const body = await req.json() + const newDocument = args.newDocuments?.shift() + const id = newDocument?.id || args.ctx.mock.value.document().id + + if (newDocument?.masterLanguageDocumentID) { + args.ctx + .expect(body.alternate_language_id) + .toBe(newDocument.masterLanguageDocumentID) + } + const response: PostDocumentResult = { title: body.title, id, @@ -47,6 +91,9 @@ export const mockPrismicMigrationAPI = ( uid: body.uid, } + // Save the document in DB + documentsDatabase[id] = response + return res(ctx.status(201), ctx.json(response)) }), rest.put(`${migrationAPIEndpoint}documents/:id`, async (req, res, ctx) => { @@ -58,23 +105,28 @@ export const mockPrismicMigrationAPI = ( return res(ctx.status(403), ctx.json({ Message: "forbidden" })) } - if (req.params.id !== args.expectedID) { + const document = documentsDatabase[req.params.id as string] + + if (!document) { return res(ctx.status(404)) } - const document = args.ctx.mock.value.document() - const id = args.expectedID || document.id const body = await req.json() const response: PutDocumentResult = { - title: body.title || args.ctx.mock.value.keyText({ state: "filled" }), - id, + title: body.title || document.title, + id: req.params.id as string, lang: document.lang, type: document.type, uid: body.uid, } + // Update the document in DB + documentsDatabase[req.params.id as string] = response + return res(ctx.json(response)) }), ) + + return { documentsDatabase } } diff --git a/test/__testutils__/mockPrismicRestAPIV2.ts b/test/__testutils__/mockPrismicRestAPIV2.ts index d4db1d5e..d8b500f3 100644 --- a/test/__testutils__/mockPrismicRestAPIV2.ts +++ b/test/__testutils__/mockPrismicRestAPIV2.ts @@ -1,4 +1,4 @@ -import type { TestContext } from "vitest" +import type { TaskContext, TestContext } from "vitest" import { expect } from "vitest" import { rest } from "msw" @@ -10,7 +10,7 @@ import type * as prismic from "../../src" const DEFAULT_DELAY = 0 type MockPrismicRestAPIV2Args = { - ctx: TestContext + ctx: TestContext & TaskContext accessToken?: string repositoryResponse?: prismic.Repository queryResponse?: prismic.Query | prismic.Query[] @@ -19,7 +19,7 @@ type MockPrismicRestAPIV2Args = { } export const mockPrismicRestAPIV2 = (args: MockPrismicRestAPIV2Args): void => { - const repositoryName = createRepositoryName() + const repositoryName = createRepositoryName(args.ctx) const repositoryEndpoint = `https://${repositoryName}.cdn.prismic.io/api/v2` const queryEndpoint = new URL( "documents/search", diff --git a/test/__testutils__/testAbortableMethod.ts b/test/__testutils__/testAbortableMethod.ts index 164a194f..d33fd973 100644 --- a/test/__testutils__/testAbortableMethod.ts +++ b/test/__testutils__/testAbortableMethod.ts @@ -22,7 +22,7 @@ export const testAbortableMethod = ( mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(async () => { await args.run(client, { @@ -43,7 +43,7 @@ export const testAbortableMethod = ( mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(async () => { await args.run(client, { diff --git a/test/__testutils__/testAnyGetMethod.ts b/test/__testutils__/testAnyGetMethod.ts index c50d93b4..99ea8d41 100644 --- a/test/__testutils__/testAnyGetMethod.ts +++ b/test/__testutils__/testAnyGetMethod.ts @@ -43,6 +43,7 @@ export const testAnyGetMethodFactory = ( const client = createTestClient({ clientConfig: args.clientConfig, + ctx, }) const res = await args.run(client) diff --git a/test/__testutils__/testConcurrentMethod.ts b/test/__testutils__/testConcurrentMethod.ts index cd736b4e..1d3f4a9c 100644 --- a/test/__testutils__/testConcurrentMethod.ts +++ b/test/__testutils__/testConcurrentMethod.ts @@ -52,9 +52,9 @@ export const testConcurrentMethod = ( queryDelay: 10, }) - const client = createTestClient({ clientConfig: { fetch: fetchSpy } }) + const client = createTestClient({ clientConfig: { fetch: fetchSpy }, ctx }) - const graphqlURL = `https://${createRepositoryName()}.cdn.prismic.io/graphql` + const graphqlURL = `https://${createRepositoryName(ctx)}.cdn.prismic.io/graphql` const graphqlResponse = { foo: "bar" } ctx.server.use( rest.get(graphqlURL, (req, res, ctx) => { diff --git a/test/__testutils__/testFetchOptions.ts b/test/__testutils__/testFetchOptions.ts index 95161a52..f8d08399 100644 --- a/test/__testutils__/testFetchOptions.ts +++ b/test/__testutils__/testFetchOptions.ts @@ -50,6 +50,7 @@ export const testFetchOptions = ( fetch: fetchSpy, fetchOptions, }, + ctx, }) await args.run(client) @@ -90,6 +91,7 @@ export const testFetchOptions = ( clientConfig: { fetch: fetchSpy, }, + ctx, }) await args.run(client, { fetchOptions }) diff --git a/test/__testutils__/testGetTTL.ts b/test/__testutils__/testGetTTL.ts index 1a3d9605..e2337d58 100644 --- a/test/__testutils__/testGetTTL.ts +++ b/test/__testutils__/testGetTTL.ts @@ -52,7 +52,7 @@ export const testGetWithinTTL = ( ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) if (args.beforeFirstGet) { args.beforeFirstGet({ client }) @@ -112,7 +112,7 @@ export const testGetOutsideTTL = ( ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) if (args.beforeFirstGet) { args.beforeFirstGet({ client }) diff --git a/test/client-buildQueryURL.test.ts b/test/client-buildQueryURL.test.ts index 1aff7f14..89ed0844 100644 --- a/test/client-buildQueryURL.test.ts +++ b/test/client-buildQueryURL.test.ts @@ -15,7 +15,7 @@ it("builds a query URL using the master ref", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.buildQueryURL() const url = new URL(res) @@ -40,7 +40,7 @@ it("includes params if provided", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.buildQueryURL(params) const url = new URL(res) @@ -69,7 +69,7 @@ it("includes default params if provided", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient({ clientConfig }) + const client = createTestClient({ clientConfig, ctx }) const res = await client.buildQueryURL() const url = new URL(res) @@ -101,7 +101,7 @@ it("merges params and default params if provided", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient({ clientConfig }) + const client = createTestClient({ clientConfig, ctx }) const res = await client.buildQueryURL(params) const url = new URL(res) diff --git a/test/client-dangerouslyGetAll.test.ts b/test/client-dangerouslyGetAll.test.ts index e48f72e6..b767f419 100644 --- a/test/client-dangerouslyGetAll.test.ts +++ b/test/client-dangerouslyGetAll.test.ts @@ -138,7 +138,7 @@ it("throttles requests past first page", async (ctx) => { queryDelay, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const startTime = Date.now() await client.dangerouslyGetAll() @@ -173,7 +173,7 @@ it("does not throttle single page queries", async (ctx) => { queryDelay, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const startTime = Date.now() await client.dangerouslyGetAll() diff --git a/test/client-get.test.ts b/test/client-get.test.ts index e4caa71b..61893155 100644 --- a/test/client-get.test.ts +++ b/test/client-get.test.ts @@ -62,7 +62,7 @@ testGetMethod("merges params and default params if provided", { it("uses cached repository metadata within the client's repository cache TTL", async (ctx) => { const fetchSpy = vi.fn(fetch) - const client = createTestClient({ clientConfig: { fetch: fetchSpy } }) + const client = createTestClient({ clientConfig: { fetch: fetchSpy }, ctx }) const repositoryResponse1 = ctx.mock.api.repository() repositoryResponse1.refs = [ctx.mock.api.ref({ isMasterRef: true })] diff --git a/test/client-getFirst.test.ts b/test/client-getFirst.test.ts index 307e6814..48634f67 100644 --- a/test/client-getFirst.test.ts +++ b/test/client-getFirst.test.ts @@ -84,7 +84,7 @@ it("throws if no documents were returned", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(() => client.getFirst()).rejects.toThrowError( /no documents were returned/i, diff --git a/test/client-getMasterRef.test.ts b/test/client-getMasterRef.test.ts index 0e1b98a9..73f84075 100644 --- a/test/client-getMasterRef.test.ts +++ b/test/client-getMasterRef.test.ts @@ -17,7 +17,7 @@ it("returns the master ref", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getMasterRef() expect(res).toStrictEqual(masterRef) diff --git a/test/client-getRefByID.test.ts b/test/client-getRefByID.test.ts index f4449ce7..92f018cb 100644 --- a/test/client-getRefByID.test.ts +++ b/test/client-getRefByID.test.ts @@ -18,7 +18,7 @@ it("returns a ref by ID", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getRefByID(ref2.id) expect(res).toStrictEqual(ref2) @@ -29,7 +29,7 @@ it("throws if ref could not be found", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(() => client.getRefByID("non-existant")).rejects.toThrowError( /could not be found/i, diff --git a/test/client-getRefByLabel.test.ts b/test/client-getRefByLabel.test.ts index d5798736..5d219256 100644 --- a/test/client-getRefByLabel.test.ts +++ b/test/client-getRefByLabel.test.ts @@ -18,7 +18,7 @@ it("returns a ref by label", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getRefByLabel(ref2.label) expect(res).toStrictEqual(ref2) @@ -29,7 +29,7 @@ it("throws if ref could not be found", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(() => client.getRefByLabel("non-existant")).rejects.toThrowError( /could not be found/i, diff --git a/test/client-getRefs.test.ts b/test/client-getRefs.test.ts index 32c03bd4..55ff40a6 100644 --- a/test/client-getRefs.test.ts +++ b/test/client-getRefs.test.ts @@ -13,7 +13,7 @@ it("returns all refs", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getRefs() expect(res).toStrictEqual(repositoryResponse.refs) diff --git a/test/client-getReleaseById.test.ts b/test/client-getReleaseById.test.ts index 35c4be77..2521e80b 100644 --- a/test/client-getReleaseById.test.ts +++ b/test/client-getReleaseById.test.ts @@ -18,7 +18,7 @@ it("returns a Release by ID", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getReleaseByID(ref2.id) expect(res).toStrictEqual(ref2) @@ -27,7 +27,7 @@ it("returns a Release by ID", async (ctx) => { it("throws if Release could not be found", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(() => client.getReleaseByID("non-existant"), diff --git a/test/client-getReleaseByLabel.test.ts b/test/client-getReleaseByLabel.test.ts index 81c54ccc..fbb861ab 100644 --- a/test/client-getReleaseByLabel.test.ts +++ b/test/client-getReleaseByLabel.test.ts @@ -18,7 +18,7 @@ it("returns a Release by label", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getReleaseByLabel(ref2.label) expect(res).toStrictEqual(ref2) @@ -27,7 +27,7 @@ it("returns a Release by label", async (ctx) => { it("throws if Release could not be found", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(() => client.getReleaseByLabel("non-existant"), diff --git a/test/client-getReleases.test.ts b/test/client-getReleases.test.ts index 61cefcc4..1c249156 100644 --- a/test/client-getReleases.test.ts +++ b/test/client-getReleases.test.ts @@ -13,7 +13,7 @@ it("returns all Releases", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getReleases() expect(res).toStrictEqual( diff --git a/test/client-getRepository.test.ts b/test/client-getRepository.test.ts index 3fe18100..da19bd6b 100644 --- a/test/client-getRepository.test.ts +++ b/test/client-getRepository.test.ts @@ -14,7 +14,7 @@ it("returns repository metadata", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getRepository() expect(res).toStrictEqual(repositoryResponse) @@ -33,7 +33,7 @@ it("includes access token if configured", async (ctx) => { ctx, }) - const client = createTestClient({ clientConfig }) + const client = createTestClient({ clientConfig, ctx }) const res = await client.getRepository() expect(res).toStrictEqual(repositoryResponse) diff --git a/test/client-getTags.test.ts b/test/client-getTags.test.ts index c170591e..4723caea 100644 --- a/test/client-getTags.test.ts +++ b/test/client-getTags.test.ts @@ -15,7 +15,7 @@ it("returns all tags", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getTags() expect(res).toStrictEqual(repositoryResponse.tags) @@ -44,7 +44,7 @@ it("uses form endpoint if available", async (ctx) => { }), ) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.getTags() expect(res).toStrictEqual(tagsResponse) @@ -76,7 +76,7 @@ it("sends access token if form endpoint is used", async (ctx) => { }), ) - const client = createTestClient({ clientConfig: { accessToken } }) + const client = createTestClient({ clientConfig: { accessToken }, ctx }) const res = await client.getTags() expect(res).toStrictEqual(tagsResponse) diff --git a/test/client-graphQLFetch.test.ts b/test/client-graphQLFetch.test.ts index 5a4c540e..12a801ac 100644 --- a/test/client-graphQLFetch.test.ts +++ b/test/client-graphQLFetch.test.ts @@ -11,7 +11,7 @@ import { testConcurrentMethod } from "./__testutils__/testConcurrentMethod" it("resolves a query", async (ctx) => { const repositoryResponse = ctx.mock.api.repository() - const graphqlURL = `https://${createRepositoryName()}.cdn.prismic.io/graphql` + const graphqlURL = `https://${createRepositoryName(ctx)}.cdn.prismic.io/graphql` const graphqlResponse = { foo: "bar" } mockPrismicRestAPIV2({ @@ -26,7 +26,7 @@ it("resolves a query", async (ctx) => { }), ) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.graphQLFetch(graphqlURL) const json = await res.json() @@ -38,7 +38,7 @@ it("merges provided headers with defaults", async (ctx) => { repositoryResponse.integrationFieldsRef = ctx.mock.api.ref().ref const ref = "custom-ref" - const graphqlURL = `https://${createRepositoryName()}.cdn.prismic.io/graphql` + const graphqlURL = `https://${createRepositoryName(ctx)}.cdn.prismic.io/graphql` const graphqlResponse = { foo: "bar" } mockPrismicRestAPIV2({ @@ -57,7 +57,7 @@ it("merges provided headers with defaults", async (ctx) => { }), ) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.graphQLFetch(graphqlURL, { headers: { "Prismic-Ref": ref, @@ -73,7 +73,7 @@ it("includes Authorization header if access token is provided", async (ctx) => { const repositoryResponse = ctx.mock.api.repository() repositoryResponse.integrationFieldsRef = ctx.mock.api.ref().ref - const graphqlURL = `https://${createRepositoryName()}.cdn.prismic.io/graphql` + const graphqlURL = `https://${createRepositoryName(ctx)}.cdn.prismic.io/graphql` const graphqlResponse = { foo: "bar" } mockPrismicRestAPIV2({ @@ -92,7 +92,7 @@ it("includes Authorization header if access token is provided", async (ctx) => { }), ) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.graphQLFetch(graphqlURL) const json = await res.json() @@ -104,7 +104,7 @@ it("includes integration fields header if ref is available", async (ctx) => { const repositoryResponse = ctx.mock.api.repository() const accessToken = "accessToken" - const graphqlURL = `https://${createRepositoryName()}.cdn.prismic.io/graphql` + const graphqlURL = `https://${createRepositoryName(ctx)}.cdn.prismic.io/graphql` const graphqlResponse = { foo: "bar" } mockPrismicRestAPIV2({ @@ -123,7 +123,7 @@ it("includes integration fields header if ref is available", async (ctx) => { }), ) - const client = createTestClient({ clientConfig: { accessToken } }) + const client = createTestClient({ clientConfig: { accessToken }, ctx }) const res = await client.graphQLFetch(graphqlURL) const json = await res.json() @@ -133,7 +133,7 @@ it("includes integration fields header if ref is available", async (ctx) => { it("optimizes queries by removing whitespace", async (ctx) => { const repositoryResponse = ctx.mock.api.repository() - const graphqlURL = `https://${createRepositoryName()}.cdn.prismic.io/graphql` + const graphqlURL = `https://${createRepositoryName(ctx)}.cdn.prismic.io/graphql` const graphqlResponse = { foo: "bar" } const graphqlURLWithUncompressedQuery = new URL(graphqlURL) @@ -168,7 +168,7 @@ it("optimizes queries by removing whitespace", async (ctx) => { }), ) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.graphQLFetch( graphqlURLWithUncompressedQuery.toString(), ) @@ -181,7 +181,7 @@ it("includes a ref URL parameter to cache-bust", async (ctx) => { const repositoryResponse = ctx.mock.api.repository() const ref = getMasterRef(repositoryResponse) - const graphqlURL = `https://${createRepositoryName()}.cdn.prismic.io/graphql` + const graphqlURL = `https://${createRepositoryName(ctx)}.cdn.prismic.io/graphql` const graphqlResponse = { foo: "bar" } mockPrismicRestAPIV2({ @@ -199,7 +199,7 @@ it("includes a ref URL parameter to cache-bust", async (ctx) => { }), ) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.graphQLFetch(graphqlURL) const json = await res.json() @@ -214,7 +214,7 @@ it("is abortable with an AbortController", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(async () => { await client.graphQLFetch("https://foo.cdn.prismic.io/graphql", { @@ -226,7 +226,7 @@ it("is abortable with an AbortController", async (ctx) => { testConcurrentMethod("does not share concurrent equivalent network requests", { run: (client, params) => client.graphQLFetch( - `https://${createRepositoryName()}.cdn.prismic.io/graphql`, + `https://${client.repositoryName}.cdn.prismic.io/graphql`, params, ), mode: "NOT-SHARED___graphQL", diff --git a/test/client-queryContentFromRef.test.ts b/test/client-queryContentFromRef.test.ts index aa74ad65..c5d6e676 100644 --- a/test/client-queryContentFromRef.test.ts +++ b/test/client-queryContentFromRef.test.ts @@ -42,7 +42,7 @@ it("uses master ref if manual thunk ref returns non-string value", async (ctx) = ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.queryContentFromRef(async () => undefined) diff --git a/test/client-queryContentFromReleaseById.test.ts b/test/client-queryContentFromReleaseById.test.ts index 3855d2ef..2b519e12 100644 --- a/test/client-queryContentFromReleaseById.test.ts +++ b/test/client-queryContentFromReleaseById.test.ts @@ -28,7 +28,7 @@ it("uses a releases ref by ID", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.queryContentFromReleaseByID(ref1.id) diff --git a/test/client-queryContentFromReleaseByLabel.test.ts b/test/client-queryContentFromReleaseByLabel.test.ts index 8eb2e78f..e43d3a49 100644 --- a/test/client-queryContentFromReleaseByLabel.test.ts +++ b/test/client-queryContentFromReleaseByLabel.test.ts @@ -28,7 +28,7 @@ it("uses a releases ref by label", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.queryContentFromReleaseByLabel(ref1.label) diff --git a/test/client-resolvePreviewUrl.test.ts b/test/client-resolvePreviewUrl.test.ts index 46b13418..50950f71 100644 --- a/test/client-resolvePreviewUrl.test.ts +++ b/test/client-resolvePreviewUrl.test.ts @@ -32,7 +32,7 @@ it("resolves a preview url in the browser", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.resolvePreviewURL({ linkResolver: (document) => `/${document.uid}`, defaultURL: "defaultURL", @@ -67,7 +67,7 @@ it("resolves a preview url using a server req object", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.enableAutoPreviewsFromReq(req) const res = await client.resolvePreviewURL({ linkResolver: (document) => `/${document.uid}`, @@ -102,7 +102,7 @@ it("resolves a preview url using a Web API-based server req object", async (ctx) ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.enableAutoPreviewsFromReq(req) const res = await client.resolvePreviewURL({ linkResolver: (document) => `/${document.uid}`, @@ -135,7 +135,7 @@ it("resolves a preview url using a Web API-based server req object containing a ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.enableAutoPreviewsFromReq(req) const res = await client.resolvePreviewURL({ linkResolver: (document) => `/${document.uid}`, @@ -168,7 +168,7 @@ it("allows providing an explicit documentId and previewToken", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.enableAutoPreviewsFromReq(req) const res = await client.resolvePreviewURL({ linkResolver: (document) => `/${document.uid}`, @@ -180,14 +180,14 @@ it("allows providing an explicit documentId and previewToken", async (ctx) => { expect(res).toBe(`/${document.uid}`) }) -it("returns defaultURL if current url does not contain preview params in browser", async () => { +it("returns defaultURL if current url does not contain preview params in browser", async (ctx) => { const defaultURL = "defaultURL" // Set a global Location object without the parameters we need for automatic // preview support. globalThis.location = { ...globalThis.location, search: "" } - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.resolvePreviewURL({ linkResolver: (document) => `/${document.uid}`, defaultURL, @@ -199,11 +199,11 @@ it("returns defaultURL if current url does not contain preview params in browser globalThis.location = undefined }) -it("returns defaultURL if req does not contain preview params in server req object", async () => { +it("returns defaultURL if req does not contain preview params in server req object", async (ctx) => { const defaultURL = "defaultURL" const req = {} - const client = createTestClient() + const client = createTestClient({ ctx }) client.enableAutoPreviewsFromReq(req) const res = await client.resolvePreviewURL({ linkResolver: (document) => `/${document.uid}`, @@ -213,10 +213,10 @@ it("returns defaultURL if req does not contain preview params in server req obje expect(res).toBe(defaultURL) }) -it("returns defaultURL if no preview context is available", async () => { +it("returns defaultURL if no preview context is available", async (ctx) => { const defaultURL = "defaultURL" - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.resolvePreviewURL({ linkResolver: (document) => `/${document.uid}`, defaultURL, @@ -245,7 +245,7 @@ it("returns defaultURL if resolved URL is not a string", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.resolvePreviewURL({ linkResolver: () => null, defaultURL, diff --git a/test/client.test.ts b/test/client.test.ts index 69b08e13..a3c32c92 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -345,7 +345,7 @@ it("uses the master ref by default", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -361,7 +361,7 @@ it("supports manual string ref", async (ctx) => { ctx, }) - const client = createTestClient({ clientConfig: { ref } }) + const client = createTestClient({ clientConfig: { ref }, ctx }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -377,7 +377,7 @@ it("supports manual thunk ref", async (ctx) => { ctx, }) - const client = createTestClient({ clientConfig: { ref: () => ref } }) + const client = createTestClient({ clientConfig: { ref: () => ref }, ctx }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -394,7 +394,10 @@ it("uses master ref if ref thunk param returns non-string value", async (ctx) => ctx, }) - const client = createTestClient({ clientConfig: { ref: () => undefined } }) + const client = createTestClient({ + clientConfig: { ref: () => undefined }, + ctx, + }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -415,7 +418,7 @@ it("uses browser preview ref if available", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -439,7 +442,7 @@ it("uses req preview ref if available", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.enableAutoPreviewsFromReq(req) const res = await client.get() @@ -463,7 +466,7 @@ it("supports req with Web APIs", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.enableAutoPreviewsFromReq(req) const res = await client.get() @@ -485,7 +488,7 @@ it("ignores req without cookies", async (ctx) => { ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) client.enableAutoPreviewsFromReq(req) const res = await client.get() @@ -502,7 +505,7 @@ it("does not use preview ref if auto previews are disabled", async (ctx) => { const repositoryResponse = ctx.mock.api.repository() const queryResponse = prismicM.api.query({ seed: ctx.task.name }) - const client = createTestClient() + const client = createTestClient({ ctx }) // Disable auto previews and ensure the default ref is being used. Note that // the global cookie has already been set by this point, which should be @@ -551,7 +554,7 @@ it("uses the integration fields ref if the repository provides it", async (ctx) ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -571,7 +574,7 @@ it("ignores the integration fields ref if the repository provides a null value", ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -606,7 +609,7 @@ it("uses client-provided routes in queries", async (ctx) => { ctx, }) - const client = createTestClient({ clientConfig: { routes } }) + const client = createTestClient({ clientConfig: { routes }, ctx }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -625,7 +628,7 @@ it("uses client-provided brokenRoute in queries", async (ctx) => { ctx, }) - const client = createTestClient({ clientConfig: { brokenRoute } }) + const client = createTestClient({ clientConfig: { brokenRoute }, ctx }) const res = await client.get() expect(res).toStrictEqual(queryResponse) @@ -637,7 +640,7 @@ it("throws ForbiddenError if access token is invalid for repository metadata", a ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(() => client.getRepository()).rejects.toThrowError( /invalid access token/i, @@ -653,7 +656,7 @@ it("throws ForbiddenError if access token is invalid for query", async (ctx) => ctx, }) - const client = createTestClient() + const client = createTestClient({ ctx }) await expect(() => client.get()).rejects.toThrowError(/invalid access token/i) await expect(() => client.get()).rejects.toThrowError(prismic.ForbiddenError) @@ -666,7 +669,7 @@ it("throws ForbiddenError if response code is 403", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "documents/search", @@ -704,7 +707,7 @@ it("throws ParsingError if response code is 400 with parsing-error type", async mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "documents/search", @@ -726,7 +729,7 @@ it("throws PrismicError if response code is 400 but is not a parsing error", asy mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "documents/search", @@ -747,7 +750,7 @@ it("throws PrismicError if response is not 200, 400, 401, 403, or 404", async (c mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "./documents/search", @@ -767,7 +770,7 @@ it("throws PrismicError if response is not 200, 400, 401, 403, or 404", async (c it("throws PrismicError if response is not JSON", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "documents/search", @@ -785,7 +788,7 @@ it("throws PrismicError if response is not JSON", async (ctx) => { }) it("throws RepositoryNotFoundError if repository does not exist", async (ctx) => { - const client = createTestClient() + const client = createTestClient({ ctx }) ctx.server.use( msw.rest.get(client.endpoint, (_req, res, ctx) => { @@ -807,7 +810,7 @@ it("throws RefNotFoundError if ref does not exist", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "./documents/search", @@ -834,7 +837,7 @@ it("throws RefExpiredError if ref is expired", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "./documents/search", @@ -861,7 +864,7 @@ it("throws PreviewTokenExpiredError if preview token is expired", async (ctx) => mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "./documents/search", @@ -888,7 +891,7 @@ it("throws NotFoundError if the 404 error is unknown", async (ctx) => { mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "./documents/search", @@ -923,7 +926,7 @@ it("retries after `retry-after` milliseconds if response code is 429", async (ct queryResponse, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "documents/search", @@ -986,7 +989,7 @@ it("retries after 1000 milliseconds if response code is 429 and an invalid `retr queryResponse, }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "documents/search", @@ -1033,7 +1036,7 @@ it("throws if a non-2xx response is returned even after retrying", async (ctx) = mockPrismicRestAPIV2({ ctx }) - const client = createTestClient() + const client = createTestClient({ ctx }) const queryEndpoint = new URL( "documents/search", From 012f59fc5405de38e5fcbcf1f229e3bdf3d16789 Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 5 Sep 2024 16:21:05 +0200 Subject: [PATCH 30/61] test: `WriteClient.fetchForeignAsset` --- src/WriteClient.ts | 2 +- test/__testutils__/mockPrismicAssetAPI.ts | 33 ++++- test/__testutils__/mockPrismicMigrationAPI.ts | 16 +++ test/writeClient-createAsset.test.ts | 18 ++- test/writeClient-createAssetTag.test.ts | 30 +++- test/writeClient-createDocument.test.ts | 31 +++- test/writeClient-fetchForeignAsset.test.ts | 133 ++++++++++++++++++ test/writeClient-getAssets.test.ts | 15 +- test/writeClient-migrateCreateAssets.test.ts | 2 +- test/writeClient-updateDocument.test.ts | 32 ++++- 10 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 test/writeClient-fetchForeignAsset.test.ts diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 9ad3c29f..8d424a73 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -1002,7 +1002,7 @@ export class WriteClient< const blob = await res.blob() return new File([blob], "", { - type: res.headers.get("content-type") || "", + type: res.headers.get("content-type") || undefined, }) } diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index d3136ff3..78a34817 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -1,5 +1,6 @@ import { type TestContext } from "vitest" +import type { RestRequest } from "msw" import { rest } from "msw" import type { WriteClient } from "../../src" @@ -20,6 +21,7 @@ type MockPrismicAssetAPIArgs = { ctx: TestContext client: WriteClient writeToken?: string + requiredHeaders?: Record getRequiredParams?: Record existingAssets?: Asset[][] | number[] newAssets?: Asset[] @@ -85,6 +87,16 @@ export const mockPrismicAssetAPI = ( }) || [] const tagsDatabase: AssetTag[] = args.existingTags || [] + const validateHeaders = (req: RestRequest) => { + if (args.requiredHeaders) { + for (const name in args.requiredHeaders) { + const requiredValue = args.requiredHeaders[name] + + args.ctx.expect(req.headers.get(name)).toBe(requiredValue) + } + } + } + args.ctx.server.use( rest.get(`${assetAPIEndpoint}assets`, async (req, res, ctx) => { if ( @@ -106,15 +118,25 @@ export const mockPrismicAssetAPI = ( } } + validateHeaders(req) + const index = Number.parseInt(req.url.searchParams.get("cursor") ?? "0") const items: Asset[] = assetsDatabase[index] || [] + let missing_ids: string[] | undefined + const ids = req.url.searchParams.getAll("ids") + if (ids.length) { + missing_ids = ids.filter((id) => + assetsDatabase.flat().find((item) => item.id === id), + ) + } + const response: GetAssetsResult = { total: items.length, items, is_opensearch_result: false, cursor: assetsDatabase[index + 1] ? `${index + 1}` : undefined, - missing_ids: [], + missing_ids, } return res(ctx.json(response)) @@ -127,6 +149,8 @@ export const mockPrismicAssetAPI = ( return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } + validateHeaders(req) + const response: PostAssetResult = args.newAssets?.shift() ?? mockAsset(args.ctx) @@ -142,6 +166,9 @@ export const mockPrismicAssetAPI = ( ) { return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } + + validateHeaders(req) + const { tags, ...body } = await req.json() const asset = assetsDatabase @@ -179,6 +206,8 @@ export const mockPrismicAssetAPI = ( return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } + validateHeaders(req) + const items: AssetTag[] = tagsDatabase const response: GetAssetTagsResult = { items } @@ -193,6 +222,8 @@ export const mockPrismicAssetAPI = ( return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } + validateHeaders(req) + const body = await req.json() const tag: AssetTag = { diff --git a/test/__testutils__/mockPrismicMigrationAPI.ts b/test/__testutils__/mockPrismicMigrationAPI.ts index a610338e..1c03fcd6 100644 --- a/test/__testutils__/mockPrismicMigrationAPI.ts +++ b/test/__testutils__/mockPrismicMigrationAPI.ts @@ -1,5 +1,6 @@ import type { TestContext } from "vitest" +import type { RestRequest } from "msw" import { rest } from "msw" import type { PrismicDocument, WriteClient } from "../../src" @@ -15,6 +16,7 @@ type MockPrismicMigrationAPIArgs = { client: WriteClient writeToken?: string migrationAPIKey?: string + requiredHeaders?: Record existingDocuments?: (PostDocumentResult | PrismicDocument)[] | number newDocuments?: { id: string; masterLanguageDocumentID?: string }[] } @@ -33,6 +35,16 @@ export const mockPrismicMigrationAPI = ( const documentsDatabase: Record = {} + const validateHeaders = (req: RestRequest) => { + if (args.requiredHeaders) { + for (const name in args.requiredHeaders) { + const requiredValue = args.requiredHeaders[name] + + args.ctx.expect(req.headers.get(name)).toBe(requiredValue) + } + } + } + if (args.existingDocuments) { if (typeof args.existingDocuments === "number") { for (let i = 0; i < args.existingDocuments; i++) { @@ -72,6 +84,8 @@ export const mockPrismicMigrationAPI = ( return res(ctx.status(403), ctx.json({ Message: "forbidden" })) } + validateHeaders(req) + const body = await req.json() const newDocument = args.newDocuments?.shift() @@ -105,6 +119,8 @@ export const mockPrismicMigrationAPI = ( return res(ctx.status(403), ctx.json({ Message: "forbidden" })) } + validateHeaders(req) + const document = documentsDatabase[req.params.id as string] if (!document) { diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts index d6cee5c5..f15232a5 100644 --- a/test/writeClient-createAsset.test.ts +++ b/test/writeClient-createAsset.test.ts @@ -192,7 +192,7 @@ it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { // It seems like p-limit and node-fetch are not happy friends :/ // https://github.com/sindresorhus/p-limit/issues/64 -it.skip("supports abort controller", async (ctx) => { +it.skip("is abortable with an AbortController", async (ctx) => { const client = createTestWriteClient({ ctx }) const controller = new AbortController() @@ -208,6 +208,22 @@ it.skip("supports abort controller", async (ctx) => { ).rejects.toThrow(/aborted/i) }) +it.concurrent("supports custom headers", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const headers = { "x-custom": "foo" } + + mockPrismicAssetAPI({ ctx, client, requiredHeaders: headers }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + fetchOptions: { headers }, + }) + + ctx.expect(asset.id).toBeTypeOf("string") + ctx.expect.assertions(2) +}) + it.concurrent("respects unknown rate limit", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-createAssetTag.test.ts b/test/writeClient-createAssetTag.test.ts index bd4386f1..6b08f6c2 100644 --- a/test/writeClient-createAssetTag.test.ts +++ b/test/writeClient-createAssetTag.test.ts @@ -67,7 +67,7 @@ it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { // It seems like p-limit and node-fetch are not happy friends :/ // https://github.com/sindresorhus/p-limit/issues/64 -it.skip("supports abort controller", async (ctx) => { +it.skip("is abortable with an AbortController", async (ctx) => { const client = createTestWriteClient({ ctx }) const controller = new AbortController() @@ -83,6 +83,34 @@ it.skip("supports abort controller", async (ctx) => { ).rejects.toThrow(/aborted/i) }) +it.concurrent("supports custom headers", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const headers = { "x-custom": "foo" } + + const newTag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ + ctx, + client, + requiredHeaders: headers, + newTags: [newTag], + }) + + // @ts-expect-error - testing purposes + const tag = await client.createAssetTag(newTag.name, { + fetchOptions: { headers }, + }) + + ctx.expect(tag).toStrictEqual(newTag) + ctx.expect.assertions(2) +}) + it.concurrent("respects unknown rate limit", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-createDocument.test.ts b/test/writeClient-createDocument.test.ts index b6205a8d..4c36bfd6 100644 --- a/test/writeClient-createDocument.test.ts +++ b/test/writeClient-createDocument.test.ts @@ -48,7 +48,7 @@ it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { // It seems like p-limit and node-fetch are not happy friends :/ // https://github.com/sindresorhus/p-limit/issues/64 -it.skip("supports abort controller", async (ctx) => { +it.skip("is abortable with an AbortController", async (ctx) => { const client = createTestWriteClient({ ctx }) const controller = new AbortController() @@ -71,6 +71,35 @@ it.skip("supports abort controller", async (ctx) => { ).rejects.toThrow(/aborted/i) }) +it.concurrent("supports custom headers", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const headers = { "x-custom": "foo" } + const newDocument = { id: "foo" } + + mockPrismicMigrationAPI({ + ctx, + client, + requiredHeaders: headers, + newDocuments: [newDocument], + }) + + // @ts-expect-error - testing purposes + const { id } = await client.createDocument( + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + { fetchOptions: { headers } }, + ) + + ctx.expect(id).toBe(newDocument.id) + ctx.expect.assertions(2) +}) + it.concurrent("respects unknown rate limit", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-fetchForeignAsset.test.ts b/test/writeClient-fetchForeignAsset.test.ts new file mode 100644 index 00000000..44938bc9 --- /dev/null +++ b/test/writeClient-fetchForeignAsset.test.ts @@ -0,0 +1,133 @@ +import { it as _it, expect } from "vitest" + +import { rest } from "msw" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" + +// Skip test on Node 16 and 18 (File support) +const isNode16 = process.version.startsWith("v16") +const it = _it.skipIf(isNode16) + +it.concurrent("fetches a foreign asset with content type", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const url = `https://example.com/${client.repositoryName}.jpg` + + ctx.server.use( + rest.get(url, (_req, res, ctx) => { + return res(ctx.text("foo")) + }), + ) + + // @ts-expect-error - testing purposes + const file = await client.fetchForeignAsset(url) + + expect(file.type).toBe("text/plain") +}) + +it.concurrent("fetches a foreign asset with no content type", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const url = `https://example.com/${client.repositoryName}.jpg` + + ctx.server.use( + rest.get(url, (_req, res, ctx) => { + return res(ctx.set("content-type", ""), ctx.body("foo")) + }), + ) + + // @ts-expect-error - testing purposes + const file = await client.fetchForeignAsset(url) + + expect(file.type).toBe("") +}) + +it.concurrent("is abortable with an AbortController", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + const url = `https://example.com/${client.repositoryName}.jpg` + + ctx.server.use( + rest.get(url, (_req, res, ctx) => { + return res(ctx.set("content-type", ""), ctx.body("foo")) + }), + ) + + await expect(() => + // @ts-expect-error - testing purposes + client.fetchForeignAsset(url, { + fetchOptions: { signal: controller.signal }, + }), + ).rejects.toThrow(/aborted/i) +}) + +it.concurrent("is abortable with a global AbortController", async (ctx) => { + const controller = new AbortController() + controller.abort() + const client = createTestWriteClient({ + ctx, + clientConfig: { fetchOptions: { signal: controller.signal } }, + }) + + const url = `https://example.com/${client.repositoryName}.jpg` + + ctx.server.use( + rest.get(url, (_req, res, ctx) => { + return res(ctx.set("content-type", ""), ctx.body("foo")) + }), + ) + + await expect(() => + // @ts-expect-error - testing purposes + client.fetchForeignAsset(url), + ).rejects.toThrow(/aborted/i) +}) + +it.concurrent("supports custom headers", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const headers = { "x-custom": "foo" } + const url = `https://example.com/${client.repositoryName}.jpg` + + ctx.server.use( + rest.get(url, (req, res, restCtx) => { + ctx.expect(req.headers.get("x-custom")).toBe("foo") + + return res(restCtx.text("foo")) + }), + ) + + // @ts-expect-error - testing purposes + const file = await client.fetchForeignAsset(url, { + fetchOptions: { headers }, + }) + + ctx.expect(file.type).toBe("text/plain") + ctx.expect.assertions(2) +}) + +it.concurrent("supports global custom headers", async (ctx) => { + const headers = { "x-custom": "foo" } + const client = createTestWriteClient({ + ctx, + clientConfig: { fetchOptions: { headers } }, + }) + + const url = `https://example.com/${client.repositoryName}.jpg` + + ctx.server.use( + rest.get(url, (req, res, restCtx) => { + ctx.expect(req.headers.get("x-custom")).toBe("foo") + + return res(restCtx.text("foo")) + }), + ) + + // @ts-expect-error - testing purposes + const file = await client.fetchForeignAsset(url) + + ctx.expect(file.type).toBe("text/plain") + ctx.expect.assertions(2) +}) diff --git a/test/writeClient-getAssets.test.ts b/test/writeClient-getAssets.test.ts index 1cfdc9e1..e2c2de4c 100644 --- a/test/writeClient-getAssets.test.ts +++ b/test/writeClient-getAssets.test.ts @@ -228,7 +228,7 @@ it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { await expect(() => client.getAssets()).rejects.toThrow(ForbiddenError) }) -it.concurrent("supports abort controller", async (ctx) => { +it.concurrent("is abortable with an AbortController", async (ctx) => { const client = createTestWriteClient({ ctx }) const controller = new AbortController() @@ -242,3 +242,16 @@ it.concurrent("supports abort controller", async (ctx) => { }), ).rejects.toThrow(/aborted/i) }) + +it.concurrent("supports custom headers", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const headers = { "x-custom": "foo" } + + mockPrismicAssetAPI({ ctx, client, requiredHeaders: headers }) + + const { results } = await client.getAssets({ fetchOptions: { headers } }) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) diff --git a/test/writeClient-migrateCreateAssets.test.ts b/test/writeClient-migrateCreateAssets.test.ts index 064d6c81..d0756a35 100644 --- a/test/writeClient-migrateCreateAssets.test.ts +++ b/test/writeClient-migrateCreateAssets.test.ts @@ -247,7 +247,7 @@ it.concurrent( ctx.server.use( rest.get(asset.url.split("?")[0], (_req, res, ctx) => { - return res(ctx.set("content-type", ""), ctx.text("foo")) + return res(ctx.body("foo")) }), ) diff --git a/test/writeClient-updateDocument.test.ts b/test/writeClient-updateDocument.test.ts index 4b72883b..8dab4741 100644 --- a/test/writeClient-updateDocument.test.ts +++ b/test/writeClient-updateDocument.test.ts @@ -88,7 +88,7 @@ it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { // It seems like p-limit and node-fetch are not happy friends :/ // https://github.com/sindresorhus/p-limit/issues/64 -it.skip("supports abort controller", async (ctx) => { +it.skip("is abortable with an AbortController", async (ctx) => { const client = createTestWriteClient({ ctx }) const controller = new AbortController() @@ -114,3 +114,33 @@ it.skip("supports abort controller", async (ctx) => { ), ).rejects.toThrow(/aborted/i) }) + +it.concurrent("supports custom headers", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const headers = { "x-custom": "foo" } + + const { documentsDatabase } = mockPrismicMigrationAPI({ + ctx, + client, + requiredHeaders: headers, + existingDocuments: 1, + }) + + const document = Object.values(documentsDatabase)[0] + + await ctx + .expect( + // @ts-expect-error - testing purposes + client.updateDocument( + document.id, + { + uid: "uid", + data: {}, + }, + { fetchOptions: { headers } }, + ), + ) + .resolves.toBeUndefined() + ctx.expect.assertions(2) +}) From e92f1a67de53b478c574f5eeaf443b747fe07a31 Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 5 Sep 2024 19:57:49 +0200 Subject: [PATCH 31/61] test: `WriteClient.migrateUpdateDocuments` (WIP) --- src/WriteClient.ts | 248 +++--- ...Client-migrateUpdateDocuments.test.ts.snap | 763 ++++++++++++++++++ test/__testutils__/mockPrismicAssetAPI.ts | 26 +- test/__testutils__/mockPrismicMigrationAPI.ts | 21 +- .../testMigrationFieldPatching.ts | 195 +++++ test/client.test.ts | 6 + test/helpers-mapSliceZone.test.ts | 48 +- test/writeClient-migrateCreateAssets.test.ts | 48 +- ...writeClient-migrateCreateDocuments.test.ts | 72 +- ...writeClient-migrateUpdateDocuments.test.ts | 46 ++ 10 files changed, 1287 insertions(+), 186 deletions(-) create mode 100644 test/__snapshots__/writeClient-migrateUpdateDocuments.test.ts.snap create mode 100644 test/__testutils__/testMigrationFieldPatching.ts create mode 100644 test/writeClient-migrateUpdateDocuments.test.ts diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 8d424a73..a2e75851 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -448,69 +448,67 @@ export class WriteClient< // Create assets let i = 0 let created = 0 - if (migration.assets.size) { - for (const [_, migrationAsset] of migration.assets) { - if ( - typeof migrationAsset.id === "string" && - assets.has(migrationAsset.id) - ) { - reporter?.({ - type: "assets:skipping", - data: { - reason: "already exists", - current: ++i, - remaining: migration.assets.size - i, - total: migration.assets.size, - asset: migrationAsset, - }, - }) - } else { - created++ - reporter?.({ - type: "assets:creating", - data: { - current: ++i, - remaining: migration.assets.size - i, - total: migration.assets.size, - asset: migrationAsset, - }, - }) - - const { id, file, filename, ...params } = migrationAsset - - let resolvedFile: PostAssetParams["file"] | File - if (typeof file === "string") { - let url: URL | undefined - try { - url = new URL(file) - } catch (error) { - // noop - } + for (const [_, migrationAsset] of migration.assets) { + if ( + typeof migrationAsset.id === "string" && + assets.has(migrationAsset.id) + ) { + reporter?.({ + type: "assets:skipping", + data: { + reason: "already exists", + current: ++i, + remaining: migration.assets.size - i, + total: migration.assets.size, + asset: migrationAsset, + }, + }) + } else { + created++ + reporter?.({ + type: "assets:creating", + data: { + current: ++i, + remaining: migration.assets.size - i, + total: migration.assets.size, + asset: migrationAsset, + }, + }) - if (url) { - resolvedFile = await this.fetchForeignAsset( - url.toString(), - fetchParams, - ) - } else { - resolvedFile = file - } - } else if (file instanceof URL) { + const { id, file, filename, ...params } = migrationAsset + + let resolvedFile: PostAssetParams["file"] | File + if (typeof file === "string") { + let url: URL | undefined + try { + url = new URL(file) + } catch (error) { + // noop + } + + if (url) { resolvedFile = await this.fetchForeignAsset( - file.toString(), + url.toString(), fetchParams, ) } else { resolvedFile = file } + } else if (file instanceof URL) { + resolvedFile = await this.fetchForeignAsset( + file.toString(), + fetchParams, + ) + } else { + resolvedFile = file + } - const asset = await this.createAsset(resolvedFile, filename, { - ...params, - ...fetchParams, - }) + const asset = await this.createAsset(resolvedFile, filename, { + ...params, + ...fetchParams, + }) - assets.set(id, asset) - } + assets.set(id, asset) } } @@ -585,85 +583,83 @@ export class WriteClient< let i = 0 let created = 0 - if (sortedDocuments.length) { - for (const { document, params } of sortedDocuments) { - if (document.id && documents.has(document.id)) { - reporter?.({ - type: "documents:skipping", - data: { - reason: "already exists", - current: ++i, - remaining: sortedDocuments.length - i, - total: sortedDocuments.length, - document, - documentParams: params, - }, - }) - - // Index the migration document - documents.set(document, documents.get(document.id)!) - } else { - created++ - reporter?.({ - type: "documents:creating", - data: { - current: ++i, - remaining: sortedDocuments.length - i, - total: sortedDocuments.length, - document, - documentParams: params, - }, - }) - - // Resolve master language document ID for non-master locale documents - let masterLanguageDocumentID: string | undefined - if (document.lang !== masterLocale) { - if (params.masterLanguageDocument) { - if (typeof params.masterLanguageDocument === "function") { - const masterLanguageDocument = - await params.masterLanguageDocument() - - if (masterLanguageDocument) { - // `masterLanguageDocument` is an existing document - if (masterLanguageDocument.id) { - masterLanguageDocumentID = documents.get( - masterLanguageDocument.id, - )?.id - } - - // `masterLanguageDocument` is a new document - if (!masterLanguageDocumentID) { - masterLanguageDocumentID = documents.get( - masterLanguageDocument, - )?.id - } + for (const { document, params } of sortedDocuments) { + if (document.id && documents.has(document.id)) { + reporter?.({ + type: "documents:skipping", + data: { + reason: "already exists", + current: ++i, + remaining: sortedDocuments.length - i, + total: sortedDocuments.length, + document, + documentParams: params, + }, + }) + + // Index the migration document + documents.set(document, documents.get(document.id)!) + } else { + created++ + reporter?.({ + type: "documents:creating", + data: { + current: ++i, + remaining: sortedDocuments.length - i, + total: sortedDocuments.length, + document, + documentParams: params, + }, + }) + + // Resolve master language document ID for non-master locale documents + let masterLanguageDocumentID: string | undefined + if (document.lang !== masterLocale) { + if (params.masterLanguageDocument) { + if (typeof params.masterLanguageDocument === "function") { + const masterLanguageDocument = + await params.masterLanguageDocument() + + if (masterLanguageDocument) { + // `masterLanguageDocument` is an existing document + if (masterLanguageDocument.id) { + masterLanguageDocumentID = documents.get( + masterLanguageDocument.id, + )?.id + } + + // `masterLanguageDocument` is a new document + if (!masterLanguageDocumentID) { + masterLanguageDocumentID = documents.get( + masterLanguageDocument, + )?.id } - } else { - masterLanguageDocumentID = params.masterLanguageDocument.id } - } else if (document.alternate_languages) { - masterLanguageDocumentID = document.alternate_languages.find( - ({ lang }) => lang === masterLocale, - )?.id + } else { + masterLanguageDocumentID = params.masterLanguageDocument.id } + } else if (document.alternate_languages) { + masterLanguageDocumentID = document.alternate_languages.find( + ({ lang }) => lang === masterLocale, + )?.id } + } - const { id } = await this.createDocument( - // We'll upload docuements data later on. - { ...document, data: {} }, - params.documentName, - { - masterLanguageDocumentID, - ...fetchParams, - }, - ) + const { id } = await this.createDocument( + // We'll upload docuements data later on. + { ...document, data: {} }, + params.documentName, + { + masterLanguageDocumentID, + ...fetchParams, + }, + ) - // Index old ID for Prismic to Prismic migration - if (document.id) { - documents.set(document.id, { ...document, id }) - } - documents.set(document, { ...document, id }) + // Index old ID for Prismic to Prismic migration + if (document.id) { + documents.set(document.id, { ...document, id }) } + documents.set(document, { ...document, id }) } } diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments.test.ts.snap b/test/__snapshots__/writeClient-migrateUpdateDocuments.test.ts.snap new file mode 100644 index 00000000..bdf2af8c --- /dev/null +++ b/test/__snapshots__/writeClient-migrateUpdateDocuments.test.ts.snap @@ -0,0 +1,763 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`patches image fields > regular > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "ef95d5daa4d", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", + }, + }, + ], +} +`; + +exports[`patches image fields > regular > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > regular > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "45306297c5e", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "45306297c5e", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > regular > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "c5c95f8d3ac", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, +} +`; + +exports[`patches image fields > richText > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "cdfdd322ca9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches image fields > richText > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > richText > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "7963dc361cc", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "7963dc361cc", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > richText > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "0bbad670dad", + "type": "image", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + }, + ], +} +`; + +exports[`patches link fields > existingDocument > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "058058c", + "isBroken": false, + "lang": "laoreet", + "link_type": "Document", + "tags": [], + "type": "orci", + }, + }, + ], +} +`; + +exports[`patches link fields > existingDocument > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "0d137ed", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + }, + ], + "primary": { + "field": { + "id": "0d137ed", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "group": [ + { + "field": { + "id": "0d137ed", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > existingDocument > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "382a11c", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + }, + ], + "primary": { + "field": { + "id": "382a11c", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > existingDocument > static zone 1`] = ` +{ + "field": { + "id": "edeaf3a", + "isBroken": false, + "lang": "eget", + "link_type": "Document", + "tags": [], + "type": "phasellus", + }, +} +`; + +exports[`patches link fields > migrationDocument > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "9f3857a", + "isBroken": false, + "lang": "a", + "link_type": "Document", + "tags": [ + "Odio Eu", + ], + "type": "amet", + "uid": "Non sodales neque", + }, + }, + ], +} +`; + +exports[`patches link fields > migrationDocument > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "aa66b08", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + }, + ], + "primary": { + "field": { + "id": "aa66b08", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + "group": [ + { + "field": { + "id": "aa66b08", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > migrationDocument > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "dfcf6a9", + "isBroken": false, + "lang": "ultrices", + "link_type": "Document", + "tags": [ + "Ipsum Consequat", + ], + "type": "eget", + "uid": "Amet dictum sit", + }, + }, + ], + "primary": { + "field": { + "id": "dfcf6a9", + "isBroken": false, + "lang": "ultrices", + "link_type": "Document", + "tags": [ + "Ipsum Consequat", + ], + "type": "eget", + "uid": "Amet dictum sit", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > migrationDocument > static zone 1`] = ` +{ + "field": { + "id": "3f9fce9", + "isBroken": false, + "lang": "nisi,", + "link_type": "Document", + "tags": [], + "type": "eros", + "uid": "Scelerisque mauris pellentesque", + }, +} +`; + +exports[`patches link fields > otherRepositoryContentRelationship > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "058058c", + "isBroken": false, + "lang": "nulla", + "link_type": "Document", + "tags": [ + "Convallis Posuere", + ], + "type": "vitae", + "url": "https://ac.example", + }, + }, + ], +} +`; + +exports[`patches link fields > otherRepositoryContentRelationship > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "0d137ed", + "isBroken": false, + "lang": "faucibus", + "link_type": "Document", + "tags": [ + "Ultrices", + ], + "type": "egestas_integer", + "url": "https://scelerisque.example", + }, + }, + ], + "primary": { + "field": { + "id": "0d137ed", + "isBroken": false, + "lang": "fringilla", + "link_type": "Document", + "tags": [], + "type": "amet", + "url": "https://nec.example", + }, + "group": [ + { + "field": { + "id": "0d137ed", + "isBroken": false, + "lang": "neque", + "link_type": "Document", + "tags": [], + "type": "lacus", + "url": "https://cras.example", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "382a11c", + "isBroken": false, + "lang": "enim,", + "link_type": "Document", + "tags": [], + "type": "felis_donec", + "url": "https://sem.example", + }, + }, + ], + "primary": { + "field": { + "id": "382a11c", + "isBroken": false, + "lang": "ac", + "link_type": "Document", + "tags": [], + "type": "enim_praesent", + "url": "https://sit.example", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > otherRepositoryContentRelationship > static zone 1`] = ` +{ + "field": { + "id": "edeaf3a", + "isBroken": false, + "lang": "elementum", + "link_type": "Document", + "tags": [], + "type": "aenean", + "url": "https://volutpat.example", + }, +} +`; diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index 78a34817..e975c056 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -35,36 +35,35 @@ type MockPrismicAssetAPIReturnType = { } const DEFAULT_ASSET: Asset = { - id: "Yz7kzxAAAB0AREK7", - uploader_id: "uploader_id", + id: "default", + uploader_id: "", created_at: 0, last_modified: 0, kind: "image", - filename: "foo.jpg", + filename: "default.jpg", extension: "jpg", - url: "https://example.com/foo.jpg", + url: "https://example.com/default.jpg", width: 1, height: 1, size: 1, - notes: "notes", - credits: "credits", - alt: "alt", - origin_url: "origin_url", + notes: "", + credits: "", + alt: "", + origin_url: "", search_highlight: { filename: [], notes: [], credits: [], alt: [] }, tags: [], } -export const mockAsset = (ctx: TestContext): Asset => { - const { id, url, alt, copyright } = ctx.mock.value.image({ +export const mockAsset = (ctx: TestContext, assets?: Partial): Asset => { + const { id, url } = ctx.mock.value.image({ state: "filled", }) return { ...DEFAULT_ASSET, + ...assets, id, url, - alt: alt ?? undefined, - credits: copyright ?? undefined, } } @@ -170,7 +169,6 @@ export const mockPrismicAssetAPI = ( validateHeaders(req) const { tags, ...body } = await req.json() - const asset = assetsDatabase .flat() .find((asset) => asset.id === req.params.id) @@ -209,7 +207,6 @@ export const mockPrismicAssetAPI = ( validateHeaders(req) const items: AssetTag[] = tagsDatabase - const response: GetAssetTagsResult = { items } return res(ctx.json(response)) @@ -225,7 +222,6 @@ export const mockPrismicAssetAPI = ( validateHeaders(req) const body = await req.json() - const tag: AssetTag = { id: `${`${Date.now()}`.slice(-8)}-4444-4444-4444-121212121212`, created_at: Date.now(), diff --git a/test/__testutils__/mockPrismicMigrationAPI.ts b/test/__testutils__/mockPrismicMigrationAPI.ts index 1c03fcd6..d1b54a1d 100644 --- a/test/__testutils__/mockPrismicMigrationAPI.ts +++ b/test/__testutils__/mockPrismicMigrationAPI.ts @@ -22,7 +22,10 @@ type MockPrismicMigrationAPIArgs = { } type MockPrismicMigrationAPIReturnType = { - documentsDatabase: Record + documentsDatabase: Record< + string, + PostDocumentResult & Pick + > } export const mockPrismicMigrationAPI = ( @@ -33,7 +36,10 @@ export const mockPrismicMigrationAPI = ( const writeToken = args.writeToken || args.client.writeToken const migrationAPIKey = args.migrationAPIKey || args.client.migrationAPIKey - const documentsDatabase: Record = {} + const documentsDatabase: Record< + string, + PostDocumentResult & Pick + > = {} const validateHeaders = (req: RestRequest) => { if (args.requiredHeaders) { @@ -55,12 +61,13 @@ export const mockPrismicMigrationAPI = ( lang: document.lang, type: document.type, uid: document.uid, + data: document.data, } } } else { for (const document of args.existingDocuments) { if ("title" in document) { - documentsDatabase[document.id] = document + documentsDatabase[document.id] = { ...document, data: {} } } else { documentsDatabase[document.id] = { title: args.ctx.mock.value.keyText({ state: "filled" }), @@ -68,6 +75,7 @@ export const mockPrismicMigrationAPI = ( lang: document.lang, type: document.type, uid: document.uid, + data: document.data, } } } @@ -106,7 +114,7 @@ export const mockPrismicMigrationAPI = ( } // Save the document in DB - documentsDatabase[id] = response + documentsDatabase[id] = { ...response, data: body.data } return res(ctx.status(201), ctx.json(response)) }), @@ -138,7 +146,10 @@ export const mockPrismicMigrationAPI = ( } // Update the document in DB - documentsDatabase[req.params.id as string] = response + documentsDatabase[req.params.id as string] = { + ...response, + data: body.data, + } return res(ctx.json(response)) }), diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts new file mode 100644 index 00000000..f6e597b1 --- /dev/null +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -0,0 +1,195 @@ +import type { TestContext } from "vitest" +import { it as _it, describe, vi } from "vitest" + +import { createPagedQueryResponses } from "./createPagedQueryResponses" +import { createTestWriteClient } from "./createWriteClient" +import { mockPrismicAssetAPI } from "./mockPrismicAssetAPI" +import { mockPrismicMigrationAPI } from "./mockPrismicMigrationAPI" +import { mockPrismicRestAPIV2 } from "./mockPrismicRestAPIV2" + +import * as prismic from "../../src" +import type { Asset } from "../../src/types/api/asset/asset" + +// Skip test on Node 16 and 18 (File and FormData support) +const isNode16 = process.version.startsWith("v16") +const isNode18 = process.version.startsWith("v18") +const it = _it.skipIf(isNode16 || isNode18) + +type GetDataArgs = { + ctx: TestContext + migration: prismic.Migration + existingAssets: Asset[] + existingDocuments: prismic.PrismicDocument[] + migrationDocuments: (Omit & { + uid: string + })[] +} + +type InternalTestMigrationFieldPatchingArgs = { + getData: (args: GetDataArgs) => prismic.PrismicMigrationDocument["data"] + expectStrictEqual: boolean +} + +const internalTestMigrationFieldPatching = ( + description: string, + args: InternalTestMigrationFieldPatchingArgs, +): void => { + it.concurrent(description, async (ctx) => { + vi.useFakeTimers() + + const client = createTestWriteClient({ ctx }) + + const repository = ctx.mock.api.repository() + repository.languages[0].id = "en-us" + repository.languages[0].is_master = true + + const queryResponse = createPagedQueryResponses({ + ctx, + pages: 1, + pageSize: 1, + }) + + const otherDocument = { + ...ctx.mock.value.document(), + uid: ctx.mock.value.keyText(), + } + const newDocument = ctx.mock.value.document() + + mockPrismicRestAPIV2({ ctx, repositoryResponse: repository, queryResponse }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [1], + }) + const { documentsDatabase } = mockPrismicMigrationAPI({ + ctx, + client, + newDocuments: [otherDocument, newDocument], + }) + + const migration = prismic.createMigration() + + newDocument.data = args.getData({ + ctx, + migration, + existingAssets: assetsDatabase.flat(), + existingDocuments: queryResponse[0].results, + migrationDocuments: [otherDocument], + }) + + migration.createDocument(otherDocument, "other") + migration.createDocument(newDocument, "new") + + // We speed up the internal rate limiter to make these tests run faster (from 4500ms to nearly instant) + const migrationProcess = client.migrate(migration) + await vi.runAllTimersAsync() + await migrationProcess + + const { data } = documentsDatabase[newDocument.id] + + if (args.expectStrictEqual) { + ctx.expect(data).toStrictEqual(newDocument.data) + } else { + ctx.expect(data).toMatchSnapshot() + } + + vi.restoreAllMocks() + }) +} + +type TestMigrationFieldPatchingFactoryCases = Record< + string, + (args: GetDataArgs) => prismic.PrismicMigrationDocument["data"][string] +> + +const testMigrationFieldPatchingFactory = ( + description: string, + cases: TestMigrationFieldPatchingFactoryCases, + args: Omit, +): void => { + describe(description, () => { + for (const name in cases) { + const getFields = (args: GetDataArgs) => { + return { field: cases[name](args) } + } + + describe(name, () => { + internalTestMigrationFieldPatching("static zone", { + getData(args) { + return getFields(args) + }, + ...args, + }) + + internalTestMigrationFieldPatching("group", { + getData(args) { + return { + group: [getFields(args)], + } + }, + ...args, + }) + + internalTestMigrationFieldPatching("slice", { + getData(args) { + return { + slices: [ + { + ...args.ctx.mock.value.slice(), + primary: getFields(args), + items: [getFields(args)], + }, + ], + } + }, + ...args, + }) + + internalTestMigrationFieldPatching("shared slice", { + getData(args) { + return { + slices: [ + { + ...args.ctx.mock.value.sharedSlice(), + primary: { + ...getFields(args), + group: [getFields(args)], + }, + items: [getFields(args)], + }, + ], + } + }, + ...args, + }) + }) + } + }) +} + +export const testMigrationSimpleFieldPatching = ( + description: string, + cases: TestMigrationFieldPatchingFactoryCases, +): void => { + testMigrationFieldPatchingFactory(description, cases, { + expectStrictEqual: true, + }) +} + +export const testMigrationImageFieldPatching = ( + description: string, + cases: TestMigrationFieldPatchingFactoryCases, +): void => { + testMigrationFieldPatchingFactory(description, cases, { + expectStrictEqual: false, + }) +} + +export const testMigrationLinkFieldPatching = ( + description: string, + cases: TestMigrationFieldPatchingFactoryCases, +): void => { + testMigrationFieldPatchingFactory(description, cases, { + expectStrictEqual: false, + }) +} diff --git a/test/client.test.ts b/test/client.test.ts index a3c32c92..d36b0793 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -93,6 +93,10 @@ it("constructor throws if a prismic.io endpoint is given that is not for Rest AP prismic.createClient("https://qwerty.cdn.prismic.io/api/v1", { fetch }) }).toThrowError(prismic.PrismicError) + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => void 0) + expect(() => { prismic.createClient("https://example.com/custom/endpoint", { fetch }) }, "Non-prismic.io endpoints are not checked").not.toThrow() @@ -105,6 +109,8 @@ it("constructor throws if a prismic.io endpoint is given that is not for Rest AP prismic.createClient(prismic.getRepositoryEndpoint("qwerty"), { fetch }) }, "An endpoint created with getRepositoryEndpoint does not throw").not.toThrow() + consoleWarnSpy.mockRestore() + process.env.NODE_ENV = originalNodeEnv }) diff --git a/test/helpers-mapSliceZone.test.ts b/test/helpers-mapSliceZone.test.ts index f7b71b24..f513fdba 100644 --- a/test/helpers-mapSliceZone.test.ts +++ b/test/helpers-mapSliceZone.test.ts @@ -42,8 +42,18 @@ it("maps a Slice Zone", async (ctx) => { }) expect(actual).toStrictEqual([ - { __mapped: true, id: undefined, slice_type: model1.id, foo: "bar" }, - { __mapped: true, id: undefined, slice_type: model2.id, baz: "qux" }, + { + __mapped: true, + id: expect.any(String), + slice_type: model1.id, + foo: "bar", + }, + { + __mapped: true, + id: expect.any(String), + slice_type: model2.id, + baz: "qux", + }, ]) }) @@ -56,8 +66,8 @@ it("supports mapping functions that return undefined", async (ctx) => { }) expect(actual).toStrictEqual([ - { __mapped: true, id: undefined, slice_type: model1.id }, - { __mapped: true, id: undefined, slice_type: model2.id }, + { __mapped: true, id: expect.any(String), slice_type: model1.id }, + { __mapped: true, id: expect.any(String), slice_type: model2.id }, ]) }) @@ -70,8 +80,18 @@ it("supports async mapping functions", async (ctx) => { }) expect(actual).toStrictEqual([ - { __mapped: true, id: undefined, slice_type: model1.id, foo: "bar" }, - { __mapped: true, id: undefined, slice_type: model2.id, baz: "qux" }, + { + __mapped: true, + id: expect.any(String), + slice_type: model1.id, + foo: "bar", + }, + { + __mapped: true, + id: expect.any(String), + slice_type: model2.id, + baz: "qux", + }, ]) }) @@ -156,8 +176,18 @@ it("supports lazy-loaded mapping functions", async (ctx) => { }) expect(actual).toStrictEqual([ - { __mapped: true, id: undefined, slice_type: model1.id, foo: "bar" }, - { __mapped: true, id: undefined, slice_type: model2.id, baz: "qux" }, + { + __mapped: true, + id: expect.any(String), + slice_type: model1.id, + foo: "bar", + }, + { + __mapped: true, + id: expect.any(String), + slice_type: model2.id, + baz: "qux", + }, ]) }) @@ -169,7 +199,7 @@ it("skips Slices without a mapping function", async (ctx) => { }) expect(actual).toStrictEqual([ - { __mapped: true, id: undefined, slice_type: model1.id }, + { __mapped: true, id: expect.any(String), slice_type: model1.id }, sliceZone[1], ]) }) diff --git a/test/writeClient-migrateCreateAssets.test.ts b/test/writeClient-migrateCreateAssets.test.ts index d0756a35..c3417200 100644 --- a/test/writeClient-migrateCreateAssets.test.ts +++ b/test/writeClient-migrateCreateAssets.test.ts @@ -286,7 +286,7 @@ it.concurrent( ctx.server.use( rest.get(asset.url.split("?")[0], (_req, res, ctx) => { - return res(ctx.set("content-type", ""), ctx.text("foo")) + return res(ctx.text("foo")) }), ) @@ -312,6 +312,52 @@ it.concurrent( }, ) +it.concurrent("creates new asset with metadata", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const assetMetadata = { + notes: "foo", + alt: "bar", + credits: "baz", + } + const asset = mockAsset(ctx, assetMetadata) + const dummyFileData = "foo" + + const existingTags = [ + { + id: "00000000-4444-4444-4444-121212121212", + name: "qux", + created_at: 0, + last_modified: 0, + }, + ] + + mockPrismicRestAPIV2({ ctx }) + const { assetsDatabase, tagsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingTags, + newAssets: [asset], + }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() + migration.createAsset(dummyFileData, asset.filename, { + ...assetMetadata, + tags: ["qux", "quux"], + }) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + const newAsset = assetsDatabase[0][0] + expect(newAsset.notes).toBe(assetMetadata.notes) + expect(newAsset.alt).toBe(assetMetadata.alt) + expect(newAsset.credits).toBe(assetMetadata.credits) + expect(newAsset.tags).toStrictEqual(tagsDatabase) +}) + it.concurrent( "throws on fetch error when creating a new asset from a file URL", async (ctx) => { diff --git a/test/writeClient-migrateCreateDocuments.test.ts b/test/writeClient-migrateCreateDocuments.test.ts index 3220a8de..f5507c2e 100644 --- a/test/writeClient-migrateCreateDocuments.test.ts +++ b/test/writeClient-migrateCreateDocuments.test.ts @@ -140,11 +140,13 @@ it.concurrent("creates new documents", async (ctx) => { migration.createDocument(document, "foo") let documents: DocumentMap | undefined - const reporter = vi.fn((event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }) + const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( + (event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }, + ) await client.migrate(migration, { reporter }) @@ -184,11 +186,13 @@ it.concurrent( migration.createDocument(document, "foo", { masterLanguageDocument }) let documents: DocumentMap | undefined - const reporter = vi.fn((event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }) + const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( + (event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }, + ) await client.migrate(migration, { reporter }) @@ -239,11 +243,13 @@ it.concurrent( }) let documents: DocumentMap | undefined - const reporter = vi.fn((event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }) + const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( + (event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }, + ) await client.migrate(migration, { reporter }) @@ -299,11 +305,13 @@ it.concurrent( }) let documents: DocumentMap | undefined - const reporter = vi.fn((event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }) + const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( + (event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }, + ) await client.migrate(migration, { reporter }) @@ -346,11 +354,13 @@ it.concurrent( migration.createDocument(document, "foo") let documents: DocumentMap | undefined - const reporter = vi.fn((event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }) + const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( + (event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }, + ) await client.migrate(migration, { reporter }) @@ -398,11 +408,13 @@ it.concurrent("creates master locale documents first", async (ctx) => { migration.createDocument(masterLanguageDocument, "foo") let documents: DocumentMap | undefined - const reporter = vi.fn((event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }) + const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( + (event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }, + ) await client.migrate(migration, { reporter }) diff --git a/test/writeClient-migrateUpdateDocuments.test.ts b/test/writeClient-migrateUpdateDocuments.test.ts new file mode 100644 index 00000000..7446708a --- /dev/null +++ b/test/writeClient-migrateUpdateDocuments.test.ts @@ -0,0 +1,46 @@ +import { + testMigrationImageFieldPatching, + testMigrationLinkFieldPatching, + testMigrationSimpleFieldPatching, +} from "./__testutils__/testMigrationFieldPatching" + +testMigrationSimpleFieldPatching("does not patch simple fields", { + embed: ({ ctx }) => ctx.mock.value.embed({ state: "filled" }), + date: ({ ctx }) => ctx.mock.value.date({ state: "filled" }), + timestamp: ({ ctx }) => ctx.mock.value.timestamp({ state: "filled" }), + color: ({ ctx }) => ctx.mock.value.color({ state: "filled" }), + number: ({ ctx }) => ctx.mock.value.number({ state: "filled" }), + keyText: ({ ctx }) => ctx.mock.value.keyText({ state: "filled" }), + select: ({ ctx }) => + ctx.mock.value.select({ + model: ctx.mock.model.select({ options: ["foo", "bar"] }), + state: "filled", + }), + boolean: ({ ctx }) => ctx.mock.value.boolean(), + geoPoint: ({ ctx }) => ctx.mock.value.geoPoint({ state: "filled" }), + integration: ({ ctx }) => ctx.mock.value.integration({ state: "filled" }), + linkToWeb: ({ ctx }) => + ctx.mock.value.link({ type: "Web", withTargetBlank: true }), +}) + +testMigrationImageFieldPatching("patches image fields", { + regular: ({ migration }) => migration.createAsset("foo", "foo.png"), + richText: ({ ctx, migration }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + migration.createAsset("foo", "foo.png"), + ], +}) + +testMigrationLinkFieldPatching("patches link fields", { + existingDocument: ({ existingDocuments }) => existingDocuments[0], + migrationDocument: + ({ migration, migrationDocuments }) => + () => + migration.getByUID(migrationDocuments[0].type, migrationDocuments[0].uid), + otherRepositoryContentRelationship: ({ ctx, existingDocuments }) => { + const contentRelationship = ctx.mock.value.link({ type: "Document" }) + contentRelationship.id = existingDocuments[0].id + + return contentRelationship + }, +}) From 580f2f131e63eeddef479f446dd45330fd3497b8 Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 6 Sep 2024 12:05:25 +0200 Subject: [PATCH 32/61] test: `WriteClient.migrateUpdateDocuments` --- package-lock.json | 8 +- package.json | 2 +- src/lib/isMigrationField.ts | 9 +- ...migrateUpdateDocuments-images.test.ts.snap | 1800 +++++++++++++++++ ...t-migrateUpdateDocuments-link.test.ts.snap | 1441 +++++++++++++ ...teUpdateDocuments-linkToMedia.test.ts.snap | 1324 ++++++++++++ ...Client-migrateUpdateDocuments.test.ts.snap | 763 ------- test/__testutils__/mockPrismicAssetAPI.ts | 2 +- .../testMigrationFieldPatching.ts | 34 +- ...ient-migrateUpdateDocuments-images.test.ts | 65 + ...Client-migrateUpdateDocuments-link.test.ts | 61 + ...migrateUpdateDocuments-linkToMedia.test.ts | 81 + ...igrateUpdateDocuments-simpleFields.test.ts | 22 + ...writeClient-migrateUpdateDocuments.test.ts | 46 - 14 files changed, 4821 insertions(+), 837 deletions(-) create mode 100644 test/__snapshots__/writeClient-migrateUpdateDocuments-images.test.ts.snap create mode 100644 test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap create mode 100644 test/__snapshots__/writeClient-migrateUpdateDocuments-linkToMedia.test.ts.snap delete mode 100644 test/__snapshots__/writeClient-migrateUpdateDocuments.test.ts.snap create mode 100644 test/writeClient-migrateUpdateDocuments-images.test.ts create mode 100644 test/writeClient-migrateUpdateDocuments-link.test.ts create mode 100644 test/writeClient-migrateUpdateDocuments-linkToMedia.test.ts create mode 100644 test/writeClient-migrateUpdateDocuments-simpleFields.test.ts delete mode 100644 test/writeClient-migrateUpdateDocuments.test.ts diff --git a/package-lock.json b/package-lock.json index ed54e589..45c72788 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "imgix-url-builder": "^0.0.5" }, "devDependencies": { - "@prismicio/mock": "^0.3.8", + "@prismicio/mock": "^0.3.9", "@prismicio/types-internal": "^2.6.0", "@size-limit/preset-small-lib": "^11.1.4", "@trivago/prettier-plugin-sort-imports": "^4.3.0", @@ -1212,9 +1212,9 @@ } }, "node_modules/@prismicio/mock": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@prismicio/mock/-/mock-0.3.8.tgz", - "integrity": "sha512-kc7MrxxyD2uzl8kAzc51CNHPrJTfKDxtJ0FLO0AcoODQUnd12JxumLvNY+O6ydbH6Zg8A56FGt22SH3yIxtcAQ==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@prismicio/mock/-/mock-0.3.9.tgz", + "integrity": "sha512-gZPuacdWJsNZmI+EQx2NB5c7cqUUK+3vht/Uu9OjbRILOmzoZZ5crArhudoLO1POf+x1DNx3PA5BGO2UtyizoA==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index d4f49a80..88236c34 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "imgix-url-builder": "^0.0.5" }, "devDependencies": { - "@prismicio/mock": "^0.3.8", + "@prismicio/mock": "^0.3.9", "@prismicio/types-internal": "^2.6.0", "@size-limit/preset-small-lib": "^11.1.4", "@trivago/prettier-plugin-sort-imports": "^4.3.0", diff --git a/src/lib/isMigrationField.ts b/src/lib/isMigrationField.ts index 9219a4ab..a10174c2 100644 --- a/src/lib/isMigrationField.ts +++ b/src/lib/isMigrationField.ts @@ -17,8 +17,6 @@ import type { RichTextField } from "../types/value/richText" import type { SliceZone } from "../types/value/sliceZone" import type { AnyRegularField } from "../types/value/types" -import * as isFilled from "../helpers/isFilled" - /** * Checks if a field is a slice zone. * @@ -149,12 +147,7 @@ export const image = ( ) { // Migration image field return true - } else if ( - "id" in field && - "dimensions" in field && - field.dimensions && - isFilled.image(field) - ) { + } else if ("id" in field && "url" in field && "dimensions" in field) { // Regular image field return true } diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-images.test.ts.snap b/test/__snapshots__/writeClient-migrateUpdateDocuments-images.test.ts.snap new file mode 100644 index 00000000..daa3e7f0 --- /dev/null +++ b/test/__snapshots__/writeClient-migrateUpdateDocuments-images.test.ts.snap @@ -0,0 +1,1800 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`patches image fields > existing > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + }, + }, + ], +} +`; + +exports[`patches image fields > existing > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > existing > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > existing > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + }, +} +`; + +exports[`patches image fields > new > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "ef95d5daa4d", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", + }, + }, + ], +} +`; + +exports[`patches image fields > new > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > new > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "45306297c5e", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "45306297c5e", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > new > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "c5c95f8d3ac", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, +} +`; + +exports[`patches image fields > otherRepository > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + }, + }, + ], +} +`; + +exports[`patches image fields > otherRepository > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + }, + }, + ], + "primary": { + "field": { + "alt": "Ut consequat semper viverra nam libero justo laoreet", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + }, + "group": [ + { + "field": { + "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > otherRepository > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c", + }, + }, + ], + "primary": { + "field": { + "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > otherRepository > static zone 1`] = ` +{ + "field": { + "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f", + }, +} +`; + +exports[`patches image fields > otherRepositoryEmpty > group 1`] = ` +{ + "group": [ + { + "field": {}, + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryEmpty > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": {}, + }, + ], + "primary": { + "field": {}, + "group": [ + { + "field": {}, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryEmpty > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": {}, + }, + ], + "primary": { + "field": {}, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryEmpty > static zone 1`] = ` +{ + "field": {}, +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnails > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "square": { + "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", + }, + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": "Ut consequat semper viverra nam libero justo laoreet", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": "Ut consequat semper viverra nam libero justo laoreet", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + "group": [ + { + "field": { + "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "square": { + "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "square": { + "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnails > static zone 1`] = ` +{ + "field": { + "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "square": { + "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + }, +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", + }, + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + }, +} +`; + +exports[`patches image fields > richTextExisting > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches image fields > richTextExisting > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > richTextExisting > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > richTextExisting > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + }, + ], +} +`; + +exports[`patches image fields > richTextNew > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "cdfdd322ca9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches image fields > richTextNew > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > richTextNew > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "7963dc361cc", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "7963dc361cc", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > richTextNew > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "0bbad670dad", + "type": "image", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + }, + ], +} +`; + +exports[`patches image fields > richTextOtherRepository > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#7cfc26", + "x": 2977, + "y": -1163, + "zoom": 1.0472585898934068, + }, + "id": "e8d0985c099", + "type": "image", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + }, + ], + }, + ], +} +`; + +exports[`patches image fields > richTextOtherRepository > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Facilisis", + "type": "heading3", + }, + { + "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#37ae3f", + "x": -1505, + "y": 902, + "zoom": 1.8328975606320652, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": "Proin libero nunc consequat interdum varius", + "copyright": null, + "dimensions": { + "height": 3036, + "width": 4554, + }, + "edit": { + "background": "#904f4f", + "x": 1462, + "y": 1324, + "zoom": 1.504938844941775, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Egestas Integer Eget Aliquet Nibh", + "type": "heading5", + }, + { + "alt": "Arcu cursus vitae congue mauris", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#5f7a9b", + "x": -119, + "y": -2667, + "zoom": 1.9681315715350518, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > richTextOtherRepository > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Amet", + "type": "heading5", + }, + { + "alt": "Auctor neque vitae tempus quam", + "copyright": null, + "dimensions": { + "height": 1440, + "width": 2560, + }, + "edit": { + "background": "#5bc5aa", + "x": -1072, + "y": -281, + "zoom": 1.3767766101231744, + }, + "id": "f70ca27104d", + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", + "copyright": null, + "dimensions": { + "height": 3036, + "width": 4554, + }, + "edit": { + "background": "#4860cb", + "x": 280, + "y": -379, + "zoom": 1.2389796902982004, + }, + "id": "f70ca27104d", + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > richTextOtherRepository > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": "Interdum velit euismod in pellentesque", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#ab1d17", + "x": 3605, + "y": 860, + "zoom": 1.9465488211593005, + }, + "id": "04a95cc61c3", + "type": "image", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + }, + ], +} +`; diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap b/test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap new file mode 100644 index 00000000..9bef61ca --- /dev/null +++ b/test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap @@ -0,0 +1,1441 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`patches link fields > brokenLink > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "adcdf90", + "isBroken": true, + "lang": "nulla", + "link_type": "Document", + "tags": [ + "Convallis Posuere", + ], + "type": "vitae", + "url": "https://ac.example", + }, + }, + ], +} +`; + +exports[`patches link fields > brokenLink > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "9be9130", + "isBroken": true, + "lang": "faucibus", + "link_type": "Document", + "tags": [ + "Ultrices", + ], + "type": "egestas_integer", + "url": "https://scelerisque.example", + }, + }, + ], + "primary": { + "field": { + "id": "06ad9e3", + "isBroken": true, + "lang": "fringilla", + "link_type": "Document", + "tags": [], + "type": "amet", + "url": "https://nec.example", + }, + "group": [ + { + "field": { + "id": "bd0255b", + "isBroken": true, + "lang": "neque", + "link_type": "Document", + "tags": [], + "type": "lacus", + "url": "https://cras.example", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > brokenLink > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "5ec2f4d", + "isBroken": true, + "lang": "enim,", + "link_type": "Document", + "tags": [], + "type": "felis_donec", + "url": "https://sem.example", + }, + }, + ], + "primary": { + "field": { + "id": "31a1bd2", + "isBroken": true, + "lang": "ac", + "link_type": "Document", + "tags": [], + "type": "enim_praesent", + "url": "https://sit.example", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > brokenLink > static zone 1`] = ` +{ + "field": { + "id": "2adafa9", + "isBroken": true, + "lang": "elementum", + "link_type": "Document", + "tags": [], + "type": "aenean", + "url": "https://volutpat.example", + }, +} +`; + +exports[`patches link fields > existing > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "laoreet", + "link_type": "Document", + "tags": [], + "type": "orci", + }, + }, + ], +} +`; + +exports[`patches link fields > existing > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + }, + ], + "primary": { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "group": [ + { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > existing > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + }, + ], + "primary": { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > existing > static zone 1`] = ` +{ + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "eget", + "link_type": "Document", + "tags": [], + "type": "phasellus", + }, +} +`; + +exports[`patches link fields > migration > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "a", + "link_type": "Document", + "tags": [ + "Odio Eu", + ], + "type": "amet", + "uid": "Non sodales neque", + }, + }, + ], +} +`; + +exports[`patches link fields > migration > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + }, + ], + "primary": { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + "group": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > migration > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "ultrices", + "link_type": "Document", + "tags": [ + "Ipsum Consequat", + ], + "type": "eget", + "uid": "Amet dictum sit", + }, + }, + ], + "primary": { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "ultrices", + "link_type": "Document", + "tags": [ + "Ipsum Consequat", + ], + "type": "eget", + "uid": "Amet dictum sit", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > migration > static zone 1`] = ` +{ + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "nisi,", + "link_type": "Document", + "tags": [], + "type": "eros", + "uid": "Scelerisque mauris pellentesque", + }, +} +`; + +exports[`patches link fields > migrationNoTags > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "a", + "link_type": "Document", + "tags": [], + "type": "amet", + "uid": "Non sodales neque", + }, + }, + ], +} +`; + +exports[`patches link fields > migrationNoTags > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + }, + ], + "primary": { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + "group": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > migrationNoTags > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "ultrices", + "link_type": "Document", + "tags": [], + "type": "eget", + "uid": "Amet dictum sit", + }, + }, + ], + "primary": { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "ultrices", + "link_type": "Document", + "tags": [], + "type": "eget", + "uid": "Amet dictum sit", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > migrationNoTags > static zone 1`] = ` +{ + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "nisi,", + "link_type": "Document", + "tags": [], + "type": "eros", + "uid": "Scelerisque mauris pellentesque", + }, +} +`; + +exports[`patches link fields > otherRepositoryContentRelationship > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "nulla", + "link_type": "Document", + "tags": [ + "Convallis Posuere", + ], + "type": "vitae", + "url": "https://ac.example", + }, + }, + ], +} +`; + +exports[`patches link fields > otherRepositoryContentRelationship > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "faucibus", + "link_type": "Document", + "tags": [ + "Ultrices", + ], + "type": "egestas_integer", + "url": "https://scelerisque.example", + }, + }, + ], + "primary": { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "fringilla", + "link_type": "Document", + "tags": [], + "type": "amet", + "url": "https://nec.example", + }, + "group": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "neque", + "link_type": "Document", + "tags": [], + "type": "lacus", + "url": "https://cras.example", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "enim,", + "link_type": "Document", + "tags": [], + "type": "felis_donec", + "url": "https://sem.example", + }, + }, + ], + "primary": { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "ac", + "link_type": "Document", + "tags": [], + "type": "enim_praesent", + "url": "https://sit.example", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > otherRepositoryContentRelationship > static zone 1`] = ` +{ + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "elementum", + "link_type": "Document", + "tags": [], + "type": "aenean", + "url": "https://volutpat.example", + }, +} +`; + +exports[`patches link fields > richTextLinkNode > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "id": "id-existing", + "isBroken": false, + "lang": "laoreet", + "link_type": "Document", + "tags": [], + "type": "orci", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], +} +`; + +exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > richTextLinkNode > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > richTextLinkNode > static zone 1`] = ` +{ + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "id": "id-existing", + "isBroken": false, + "lang": "eget", + "link_type": "Document", + "tags": [], + "type": "phasellus", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], +} +`; + +exports[`patches link fields > richTextNewImageNodeLink > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "cdfdd322ca9", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "laoreet", + "link_type": "Document", + "tags": [], + "type": "orci", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches link fields > richTextNewImageNodeLink > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > richTextNewImageNodeLink > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "7963dc361cc", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "7963dc361cc", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > richTextNewImageNodeLink > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "0bbad670dad", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "eget", + "link_type": "Document", + "tags": [], + "type": "phasellus", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + }, + ], +} +`; + +exports[`patches link fields > richTextOtherReposiotryImageNodeLink > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#7cfc26", + "x": 2977, + "y": -1163, + "zoom": 1.0472585898934068, + }, + "id": "e8d0985c099", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "laoreet", + "link_type": "Document", + "tags": [], + "type": "orci", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + }, + ], + }, + ], +} +`; + +exports[`patches link fields > richTextOtherReposiotryImageNodeLink > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Facilisis", + "type": "heading3", + }, + { + "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#37ae3f", + "x": -1505, + "y": 902, + "zoom": 1.8328975606320652, + }, + "id": "3fc0dfa9fe9", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": "Proin libero nunc consequat interdum varius", + "copyright": null, + "dimensions": { + "height": 3036, + "width": 4554, + }, + "edit": { + "background": "#904f4f", + "x": 1462, + "y": 1324, + "zoom": 1.504938844941775, + }, + "id": "3fc0dfa9fe9", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Egestas Integer Eget Aliquet Nibh", + "type": "heading5", + }, + { + "alt": "Arcu cursus vitae congue mauris", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#5f7a9b", + "x": -119, + "y": -2667, + "zoom": 1.9681315715350518, + }, + "id": "3fc0dfa9fe9", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > richTextOtherReposiotryImageNodeLink > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Amet", + "type": "heading5", + }, + { + "alt": "Auctor neque vitae tempus quam", + "copyright": null, + "dimensions": { + "height": 1440, + "width": 2560, + }, + "edit": { + "background": "#5bc5aa", + "x": -1072, + "y": -281, + "zoom": 1.3767766101231744, + }, + "id": "f70ca27104d", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", + "copyright": null, + "dimensions": { + "height": 3036, + "width": 4554, + }, + "edit": { + "background": "#4860cb", + "x": 280, + "y": -379, + "zoom": 1.2389796902982004, + }, + "id": "f70ca27104d", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > richTextOtherReposiotryImageNodeLink > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": "Interdum velit euismod in pellentesque", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#ab1d17", + "x": 3605, + "y": 860, + "zoom": 1.9465488211593005, + }, + "id": "04a95cc61c3", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "eget", + "link_type": "Document", + "tags": [], + "type": "phasellus", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + }, + ], +} +`; diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-linkToMedia.test.ts.snap b/test/__snapshots__/writeClient-migrateUpdateDocuments-linkToMedia.test.ts.snap new file mode 100644 index 00000000..7abdb040 --- /dev/null +++ b/test/__snapshots__/writeClient-migrateUpdateDocuments-linkToMedia.test.ts.snap @@ -0,0 +1,1324 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`patches link to media fields > existing > group 1`] = ` +{ + "group": [ + { + "field": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "width": "1", + }, + }, + ], +} +`; + +exports[`patches link to media fields > existing > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + }, + ], + "primary": { + "field": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + "group": [ + { + "field": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link to media fields > existing > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + }, + ], + "primary": { + "field": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link to media fields > existing > static zone 1`] = ` +{ + "field": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "width": "1", + }, +} +`; + +exports[`patches link to media fields > existingNonImage > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "id-existing", + "kind": "document", + "link_type": "Media", + "name": "foo.pdf", + "size": "1", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + }, + }, + ], +} +`; + +exports[`patches link to media fields > existingNonImage > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "id-existing", + "kind": "document", + "link_type": "Media", + "name": "foo.pdf", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "id": "id-existing", + "kind": "document", + "link_type": "Media", + "name": "foo.pdf", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + "group": [ + { + "field": { + "id": "id-existing", + "kind": "document", + "link_type": "Media", + "name": "foo.pdf", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link to media fields > existingNonImage > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "id-existing", + "kind": "document", + "link_type": "Media", + "name": "foo.pdf", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "id": "id-existing", + "kind": "document", + "link_type": "Media", + "name": "foo.pdf", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link to media fields > existingNonImage > static zone 1`] = ` +{ + "field": { + "id": "id-existing", + "kind": "document", + "link_type": "Media", + "name": "foo.pdf", + "size": "1", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + }, +} +`; + +exports[`patches link to media fields > new > group 1`] = ` +{ + "group": [ + { + "field": { + "height": "1", + "id": "ef95d5daa4d", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", + "width": "1", + }, + }, + ], +} +`; + +exports[`patches link to media fields > new > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "height": "1", + "id": "f2c3f8bfc90", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + }, + ], + "primary": { + "field": { + "height": "1", + "id": "f2c3f8bfc90", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + "group": [ + { + "field": { + "height": "1", + "id": "f2c3f8bfc90", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link to media fields > new > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "height": "1", + "id": "45306297c5e", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + }, + ], + "primary": { + "field": { + "height": "1", + "id": "45306297c5e", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link to media fields > new > static zone 1`] = ` +{ + "field": { + "height": "1", + "id": "c5c95f8d3ac", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, +} +`; + +exports[`patches link to media fields > otherRepository > group 1`] = ` +{ + "group": [ + { + "field": { + "height": "1715", + "id": "fdb33894dcf", + "kind": "purus", + "link_type": "Media", + "name": "nulla.example", + "size": "2039", + "url": "https://db0f6f37ebeb6ea09489124345af2a45.example.com/foo.png", + "width": "2091", + }, + }, + ], +} +`; + +exports[`patches link to media fields > otherRepository > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "height": "2741", + "id": "ba97bfb16bf", + "kind": "quis", + "link_type": "Media", + "name": "senectus.example", + "size": "844", + "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", + "width": "749", + }, + }, + ], + "primary": { + "field": { + "height": "1276", + "id": "ba97bfb16bf", + "kind": "ullamcorper", + "link_type": "Media", + "name": "fringilla.example", + "size": "818", + "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", + "width": "2024", + }, + "group": [ + { + "field": { + "height": "2964", + "id": "ba97bfb16bf", + "kind": "pellentesque", + "link_type": "Media", + "name": "cras.example", + "size": "1564", + "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", + "width": "599", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link to media fields > otherRepository > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "height": "1500", + "id": "a57963dc361", + "kind": "quam", + "link_type": "Media", + "name": "integer.example", + "size": "1043", + "url": "https://6d52012dca4fc77aa554f25430aef501.example.com/foo.png", + "width": "737", + }, + }, + ], + "primary": { + "field": { + "height": "761", + "id": "a57963dc361", + "kind": "nibh", + "link_type": "Media", + "name": "ac.example", + "size": "2757", + "url": "https://6d52012dca4fc77aa554f25430aef501.example.com/foo.png", + "width": "1300", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link to media fields > otherRepository > static zone 1`] = ` +{ + "field": { + "height": "740", + "id": "97acc6e9abf", + "kind": "suspendisse", + "link_type": "Media", + "name": "elementum.example", + "size": "556", + "url": "https://7713b5e0c356963c79ccf86c6ff1710c.example.com/foo.png", + "width": "2883", + }, +} +`; + +exports[`patches link to media fields > otherRepositoryNotFoundID > group 1`] = ` +{ + "group": [ + { + "field": { + "link_type": "Any", + }, + }, + ], +} +`; + +exports[`patches link to media fields > otherRepositoryNotFoundID > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "link_type": "Any", + }, + }, + ], + "primary": { + "field": { + "link_type": "Any", + }, + "group": [ + { + "field": { + "link_type": "Any", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link to media fields > otherRepositoryNotFoundID > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "link_type": "Any", + }, + }, + ], + "primary": { + "field": { + "link_type": "Any", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link to media fields > otherRepositoryNotFoundID > static zone 1`] = ` +{ + "field": { + "link_type": "Any", + }, +} +`; + +exports[`patches link to media fields > richTextExisting > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], +} +`; + +exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link to media fields > richTextExisting > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link to media fields > richTextExisting > static zone 1`] = ` +{ + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "id-existing", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], +} +`; + +exports[`patches link to media fields > richTextNew > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "ef95d5daa4d", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], +} +`; + +exports[`patches link to media fields > richTextNew > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "f2c3f8bfc90", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "f2c3f8bfc90", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "f2c3f8bfc90", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link to media fields > richTextNew > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "45306297c5e", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "45306297c5e", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link to media fields > richTextNew > static zone 1`] = ` +{ + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "c5c95f8d3ac", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], +} +`; + +exports[`patches link to media fields > richTextOtherRepository > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1715", + "id": "fdb33894dcf", + "kind": "purus", + "link_type": "Media", + "name": "nulla.example", + "size": "2039", + "url": "https://db0f6f37ebeb6ea09489124345af2a45.example.com/foo.png", + "width": "2091", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], +} +`; + +exports[`patches link to media fields > richTextOtherRepository > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "2741", + "id": "ba97bfb16bf", + "kind": "quis", + "link_type": "Media", + "name": "senectus.example", + "size": "844", + "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", + "width": "749", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1276", + "id": "ba97bfb16bf", + "kind": "ullamcorper", + "link_type": "Media", + "name": "fringilla.example", + "size": "818", + "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", + "width": "2024", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "2964", + "id": "ba97bfb16bf", + "kind": "pellentesque", + "link_type": "Media", + "name": "cras.example", + "size": "1564", + "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", + "width": "599", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link to media fields > richTextOtherRepository > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1500", + "id": "a57963dc361", + "kind": "quam", + "link_type": "Media", + "name": "integer.example", + "size": "1043", + "url": "https://6d52012dca4fc77aa554f25430aef501.example.com/foo.png", + "width": "737", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "761", + "id": "a57963dc361", + "kind": "nibh", + "link_type": "Media", + "name": "ac.example", + "size": "2757", + "url": "https://6d52012dca4fc77aa554f25430aef501.example.com/foo.png", + "width": "1300", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link to media fields > richTextOtherRepository > static zone 1`] = ` +{ + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "740", + "id": "97acc6e9abf", + "kind": "suspendisse", + "link_type": "Media", + "name": "elementum.example", + "size": "556", + "url": "https://7713b5e0c356963c79ccf86c6ff1710c.example.com/foo.png", + "width": "2883", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], +} +`; diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments.test.ts.snap b/test/__snapshots__/writeClient-migrateUpdateDocuments.test.ts.snap deleted file mode 100644 index bdf2af8c..00000000 --- a/test/__snapshots__/writeClient-migrateUpdateDocuments.test.ts.snap +++ /dev/null @@ -1,763 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`patches image fields > regular > group 1`] = ` -{ - "group": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "ef95d5daa4d", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", - }, - }, - ], -} -`; - -exports[`patches image fields > regular > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - }, - ], - "primary": { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - "group": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > regular > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "45306297c5e", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - }, - ], - "primary": { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "45306297c5e", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > regular > static zone 1`] = ` -{ - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "c5c95f8d3ac", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, -} -`; - -exports[`patches image fields > richText > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "cdfdd322ca9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", - }, - ], - }, - ], -} -`; - -exports[`patches image fields > richText > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Eu Mi", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - "group": [ - { - "field": [ - { - "spans": [], - "text": "Arcu", - "type": "heading1", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > richText > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "7963dc361cc", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "7963dc361cc", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > richText > static zone 1`] = ` -{ - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "0bbad670dad", - "type": "image", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - }, - ], -} -`; - -exports[`patches link fields > existingDocument > group 1`] = ` -{ - "group": [ - { - "field": { - "id": "058058c", - "isBroken": false, - "lang": "laoreet", - "link_type": "Document", - "tags": [], - "type": "orci", - }, - }, - ], -} -`; - -exports[`patches link fields > existingDocument > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "id": "0d137ed", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - }, - ], - "primary": { - "field": { - "id": "0d137ed", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - "group": [ - { - "field": { - "id": "0d137ed", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches link fields > existingDocument > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "id": "382a11c", - "isBroken": false, - "lang": "sapien", - "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", - }, - }, - ], - "primary": { - "field": { - "id": "382a11c", - "isBroken": false, - "lang": "sapien", - "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches link fields > existingDocument > static zone 1`] = ` -{ - "field": { - "id": "edeaf3a", - "isBroken": false, - "lang": "eget", - "link_type": "Document", - "tags": [], - "type": "phasellus", - }, -} -`; - -exports[`patches link fields > migrationDocument > group 1`] = ` -{ - "group": [ - { - "field": { - "id": "9f3857a", - "isBroken": false, - "lang": "a", - "link_type": "Document", - "tags": [ - "Odio Eu", - ], - "type": "amet", - "uid": "Non sodales neque", - }, - }, - ], -} -`; - -exports[`patches link fields > migrationDocument > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "id": "aa66b08", - "isBroken": false, - "lang": "gravida", - "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", - }, - }, - ], - "primary": { - "field": { - "id": "aa66b08", - "isBroken": false, - "lang": "gravida", - "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", - }, - "group": [ - { - "field": { - "id": "aa66b08", - "isBroken": false, - "lang": "gravida", - "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches link fields > migrationDocument > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "id": "dfcf6a9", - "isBroken": false, - "lang": "ultrices", - "link_type": "Document", - "tags": [ - "Ipsum Consequat", - ], - "type": "eget", - "uid": "Amet dictum sit", - }, - }, - ], - "primary": { - "field": { - "id": "dfcf6a9", - "isBroken": false, - "lang": "ultrices", - "link_type": "Document", - "tags": [ - "Ipsum Consequat", - ], - "type": "eget", - "uid": "Amet dictum sit", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches link fields > migrationDocument > static zone 1`] = ` -{ - "field": { - "id": "3f9fce9", - "isBroken": false, - "lang": "nisi,", - "link_type": "Document", - "tags": [], - "type": "eros", - "uid": "Scelerisque mauris pellentesque", - }, -} -`; - -exports[`patches link fields > otherRepositoryContentRelationship > group 1`] = ` -{ - "group": [ - { - "field": { - "id": "058058c", - "isBroken": false, - "lang": "nulla", - "link_type": "Document", - "tags": [ - "Convallis Posuere", - ], - "type": "vitae", - "url": "https://ac.example", - }, - }, - ], -} -`; - -exports[`patches link fields > otherRepositoryContentRelationship > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "id": "0d137ed", - "isBroken": false, - "lang": "faucibus", - "link_type": "Document", - "tags": [ - "Ultrices", - ], - "type": "egestas_integer", - "url": "https://scelerisque.example", - }, - }, - ], - "primary": { - "field": { - "id": "0d137ed", - "isBroken": false, - "lang": "fringilla", - "link_type": "Document", - "tags": [], - "type": "amet", - "url": "https://nec.example", - }, - "group": [ - { - "field": { - "id": "0d137ed", - "isBroken": false, - "lang": "neque", - "link_type": "Document", - "tags": [], - "type": "lacus", - "url": "https://cras.example", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "id": "382a11c", - "isBroken": false, - "lang": "enim,", - "link_type": "Document", - "tags": [], - "type": "felis_donec", - "url": "https://sem.example", - }, - }, - ], - "primary": { - "field": { - "id": "382a11c", - "isBroken": false, - "lang": "ac", - "link_type": "Document", - "tags": [], - "type": "enim_praesent", - "url": "https://sit.example", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches link fields > otherRepositoryContentRelationship > static zone 1`] = ` -{ - "field": { - "id": "edeaf3a", - "isBroken": false, - "lang": "elementum", - "link_type": "Document", - "tags": [], - "type": "aenean", - "url": "https://volutpat.example", - }, -} -`; diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index e975c056..4d4a144a 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -61,9 +61,9 @@ export const mockAsset = (ctx: TestContext, assets?: Partial): Asset => { return { ...DEFAULT_ASSET, - ...assets, id, url, + ...assets, } } diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index f6e597b1..2495629e 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -1,9 +1,11 @@ import type { TestContext } from "vitest" import { it as _it, describe, vi } from "vitest" +import { rest } from "msw" + import { createPagedQueryResponses } from "./createPagedQueryResponses" import { createTestWriteClient } from "./createWriteClient" -import { mockPrismicAssetAPI } from "./mockPrismicAssetAPI" +import { mockAsset, mockPrismicAssetAPI } from "./mockPrismicAssetAPI" import { mockPrismicMigrationAPI } from "./mockPrismicMigrationAPI" import { mockPrismicRestAPIV2 } from "./mockPrismicRestAPIV2" @@ -23,6 +25,7 @@ type GetDataArgs = { migrationDocuments: (Omit & { uid: string })[] + mockedDomain: string } type InternalTestMigrationFieldPatchingArgs = { @@ -48,6 +51,7 @@ const internalTestMigrationFieldPatching = ( pages: 1, pageSize: 1, }) + queryResponse[0].results[0].id = "id-existing" const otherDocument = { ...ctx.mock.value.document(), @@ -55,18 +59,27 @@ const internalTestMigrationFieldPatching = ( } const newDocument = ctx.mock.value.document() + const newID = "id-new" + mockPrismicRestAPIV2({ ctx, repositoryResponse: repository, queryResponse }) const { assetsDatabase } = mockPrismicAssetAPI({ ctx, client, - existingAssets: [1], + existingAssets: [[mockAsset(ctx, { id: "id-existing" })]], }) const { documentsDatabase } = mockPrismicMigrationAPI({ ctx, client, - newDocuments: [otherDocument, newDocument], + newDocuments: [{ id: "id-migration" }, { id: newID }], }) + const mockedDomain = `https://${client.repositoryName}.example.com` + ctx.server.use( + rest.get(`${mockedDomain}/:path`, (__testutils__req, res, ctx) => + res(ctx.text("foo")), + ), + ) + const migration = prismic.createMigration() newDocument.data = args.getData({ @@ -75,6 +88,7 @@ const internalTestMigrationFieldPatching = ( existingAssets: assetsDatabase.flat(), existingDocuments: queryResponse[0].results, migrationDocuments: [otherDocument], + mockedDomain, }) migration.createDocument(otherDocument, "other") @@ -85,7 +99,7 @@ const internalTestMigrationFieldPatching = ( await vi.runAllTimersAsync() await migrationProcess - const { data } = documentsDatabase[newDocument.id] + const { data } = documentsDatabase[newID] if (args.expectStrictEqual) { ctx.expect(data).toStrictEqual(newDocument.data) @@ -93,6 +107,7 @@ const internalTestMigrationFieldPatching = ( ctx.expect(data).toMatchSnapshot() } + vi.useRealTimers() vi.restoreAllMocks() }) } @@ -176,16 +191,7 @@ export const testMigrationSimpleFieldPatching = ( }) } -export const testMigrationImageFieldPatching = ( - description: string, - cases: TestMigrationFieldPatchingFactoryCases, -): void => { - testMigrationFieldPatchingFactory(description, cases, { - expectStrictEqual: false, - }) -} - -export const testMigrationLinkFieldPatching = ( +export const testMigrationFieldPatching = ( description: string, cases: TestMigrationFieldPatchingFactoryCases, ): void => { diff --git a/test/writeClient-migrateUpdateDocuments-images.test.ts b/test/writeClient-migrateUpdateDocuments-images.test.ts new file mode 100644 index 00000000..39efa933 --- /dev/null +++ b/test/writeClient-migrateUpdateDocuments-images.test.ts @@ -0,0 +1,65 @@ +import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" + +import { RichTextNodeType } from "../src" + +testMigrationFieldPatching("patches image fields", { + new: ({ migration }) => migration.createAsset("foo", "foo.png"), + existing: ({ migration, existingAssets }) => + migration.createAsset(existingAssets[0]), + otherRepository: ({ ctx, mockedDomain }) => { + return { + ...ctx.mock.value.image({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + } + }, + otherRepositoryWithThumbnails: ({ ctx, mockedDomain }) => { + const image = ctx.mock.value.image({ state: "filled" }) + const id = "foo-id" + + return { + ...image, + id, + url: `${mockedDomain}/foo.png?some=query`, + square: { + ...image, + id, + url: `${mockedDomain}/foo.png?some=other&query=params`, + }, + } + }, + otherRepositoryWithThumbnailsNoAlt: ({ ctx, mockedDomain }) => { + const image = ctx.mock.value.image({ state: "filled" }) + image.alt = null + const id = "foo-id" + + return { + ...image, + id, + url: `${mockedDomain}/foo.png?some=query`, + square: { + ...image, + id, + url: `${mockedDomain}/foo.png?some=other&query=params`, + }, + } + }, + otherRepositoryEmpty: ({ ctx }) => ctx.mock.value.image({ state: "empty" }), + richTextNew: ({ ctx, migration }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + migration.createAsset("foo", "foo.png"), + ], + richTextExisting: ({ ctx, migration, existingAssets }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + migration.createAsset(existingAssets[0]), + ], + richTextOtherRepository: ({ ctx, mockedDomain }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + ...ctx.mock.value.image({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + }, + ], +}) diff --git a/test/writeClient-migrateUpdateDocuments-link.test.ts b/test/writeClient-migrateUpdateDocuments-link.test.ts new file mode 100644 index 00000000..9c5a6e19 --- /dev/null +++ b/test/writeClient-migrateUpdateDocuments-link.test.ts @@ -0,0 +1,61 @@ +import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" + +import { RichTextNodeType } from "../src" + +testMigrationFieldPatching("patches link fields", { + existing: ({ existingDocuments }) => existingDocuments[0], + migration: ({ migration, migrationDocuments }) => { + return () => + migration.getByUID(migrationDocuments[0].type, migrationDocuments[0].uid) + }, + migrationNoTags: ({ migration, migrationDocuments }) => { + migrationDocuments[0].tags = undefined + + return () => + migration.getByUID(migrationDocuments[0].type, migrationDocuments[0].uid) + }, + otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { + const contentRelationship = ctx.mock.value.link({ type: "Document" }) + // `migrationDocuments` contains documents from "another repository" + contentRelationship.id = migrationDocuments[0].id! + + return contentRelationship + }, + brokenLink: ({ ctx }) => ctx.mock.value.link({ type: "Document" }), + richTextLinkNode: ({ existingDocuments }) => [ + { + type: RichTextNodeType.paragraph, + text: "lorem", + spans: [ + { type: RichTextNodeType.strong, start: 0, end: 5 }, + { + type: RichTextNodeType.hyperlink, + start: 0, + end: 5, + data: existingDocuments[0], + }, + ], + }, + ], + richTextNewImageNodeLink: ({ ctx, migration, existingDocuments }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + ...migration.createAsset("foo", "foo.png"), + linkTo: existingDocuments[0], + }, + ], + richTextOtherReposiotryImageNodeLink: ({ + ctx, + mockedDomain, + existingDocuments, + }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + ...ctx.mock.value.image({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + linkTo: existingDocuments[0], + }, + ], +}) diff --git a/test/writeClient-migrateUpdateDocuments-linkToMedia.test.ts b/test/writeClient-migrateUpdateDocuments-linkToMedia.test.ts new file mode 100644 index 00000000..abe49fa0 --- /dev/null +++ b/test/writeClient-migrateUpdateDocuments-linkToMedia.test.ts @@ -0,0 +1,81 @@ +import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" + +import { RichTextNodeType } from "../src" +import { AssetType } from "../src/types/api/asset/asset" + +testMigrationFieldPatching("patches link to media fields", { + new: ({ migration }) => migration.createAsset("foo", "foo.png").linkToMedia, + existing: ({ migration, existingAssets }) => + migration.createAsset(existingAssets[0]).linkToMedia, + existingNonImage: ({ migration, existingAssets }) => { + existingAssets[0].filename = "foo.pdf" + existingAssets[0].extension = "pdf" + existingAssets[0].kind = AssetType.Document + existingAssets[0].width = undefined + existingAssets[0].height = undefined + + return migration.createAsset(existingAssets[0]).linkToMedia + }, + otherRepository: ({ ctx, mockedDomain }) => { + return { + ...ctx.mock.value.linkToMedia({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + } + }, + otherRepositoryNotFoundID: ({ ctx }) => { + return { + ...ctx.mock.value.linkToMedia({ state: "empty" }), + id: null, + } + }, + richTextNew: ({ migration }) => [ + { + type: RichTextNodeType.paragraph, + text: "lorem", + spans: [ + { type: RichTextNodeType.strong, start: 0, end: 5 }, + { + type: RichTextNodeType.hyperlink, + start: 0, + end: 5, + data: migration.createAsset("foo", "foo.png").linkToMedia, + }, + ], + }, + ], + richTextExisting: ({ migration, existingAssets }) => [ + { + type: RichTextNodeType.paragraph, + text: "lorem", + spans: [ + { type: RichTextNodeType.strong, start: 0, end: 5 }, + { + type: RichTextNodeType.hyperlink, + start: 0, + end: 5, + data: migration.createAsset(existingAssets[0]).linkToMedia, + }, + ], + }, + ], + richTextOtherRepository: ({ ctx, mockedDomain }) => [ + { + type: RichTextNodeType.paragraph, + text: "lorem", + spans: [ + { type: RichTextNodeType.strong, start: 0, end: 5 }, + { + type: RichTextNodeType.hyperlink, + start: 0, + end: 5, + data: { + ...ctx.mock.value.linkToMedia({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + }, + }, + ], + }, + ], +}) diff --git a/test/writeClient-migrateUpdateDocuments-simpleFields.test.ts b/test/writeClient-migrateUpdateDocuments-simpleFields.test.ts new file mode 100644 index 00000000..fce0c932 --- /dev/null +++ b/test/writeClient-migrateUpdateDocuments-simpleFields.test.ts @@ -0,0 +1,22 @@ +import { testMigrationSimpleFieldPatching } from "./__testutils__/testMigrationFieldPatching" + +testMigrationSimpleFieldPatching("does not patch simple fields", { + embed: ({ ctx }) => ctx.mock.value.embed({ state: "filled" }), + date: ({ ctx }) => ctx.mock.value.date({ state: "filled" }), + timestamp: ({ ctx }) => ctx.mock.value.timestamp({ state: "filled" }), + color: ({ ctx }) => ctx.mock.value.color({ state: "filled" }), + number: ({ ctx }) => ctx.mock.value.number({ state: "filled" }), + keyText: ({ ctx }) => ctx.mock.value.keyText({ state: "filled" }), + richTextSimple: ({ ctx }) => + ctx.mock.value.richText({ state: "filled", pattern: "long" }), + select: ({ ctx }) => + ctx.mock.value.select({ + model: ctx.mock.model.select({ options: ["foo", "bar"] }), + state: "filled", + }), + boolean: ({ ctx }) => ctx.mock.value.boolean(), + geoPoint: ({ ctx }) => ctx.mock.value.geoPoint({ state: "filled" }), + integration: ({ ctx }) => ctx.mock.value.integration({ state: "filled" }), + linkToWeb: ({ ctx }) => + ctx.mock.value.link({ type: "Web", withTargetBlank: true }), +}) diff --git a/test/writeClient-migrateUpdateDocuments.test.ts b/test/writeClient-migrateUpdateDocuments.test.ts deleted file mode 100644 index 7446708a..00000000 --- a/test/writeClient-migrateUpdateDocuments.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - testMigrationImageFieldPatching, - testMigrationLinkFieldPatching, - testMigrationSimpleFieldPatching, -} from "./__testutils__/testMigrationFieldPatching" - -testMigrationSimpleFieldPatching("does not patch simple fields", { - embed: ({ ctx }) => ctx.mock.value.embed({ state: "filled" }), - date: ({ ctx }) => ctx.mock.value.date({ state: "filled" }), - timestamp: ({ ctx }) => ctx.mock.value.timestamp({ state: "filled" }), - color: ({ ctx }) => ctx.mock.value.color({ state: "filled" }), - number: ({ ctx }) => ctx.mock.value.number({ state: "filled" }), - keyText: ({ ctx }) => ctx.mock.value.keyText({ state: "filled" }), - select: ({ ctx }) => - ctx.mock.value.select({ - model: ctx.mock.model.select({ options: ["foo", "bar"] }), - state: "filled", - }), - boolean: ({ ctx }) => ctx.mock.value.boolean(), - geoPoint: ({ ctx }) => ctx.mock.value.geoPoint({ state: "filled" }), - integration: ({ ctx }) => ctx.mock.value.integration({ state: "filled" }), - linkToWeb: ({ ctx }) => - ctx.mock.value.link({ type: "Web", withTargetBlank: true }), -}) - -testMigrationImageFieldPatching("patches image fields", { - regular: ({ migration }) => migration.createAsset("foo", "foo.png"), - richText: ({ ctx, migration }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - migration.createAsset("foo", "foo.png"), - ], -}) - -testMigrationLinkFieldPatching("patches link fields", { - existingDocument: ({ existingDocuments }) => existingDocuments[0], - migrationDocument: - ({ migration, migrationDocuments }) => - () => - migration.getByUID(migrationDocuments[0].type, migrationDocuments[0].uid), - otherRepositoryContentRelationship: ({ ctx, existingDocuments }) => { - const contentRelationship = ctx.mock.value.link({ type: "Document" }) - contentRelationship.id = existingDocuments[0].id - - return contentRelationship - }, -}) From 8759c81eefbe7516eb61a34980b0e918f3b26ddf Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 6 Sep 2024 13:03:15 +0200 Subject: [PATCH 33/61] test: `pLimit` --- src/lib/pLimit.ts | 19 +++---------------- test/writeClient-createAsset.test.ts | 16 ++++++++++++++++ test/writeClient-createAssetTag.test.ts | 16 ++++++++++++++++ test/writeClient-createDocument.test.ts | 24 ++++++++++++++++++++++++ test/writeClient-updateDocument.test.ts | 19 +++++++++++++++++++ 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/lib/pLimit.ts b/src/lib/pLimit.ts index 0f929623..0119be83 100644 --- a/src/lib/pLimit.ts +++ b/src/lib/pLimit.ts @@ -9,11 +9,6 @@ type AnyFunction = (...arguments_: readonly any[]) => unknown const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) export type LimitFunction = { - /** - * The number of queued items waiting to be executed. - */ - readonly queueSize: number - /** * @param fn - Promise-returning/async function. * @param arguments - Any arguments to pass through to `fn`. Support for @@ -97,16 +92,8 @@ export const pLimit = ({ })() } - const generator = (function_: AnyFunction, ...arguments_: unknown[]) => - new Promise((resolve) => { + return ((function_: AnyFunction, ...arguments_: unknown[]) => + new Promise((resolve) => { enqueue(function_, resolve, arguments_) - }) - - Object.defineProperties(generator, { - queueSize: { - get: () => queue.length, - }, - }) - - return generator as LimitFunction + })) as LimitFunction } diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts index f15232a5..b5f5d213 100644 --- a/test/writeClient-createAsset.test.ts +++ b/test/writeClient-createAsset.test.ts @@ -243,3 +243,19 @@ it.concurrent("respects unknown rate limit", async (ctx) => { expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) }) + +it("throws fetch errors as-is", async (ctx) => { + const client = createTestWriteClient({ + ctx, + clientConfig: { + fetch: () => { + throw new Error(ctx.task.name) + }, + }, + }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAsset("foo", "foo.png"), + ).rejects.toThrowError(ctx.task.name) +}) diff --git a/test/writeClient-createAssetTag.test.ts b/test/writeClient-createAssetTag.test.ts index 6b08f6c2..78c532c1 100644 --- a/test/writeClient-createAssetTag.test.ts +++ b/test/writeClient-createAssetTag.test.ts @@ -130,3 +130,19 @@ it.concurrent("respects unknown rate limit", async (ctx) => { expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) }) + +it("throws fetch errors as-is", async (ctx) => { + const client = createTestWriteClient({ + ctx, + clientConfig: { + fetch: () => { + throw new Error(ctx.task.name) + }, + }, + }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("foo"), + ).rejects.toThrowError(ctx.task.name) +}) diff --git a/test/writeClient-createDocument.test.ts b/test/writeClient-createDocument.test.ts index 4c36bfd6..8a8a1574 100644 --- a/test/writeClient-createDocument.test.ts +++ b/test/writeClient-createDocument.test.ts @@ -127,3 +127,27 @@ it.concurrent("respects unknown rate limit", async (ctx) => { expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) }) + +it("throws fetch errors as-is", async (ctx) => { + const client = createTestWriteClient({ + ctx, + clientConfig: { + fetch: () => { + throw new Error(ctx.task.name) + }, + }, + }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createDocument( + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + ), + ).rejects.toThrowError(ctx.task.name) +}) diff --git a/test/writeClient-updateDocument.test.ts b/test/writeClient-updateDocument.test.ts index 8dab4741..4ac07452 100644 --- a/test/writeClient-updateDocument.test.ts +++ b/test/writeClient-updateDocument.test.ts @@ -144,3 +144,22 @@ it.concurrent("supports custom headers", async (ctx) => { .resolves.toBeUndefined() ctx.expect.assertions(2) }) + +it("throws fetch errors as-is", async (ctx) => { + const client = createTestWriteClient({ + ctx, + clientConfig: { + fetch: () => { + throw new Error(ctx.task.name) + }, + }, + }) + + await expect(() => + // @ts-expect-error - testing purposes + client.updateDocument("foo", { + uid: "uid", + data: {}, + }), + ).rejects.toThrowError(ctx.task.name) +}) From afd6d69983f496fcc7c96b021920f83a8faca442 Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 6 Sep 2024 14:09:21 +0200 Subject: [PATCH 34/61] test: skip `WriteClient.fetchForeignAsset` tests on Node 18 --- test/writeClient-fetchForeignAsset.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/writeClient-fetchForeignAsset.test.ts b/test/writeClient-fetchForeignAsset.test.ts index 44938bc9..551194c2 100644 --- a/test/writeClient-fetchForeignAsset.test.ts +++ b/test/writeClient-fetchForeignAsset.test.ts @@ -4,9 +4,10 @@ import { rest } from "msw" import { createTestWriteClient } from "./__testutils__/createWriteClient" -// Skip test on Node 16 and 18 (File support) +// Skip test on Node 16 and 18 (File and FormData support) const isNode16 = process.version.startsWith("v16") -const it = _it.skipIf(isNode16) +const isNode18 = process.version.startsWith("v18") +const it = _it.skipIf(isNode16 || isNode18) it.concurrent("fetches a foreign asset with content type", async (ctx) => { const client = createTestWriteClient({ ctx }) From daac44f49ec05eb4726cae22637007987aef6208 Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 6 Sep 2024 15:34:53 +0200 Subject: [PATCH 35/61] refactor: per self review --- prismicio-client-7.8.1.tgz | Bin 0 -> 280180 bytes src/BaseClient.ts | 2 +- src/Migration.ts | 2 - src/lib/pLimit.ts | 43 +- ...-migrateUpdateDocuments-image.test.ts.snap | 1800 +++++++++++++++++ .../testMigrationFieldPatching.ts | 1 - test/migration-createDocument.test.ts | 18 +- ...ient-migrateUpdateDocuments-image.test.ts} | 0 ...igrateUpdateDocuments-simpleField.test.ts} | 0 9 files changed, 1828 insertions(+), 38 deletions(-) create mode 100644 prismicio-client-7.8.1.tgz create mode 100644 test/__snapshots__/writeClient-migrateUpdateDocuments-image.test.ts.snap rename test/{writeClient-migrateUpdateDocuments-images.test.ts => writeClient-migrateUpdateDocuments-image.test.ts} (100%) rename test/{writeClient-migrateUpdateDocuments-simpleFields.test.ts => writeClient-migrateUpdateDocuments-simpleField.test.ts} (100%) diff --git a/prismicio-client-7.8.1.tgz b/prismicio-client-7.8.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..4dd8dcc8563399fcefd2c752d4de77de35165277 GIT binary patch literal 280180 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 i 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ⅈ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 ~F6Xomi0 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 = { text?: string } -export class BaseClient { +export abstract class BaseClient { /** * The function used to make network requests to the Prismic REST API. In * environments where a global `fetch` function does not exist, such as diff --git a/src/Migration.ts b/src/Migration.ts index fa03d4b3..53b72ab1 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -143,8 +143,6 @@ export class Migration< */ assets: Map = new Map() - constructor() {} - createAsset( asset: Asset | FilledImageFieldImage | FilledLinkToMediaField, ): CreateAssetReturnType diff --git a/src/lib/pLimit.ts b/src/lib/pLimit.ts index 0119be83..40aba216 100644 --- a/src/lib/pLimit.ts +++ b/src/lib/pLimit.ts @@ -8,24 +8,31 @@ type AnyFunction = (...arguments_: readonly any[]) => unknown const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) -export type LimitFunction = { - /** - * @param fn - Promise-returning/async function. - * @param arguments - Any arguments to pass through to `fn`. Support for - * passing arguments on to the `fn` is provided in order to be able to avoid - * creating unnecessary closures. You probably don't need this optimization - * unless you're pushing a lot of functions. - * - * @returns The promise returned by calling `fn(...arguments)`. - */ - ( - function_: ( - ...arguments_: Arguments - ) => PromiseLike | ReturnType, - ...arguments_: Arguments - ): Promise -} - +/** + * @param fn - Promise-returning/async function. + * @param arguments - Any arguments to pass through to `fn`. Support for passing + * arguments on to the `fn` is provided in order to be able to avoid creating + * unnecessary closures. You probably don't need this optimization unless + * you're pushing a lot of functions. + * + * @returns The promise returned by calling `fn(...arguments)`. + */ +export type LimitFunction = ( + function_: ( + ...arguments_: TArguments + ) => PromiseLike | TReturnType, + ...arguments_: TArguments +) => Promise + +/** + * Creates a limiting function that will only execute one promise at a time and + * respect a given interval between each call. + * + * @param args - Options for the function, `interval` is the minimum time to + * wait between each promise execution. + * + * @returns A limiting function as per configuration, see {@link LimitFunction}. + */ export const pLimit = ({ interval, }: { interval?: number } = {}): LimitFunction => { diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-image.test.ts.snap b/test/__snapshots__/writeClient-migrateUpdateDocuments-image.test.ts.snap new file mode 100644 index 00000000..daa3e7f0 --- /dev/null +++ b/test/__snapshots__/writeClient-migrateUpdateDocuments-image.test.ts.snap @@ -0,0 +1,1800 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`patches image fields > existing > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + }, + }, + ], +} +`; + +exports[`patches image fields > existing > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > existing > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > existing > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + }, +} +`; + +exports[`patches image fields > new > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "ef95d5daa4d", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", + }, + }, + ], +} +`; + +exports[`patches image fields > new > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "f2c3f8bfc90", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > new > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "45306297c5e", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "45306297c5e", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > new > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "c5c95f8d3ac", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, +} +`; + +exports[`patches image fields > otherRepository > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + }, + }, + ], +} +`; + +exports[`patches image fields > otherRepository > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + }, + }, + ], + "primary": { + "field": { + "alt": "Ut consequat semper viverra nam libero justo laoreet", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + }, + "group": [ + { + "field": { + "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > otherRepository > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c", + }, + }, + ], + "primary": { + "field": { + "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > otherRepository > static zone 1`] = ` +{ + "field": { + "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f", + }, +} +`; + +exports[`patches image fields > otherRepositoryEmpty > group 1`] = ` +{ + "group": [ + { + "field": {}, + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryEmpty > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": {}, + }, + ], + "primary": { + "field": {}, + "group": [ + { + "field": {}, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryEmpty > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": {}, + }, + ], + "primary": { + "field": {}, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryEmpty > static zone 1`] = ` +{ + "field": {}, +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnails > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "square": { + "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", + }, + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": "Ut consequat semper viverra nam libero justo laoreet", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": "Ut consequat semper viverra nam libero justo laoreet", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + "group": [ + { + "field": { + "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "square": { + "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "square": { + "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnails > static zone 1`] = ` +{ + "field": { + "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "square": { + "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + }, +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1705, + "width": 2560, + }, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", + }, + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 2832, + "width": 4240, + }, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + }, +} +`; + +exports[`patches image fields > richTextExisting > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches image fields > richTextExisting > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > richTextExisting > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > richTextExisting > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + }, + ], +} +`; + +exports[`patches image fields > richTextNew > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "cdfdd322ca9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches image fields > richTextNew > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > richTextNew > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "7963dc361cc", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "7963dc361cc", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > richTextNew > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 0, + }, + "id": "0bbad670dad", + "type": "image", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + }, + ], +} +`; + +exports[`patches image fields > richTextOtherRepository > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", + "copyright": null, + "dimensions": { + "height": 4000, + "width": 6000, + }, + "edit": { + "background": "#7cfc26", + "x": 2977, + "y": -1163, + "zoom": 1.0472585898934068, + }, + "id": "e8d0985c099", + "type": "image", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + }, + ], + }, + ], +} +`; + +exports[`patches image fields > richTextOtherRepository > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Facilisis", + "type": "heading3", + }, + { + "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#37ae3f", + "x": -1505, + "y": 902, + "zoom": 1.8328975606320652, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": "Proin libero nunc consequat interdum varius", + "copyright": null, + "dimensions": { + "height": 3036, + "width": 4554, + }, + "edit": { + "background": "#904f4f", + "x": 1462, + "y": 1324, + "zoom": 1.504938844941775, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Egestas Integer Eget Aliquet Nibh", + "type": "heading5", + }, + { + "alt": "Arcu cursus vitae congue mauris", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#5f7a9b", + "x": -119, + "y": -2667, + "zoom": 1.9681315715350518, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches image fields > richTextOtherRepository > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Amet", + "type": "heading5", + }, + { + "alt": "Auctor neque vitae tempus quam", + "copyright": null, + "dimensions": { + "height": 1440, + "width": 2560, + }, + "edit": { + "background": "#5bc5aa", + "x": -1072, + "y": -281, + "zoom": 1.3767766101231744, + }, + "id": "f70ca27104d", + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", + "copyright": null, + "dimensions": { + "height": 3036, + "width": 4554, + }, + "edit": { + "background": "#4860cb", + "x": 280, + "y": -379, + "zoom": 1.2389796902982004, + }, + "id": "f70ca27104d", + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches image fields > richTextOtherRepository > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": "Interdum velit euismod in pellentesque", + "copyright": null, + "dimensions": { + "height": 4392, + "width": 7372, + }, + "edit": { + "background": "#ab1d17", + "x": 3605, + "y": 860, + "zoom": 1.9465488211593005, + }, + "id": "04a95cc61c3", + "type": "image", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + }, + ], +} +`; diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index 2495629e..a51f5e01 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -107,7 +107,6 @@ const internalTestMigrationFieldPatching = ( ctx.expect(data).toMatchSnapshot() } - vi.useRealTimers() vi.restoreAllMocks() }) } diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index bc2cd363..63d533f8 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -24,24 +24,10 @@ it("creates a document", () => { }) }) -it("creates a document from an existing Prismic document", () => { +it("creates a document from an existing Prismic document", (ctx) => { const migration = prismic.createMigration() - const document: prismic.PrismicDocument = { - id: "id", - type: "type", - uid: "uid", - lang: "lang", - url: "url", - href: "href", - slugs: [], - tags: [], - linked_documents: [], - first_publication_date: "0-0-0T0:0:0+0", - last_publication_date: "0-0-0T0:0:0+0", - alternate_languages: [], - data: {}, - } + const document = ctx.mock.value.document() const documentName = "documentName" migration.createDocument(document, documentName) diff --git a/test/writeClient-migrateUpdateDocuments-images.test.ts b/test/writeClient-migrateUpdateDocuments-image.test.ts similarity index 100% rename from test/writeClient-migrateUpdateDocuments-images.test.ts rename to test/writeClient-migrateUpdateDocuments-image.test.ts diff --git a/test/writeClient-migrateUpdateDocuments-simpleFields.test.ts b/test/writeClient-migrateUpdateDocuments-simpleField.test.ts similarity index 100% rename from test/writeClient-migrateUpdateDocuments-simpleFields.test.ts rename to test/writeClient-migrateUpdateDocuments-simpleField.test.ts From 78034616b288e2b2af975055373cdd00a553b25b Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 9 Sep 2024 16:18:58 +0200 Subject: [PATCH 36/61] refactor: use random API key pool for migration API demo --- messages/avoid-write-client-in-browser.md | 1 - src/WriteClient.ts | 53 ++++++++++++++++++++++- test/__testutils__/createWriteClient.ts | 1 - test/writeClient.test.ts | 14 ++++-- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/messages/avoid-write-client-in-browser.md b/messages/avoid-write-client-in-browser.md index c20834ce..d88dbc9f 100644 --- a/messages/avoid-write-client-in-browser.md +++ b/messages/avoid-write-client-in-browser.md @@ -9,7 +9,6 @@ import * as prismic from "@prismicio/client"; const writeClient = prismic.createWriteClient("example-prismic-repo", { writeToken: "xxx" - migrationAPIKey: "yyy" }) ``` diff --git a/src/WriteClient.ts b/src/WriteClient.ts index a2e75851..3f42feba 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -224,6 +224,18 @@ const ASSET_CREDITS_MAX_LENGTH = 500 */ const ASSET_ALT_MAX_LENGTH = 500 +/** + * Prismic migration API demo keys. + */ +const MIGRATION_API_DEMO_KEYS = [ + "cSaZlfkQlF9C6CEAM2Del6MNX9WonlV86HPbeEJL", + "pZCexCajUQ4jriYwIGSxA1drZrFxDyFf1S0D1K0P", + "Yc0mfrkGDw8gaaGKTrzwC3QUZDajv6k73DA99vWN", + "ySzSEbVMAb5S1oSCQfbVG4mbh9Cb8wlF7BCvKI0L", + "g2DA3EKWvx8uxVYcNFrmT5nJpon1Vi9V4XcOibJD", + "CCNIlI0Vz41J66oFwsHUXaZa6NYFIY6z7aDF62Bc", +] + /** * Checks if a string is an asset tag ID. * @@ -297,12 +309,45 @@ export const validateAssetMetadata = ({ * Configuration for clients that determine how content is queried. */ export type WriteClientConfig = { + /** + * A Prismic write token that allows writing content to the repository. + */ writeToken: string - migrationAPIKey: string + /** + * An explicit Prismic migration API key that allows working with the + * migration API. If none is provided, the client will pick a random one to + * authenticate your requests. + * + * @remarks + * Those keys are the same for all Prismic users. They are only useful while + * the migration API is in beta to reduce load. It should be one of: + * + * - `cSaZlfkQlF9C6CEAM2Del6MNX9WonlV86HPbeEJL` + * - `pZCexCajUQ4jriYwIGSxA1drZrFxDyFf1S0D1K0P` + * - `Yc0mfrkGDw8gaaGKTrzwC3QUZDajv6k73DA99vWN` + * - `ySzSEbVMAb5S1oSCQfbVG4mbh9Cb8wlF7BCvKI0L` + * - `g2DA3EKWvx8uxVYcNFrmT5nJpon1Vi9V4XcOibJD` + * - `CCNIlI0Vz41J66oFwsHUXaZa6NYFIY6z7aDF62Bc` + */ + migrationAPIKey?: string + /** + * The Prismic asset API endpoint. + * + * @defaultValue `"https://asset-api.prismic.io/"` + * + * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + */ assetAPIEndpoint?: string + /** + * The Prismic migration API endpoint. + * + * @defaultValue `"https://migration.prismic.io/"` + * + * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + */ migrationAPIEndpoint?: string } & ClientConfig @@ -349,7 +394,11 @@ export class WriteClient< } this.writeToken = options.writeToken - this.migrationAPIKey = options.migrationAPIKey + this.migrationAPIKey = + options.migrationAPIKey || + MIGRATION_API_DEMO_KEYS[ + Math.floor(Math.random() * MIGRATION_API_DEMO_KEYS.length) + ] if (options.assetAPIEndpoint) { this.assetAPIEndpoint = `${options.assetAPIEndpoint}/` diff --git a/test/__testutils__/createWriteClient.ts b/test/__testutils__/createWriteClient.ts index 962d8ceb..32ebde3b 100644 --- a/test/__testutils__/createWriteClient.ts +++ b/test/__testutils__/createWriteClient.ts @@ -21,7 +21,6 @@ export const createTestWriteClient = ( return prismic.createWriteClient(repositoryName, { fetch, writeToken: "xxx", - migrationAPIKey: "yyy", // We create unique endpoints so we can run tests concurrently assetAPIEndpoint: `https://${repositoryName}.asset-api.prismic.io`, migrationAPIEndpoint: `https://${repositoryName}.migration.prismic.io`, diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts index b2ba0466..55381e7c 100644 --- a/test/writeClient.test.ts +++ b/test/writeClient.test.ts @@ -6,7 +6,6 @@ it("`createWriteClient` creates a write client", () => { const client = prismic.createWriteClient("qwerty", { fetch: vi.fn(), writeToken: "xxx", - migrationAPIKey: "yyy", }) expect(client).toBeInstanceOf(prismic.WriteClient) @@ -23,7 +22,6 @@ it("constructor warns if running in a browser-like environment", () => { prismic.createWriteClient("qwerty", { fetch: vi.fn(), writeToken: "xxx", - migrationAPIKey: "yyy", }) expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringMatching(/avoid-write-client-in-browser/i), @@ -39,7 +37,6 @@ it("uses provided asset API endpoint and adds `/` suffix", () => { fetch: vi.fn(), assetAPIEndpoint: "https://example.com", writeToken: "xxx", - migrationAPIKey: "yyy", }) expect(client.assetAPIEndpoint).toBe("https://example.com/") @@ -50,8 +47,17 @@ it("uses provided migration API endpoint and adds `/` suffix", () => { fetch: vi.fn(), migrationAPIEndpoint: "https://example.com", writeToken: "xxx", - migrationAPIKey: "yyy", }) expect(client.migrationAPIEndpoint).toBe("https://example.com/") }) + +it("uses provided migration API key", () => { + const client = prismic.createWriteClient("qwerty", { + fetch: vi.fn(), + writeToken: "xxx", + migrationAPIKey: "yyy", + }) + + expect(client.migrationAPIKey).toBe("yyy") +}) From fd80f2ec231d1cee38e735f66a1a76793bc0f005 Mon Sep 17 00:00:00 2001 From: lihbr Date: Tue, 10 Sep 2024 11:13:33 +0200 Subject: [PATCH 37/61] docs: capitalize `Migration API` --- messages/avoid-write-client-in-browser.md | 2 +- src/WriteClient.ts | 26 +++++++++++------------ src/index.ts | 2 +- src/types/api/migration/document.ts | 12 +++++------ src/types/migration/document.ts | 4 ++-- src/types/migration/fields.ts | 8 +++---- src/types/migration/richText.ts | 4 ++-- test/writeClient-migrate.test.ts | 2 +- test/writeClient.test.ts | 4 ++-- 9 files changed, 32 insertions(+), 32 deletions(-) diff --git a/messages/avoid-write-client-in-browser.md b/messages/avoid-write-client-in-browser.md index d88dbc9f..9eb2d0b0 100644 --- a/messages/avoid-write-client-in-browser.md +++ b/messages/avoid-write-client-in-browser.md @@ -2,7 +2,7 @@ `@prismicio/client`'s write client uses credentials to authenticate write queries to a Prismic repository. -The repository write token and migration API key must be provided when creating a `@prismicio/client` write client like the following: +The repository write token and Migration API key must be provided when creating a `@prismicio/client` write client like the following: ```typescript import * as prismic from "@prismicio/client"; diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 3f42feba..6edc5701 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -225,7 +225,7 @@ const ASSET_CREDITS_MAX_LENGTH = 500 const ASSET_ALT_MAX_LENGTH = 500 /** - * Prismic migration API demo keys. + * Prismic Migration API demo keys. */ const MIGRATION_API_DEMO_KEYS = [ "cSaZlfkQlF9C6CEAM2Del6MNX9WonlV86HPbeEJL", @@ -315,13 +315,13 @@ export type WriteClientConfig = { writeToken: string /** - * An explicit Prismic migration API key that allows working with the - * migration API. If none is provided, the client will pick a random one to + * An explicit Prismic Migration API key that allows working with the + * Migration API. If none is provided, the client will pick a random one to * authenticate your requests. * * @remarks * Those keys are the same for all Prismic users. They are only useful while - * the migration API is in beta to reduce load. It should be one of: + * the Migration API is in beta to reduce load. It should be one of: * * - `cSaZlfkQlF9C6CEAM2Del6MNX9WonlV86HPbeEJL` * - `pZCexCajUQ4jriYwIGSxA1drZrFxDyFf1S0D1K0P` @@ -342,11 +342,11 @@ export type WriteClientConfig = { assetAPIEndpoint?: string /** - * The Prismic migration API endpoint. + * The Prismic Migration API endpoint. * * @defaultValue `"https://migration.prismic.io/"` * - * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} */ migrationAPIEndpoint?: string } & ClientConfig @@ -389,7 +389,7 @@ export class WriteClient< if (typeof globalThis.window !== "undefined") { console.warn( - `[@prismicio/client] Prismic write client appears to be running in a browser environment. This is not recommended as it exposes your write token and migration API key. Consider using Prismic write client in a server environment only, preferring the regular client for browser environement. For more details, see ${devMsg("avoid-write-client-in-browser")}`, + `[@prismicio/client] Prismic write client appears to be running in a browser environment. This is not recommended as it exposes your write token and Migration API key. Consider using Prismic write client in a server environment only, preferring the regular client for browser environement. For more details, see ${devMsg("avoid-write-client-in-browser")}`, ) } @@ -416,7 +416,7 @@ export class WriteClient< * @param migration - A migration prepared with {@link createMigration}. * @param params - An event listener and additional fetch parameters. * - * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} */ async migrate( migration: Migration, @@ -1167,7 +1167,7 @@ export class WriteClient< * * @returns The ID of the created document. * - * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} */ private async createDocument( document: PrismicMigrationDocument>, @@ -1208,7 +1208,7 @@ export class WriteClient< * @param document - The document data to update. * @param params - Additional fetch parameters. * - * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} */ private async updateDocument( id: string, @@ -1283,15 +1283,15 @@ export class WriteClient< } /** - * Builds fetch parameters for the migration API. + * Builds fetch parameters for the Migration API. * * @typeParam TBody - Type of the body to send in the fetch request. * * @param params - Method, body, and additional fetch options. * - * @returns An object that can be fetched to interact with the migration API. + * @returns An object that can be fetched to interact with the Migration API. * - * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} */ private buildMigrationAPIQueryParams< TBody extends PostDocumentParams | PutDocumentParams, diff --git a/src/index.ts b/src/index.ts index 1ff3342e..45768631 100644 --- a/src/index.ts +++ b/src/index.ts @@ -325,7 +325,7 @@ export type { CustomTypeModelFieldForSlicePrimary, } from "./types/model/types" -// Migrations - Types representing Prismic migration API content values. +// Migrations - Types representing Prismic Migration API content values. export { MigrationFieldType } from "./types/migration/fields" export type { diff --git a/src/types/api/migration/document.ts b/src/types/api/migration/document.ts index e8662236..27434225 100644 --- a/src/types/api/migration/document.ts +++ b/src/types/api/migration/document.ts @@ -2,11 +2,11 @@ import type { PrismicDocument } from "../../value/document" /** * An object representing the parameters required when creating a document - * through the migration API. + * through the Migration API. * * @typeParam TDocument - Type of the Prismic document to create. * - * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} */ export type PostDocumentParams< TDocument extends PrismicDocument = PrismicDocument, @@ -26,7 +26,7 @@ export type PostDocumentParams< : never /** - * Result of creating a document with the migration API. + * Result of creating a document with the Migration API. * * @typeParam TDocument - Type of the created Prismic document. * @@ -49,11 +49,11 @@ export type PostDocumentResult< /** * An object representing the parameters required when updating a document - * through the migration API. + * through the Migration API. * * @typeParam TDocument - Type of the Prismic document to update. * - * @see Prismic migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} */ export type PutDocumentParams< TDocument extends PrismicDocument = PrismicDocument, @@ -69,7 +69,7 @@ export type PutDocumentParams< } /** - * Result of updating a document with the migration API. + * Result of updating a document with the Migration API. * * @typeParam TDocument - Type of the updated Prismic document. * diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts index 91fae740..68123ee9 100644 --- a/src/types/migration/document.ts +++ b/src/types/migration/document.ts @@ -138,7 +138,7 @@ type MakeUIDOptional = : Omit & Partial> /** - * A Prismic document compatible with the migration API. + * A Prismic document compatible with the Migration API. * * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} */ @@ -196,7 +196,7 @@ export type PrismicMigrationDocument< : never /** - * Parameters used when creating a Prismic document with the migration API. + * Parameters used when creating a Prismic document with the Migration API. * * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} */ diff --git a/src/types/migration/fields.ts b/src/types/migration/fields.ts index 757e60c8..8b99b405 100644 --- a/src/types/migration/fields.ts +++ b/src/types/migration/fields.ts @@ -17,7 +17,7 @@ export const MigrationFieldType = { } as const /** - * An alternate version of the {@link ImageField} for use with the migration API. + * An alternate version of the {@link ImageField} for use with the Migration API. */ export type ImageMigrationField = | (MigrationAsset & { @@ -27,7 +27,7 @@ export type ImageMigrationField = /** * An alternate version of the {@link LinkToMediaField} for use with the - * migration API. + * Migration API. */ export type LinkToMediaMigrationField = | (MigrationAsset & { @@ -37,7 +37,7 @@ export type LinkToMediaMigrationField = /** * An alternate version of the {@link ContentRelationshipField} for use with the - * migration API. + * Migration API. */ export type ContentRelationshipMigrationField = | PrismicDocument @@ -49,7 +49,7 @@ export type ContentRelationshipMigrationField = | undefined /** - * An alternate version of the {@link LinkField} for use with the migration API. + * An alternate version of the {@link LinkField} for use with the Migration API. */ export type LinkMigrationField = | LinkToMediaMigrationField diff --git a/src/types/migration/richText.ts b/src/types/migration/richText.ts index 22ddc3d4..9196ee88 100644 --- a/src/types/migration/richText.ts +++ b/src/types/migration/richText.ts @@ -4,7 +4,7 @@ import type { ImageMigrationField, LinkMigrationField } from "./fields" /** * An alternate version of {@link RTLinkNode} that supports - * {@link LinkMigrationField} for use with the migration API. + * {@link LinkMigrationField} for use with the Migration API. */ export type RTLinkMigrationNode = Omit & { data: LinkMigrationField @@ -12,7 +12,7 @@ export type RTLinkMigrationNode = Omit & { /** * An alternate version of {@link RTImageNode} that supports - * {@link ImageMigrationField} for use with the migration API. + * {@link ImageMigrationField} for use with the Migration API. */ export type RTImageMigrationNode = ImageMigrationField & { linkTo?: RTImageNode["linkTo"] | LinkMigrationField diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index b05f4287..896fd005 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -16,7 +16,7 @@ it.concurrent("migrates nothing when migration is empty", async (ctx) => { mockPrismicRestAPIV2({ ctx }) mockPrismicAssetAPI({ ctx, client }) - // Not mocking migration API to test we're not calling it + // Not mocking Migration API to test we're not calling it // mockPrismicMigrationAPI({ ctx, client }) const migration = prismic.createMigration() diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts index 55381e7c..9e793b2a 100644 --- a/test/writeClient.test.ts +++ b/test/writeClient.test.ts @@ -42,7 +42,7 @@ it("uses provided asset API endpoint and adds `/` suffix", () => { expect(client.assetAPIEndpoint).toBe("https://example.com/") }) -it("uses provided migration API endpoint and adds `/` suffix", () => { +it("uses provided Migration API endpoint and adds `/` suffix", () => { const client = prismic.createWriteClient("qwerty", { fetch: vi.fn(), migrationAPIEndpoint: "https://example.com", @@ -52,7 +52,7 @@ it("uses provided migration API endpoint and adds `/` suffix", () => { expect(client.migrationAPIEndpoint).toBe("https://example.com/") }) -it("uses provided migration API key", () => { +it("uses provided Migration API key", () => { const client = prismic.createWriteClient("qwerty", { fetch: vi.fn(), writeToken: "xxx", From 50a1f7897eb0d29f81c9679f50e4b09c6d02677f Mon Sep 17 00:00:00 2001 From: lihbr Date: Tue, 10 Sep 2024 11:27:22 +0200 Subject: [PATCH 38/61] docs: messages copy editing --- messages/avoid-write-client-in-browser.md | 4 ++-- messages/endpoint-must-use-cdn.md | 10 +++------- messages/prefer-repository-name.md | 23 +++++++---------------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/messages/avoid-write-client-in-browser.md b/messages/avoid-write-client-in-browser.md index 9eb2d0b0..a45729ae 100644 --- a/messages/avoid-write-client-in-browser.md +++ b/messages/avoid-write-client-in-browser.md @@ -12,9 +12,9 @@ const writeClient = prismic.createWriteClient("example-prismic-repo", { }) ``` -If the write client gets exposed to the browser, its credentials also do. This potentially exposes them to malicious actors. +If the write client is exposed to the browser, so are its tokens. Malicious actors will have write access to your repository. -When no write actions are to be performed, using a `@prismicio/client` regular client should be preferred. This client only has read access to a Prismic repository. +Use the non-write client when write actions are not needed. The non-write client only has read access to the repository and can safely be used in the browser. Be aware the client's access token, if used, will be exposed in the browser. ```typescript import * as prismic from "@prismicio/client"; diff --git a/messages/endpoint-must-use-cdn.md b/messages/endpoint-must-use-cdn.md index 1d78848d..405e385b 100644 --- a/messages/endpoint-must-use-cdn.md +++ b/messages/endpoint-must-use-cdn.md @@ -1,20 +1,16 @@ # `endpoint` must use CDN -`@prismicio/client` uses either a Prismic repository name or a Prismic Rest API v2 repository endpoint to query content from Prismic. +`@prismicio/client` uses either a Prismic repository name or a repository-specific Document API endpoint to query content from Prismic. -The repository name or repository endpoint must be provided when creating a `@prismicio/client` like the following: +The repository name must be provided when creating a `@prismicio/client` like the following: ```typescript import * as prismic from "@prismicio/client"; -// Using the repository name const client = prismic.createClient("example-prismic-repo") - -// Using the repository endpoint -const client = prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2") ``` -When creating a `@prismicio/client` with a repository endpoint, the endpoint's subdomain must feature the `.cdn` suffix. +When creating a `@prismicio/client` with a repository endpoint (not recommended), the endpoint's subdomain must feature the `.cdn` suffix. ```typescript import * as prismic from "@prismicio/client"; diff --git a/messages/prefer-repository-name.md b/messages/prefer-repository-name.md index a90b8bf5..86b8dce7 100644 --- a/messages/prefer-repository-name.md +++ b/messages/prefer-repository-name.md @@ -1,37 +1,28 @@ # Prefer repository name -`@prismicio/client` uses either a Prismic repository name or a Prismic Rest API v2 repository endpoint to query content from Prismic. +`@prismicio/client` uses either a Prismic repository name or a repository-specific Document API endpoint to query content from Prismic. -The repository name or repository endpoint must be provided when creating a `@prismicio/client` like the following: +The repository name must be provided when creating a `@prismicio/client` like the following: ```typescript import * as prismic from "@prismicio/client"; -// Using the repository name const client = prismic.createClient("example-prismic-repo") - -// Using the repository endpoint -const client = prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2") ``` -When proxying a Prismic API v2 repository endpoint (not recommended), the `apiEndpoint` option can be used to specify that endpoint. When doing so, the repository name should be used as the first argument when creating the client because a repository name can't be inferred from a proxied endpoint. +When proxying a Prismic API v2 repository endpoint (not recommended), the `apiEndpoint` option can be used to specify that endpoint. ```typescript import * as prismic from "@prismicio/client" // ✅ Correct const client = prismic.createClient("my-repo-name", { - apiEndpoint: "https://example.com/my-repo-name/prismic" -}) - -// ❌ Incorrect, repository name can't be inferred from a proxied endpoint -const client = prismic.createClient("https://example.com/my-repo-name/prismic", { - apiEndpoint: "https://example.com/my-repo-name/prismic" + apiEndpoint: "https://example.com/my-prismic-proxy" }) -// ❌ Incorrect, the endpoint does not match the `apiEndpoint` option. -const client = prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2", { - apiEndpoint: "https://example.com/my-repo-name/prismic" +// ❌ Incorrect: repository name can't be inferred from a proxied endpoint +const client = prismic.createClient("https://example.com/my-prismic-proxy", { + apiEndpoint: "https://example.com/my-prismic-proxy" }) ``` From db040fa18e22eead305d85254dafed854d091cda Mon Sep 17 00:00:00 2001 From: lihbr Date: Tue, 10 Sep 2024 11:28:19 +0200 Subject: [PATCH 39/61] chore: ignore all `.tgz` files --- .eslintignore | 1 + .gitignore | 1 + .prettierignore | 1 + prismicio-client-7.8.1.tgz | Bin 280180 -> 0 bytes 4 files changed, 3 insertions(+) delete mode 100644 prismicio-client-7.8.1.tgz diff --git a/.eslintignore b/.eslintignore index baca58ff..7845d18c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,6 +3,7 @@ CHANGELOG.md # .gitignore copy # custom +*.tgz dist examples/**/package-lock.json diff --git a/.gitignore b/.gitignore index 3efa9b36..4a9e6b52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # custom +*.tgz dist examples/**/package-lock.json diff --git a/.prettierignore b/.prettierignore index d378e5d8..a841146e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ CHANGELOG.md # .gitignore copy # custom +*.tgz dist examples/**/package-lock.json diff --git a/prismicio-client-7.8.1.tgz b/prismicio-client-7.8.1.tgz deleted file mode 100644 index 4dd8dcc8563399fcefd2c752d4de77de35165277..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 280180 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 i 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ⅈ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 ~F6Xomi0 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 Date: Tue, 10 Sep 2024 11:35:11 +0200 Subject: [PATCH 40/61] refactor: `apiEndpoint` to `documentAPIEndpoint` Co-authored-by: Angelo Ashmore --- messages/prefer-repository-name.md | 6 ++-- src/Client.ts | 51 ++++++++++++++++-------------- test/__testutils__/createClient.ts | 6 ++-- test/client.test.ts | 18 +++++------ 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/messages/prefer-repository-name.md b/messages/prefer-repository-name.md index 86b8dce7..3b6ca43a 100644 --- a/messages/prefer-repository-name.md +++ b/messages/prefer-repository-name.md @@ -10,19 +10,19 @@ import * as prismic from "@prismicio/client"; const client = prismic.createClient("example-prismic-repo") ``` -When proxying a Prismic API v2 repository endpoint (not recommended), the `apiEndpoint` option can be used to specify that endpoint. +When proxying a Prismic API v2 repository endpoint (not recommended), the `documentAPIEndpoint` option can be used to specify that endpoint. ```typescript import * as prismic from "@prismicio/client" // ✅ Correct const client = prismic.createClient("my-repo-name", { - apiEndpoint: "https://example.com/my-prismic-proxy" + documentAPIEndpoint: "https://example.com/my-prismic-proxy" }) // ❌ Incorrect: repository name can't be inferred from a proxied endpoint const client = prismic.createClient("https://example.com/my-prismic-proxy", { - apiEndpoint: "https://example.com/my-prismic-proxy" + documentAPIEndpoint: "https://example.com/my-prismic-proxy" }) ``` diff --git a/src/Client.ts b/src/Client.ts index d4193ee7..6f0973d0 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -165,7 +165,7 @@ export type ClientConfig = { * * @defaultValue `getRepositoryEndpoint(repositoryNameOrEndpoint)` */ - apiEndpoint?: string + documentAPIEndpoint?: string /** * The secure token for accessing the Prismic repository. This is only @@ -274,7 +274,7 @@ export class Client< get repositoryName(): string { if (!this.#repositoryName) { throw new PrismicError( - `This client instance was created using a repository endpoint and the repository name could not be infered from the it (\`${this.apiEndpoint}\`). The method you're trying to use relies on the repository name to work and is therefore disabled. Please create the client using your repository name, and the \`apiEndpoint\` option if necessary, to ensure all methods are enabled. For more details, see ${devMsg("prefer-repository-name")}`, + `A repository name is required for this method but one could not be inferred from the provided API endpoint (\`${this.documentAPIEndpoint}\`). To fix this error, provide a repository name when creating the client. For more details, see ${devMsg("prefer-repository-name")}`, undefined, undefined, ) @@ -287,28 +287,28 @@ export class Client< * The Prismic REST API V2 endpoint for the repository (use * `prismic.getRepositoryEndpoint` for the default endpoint). */ - apiEndpoint: string + documentAPIEndpoint: string /** * The Prismic REST API V2 endpoint for the repository (use * `prismic.getRepositoryEndpoint` for the default endpoint). * - * @deprecated Use `apiEndpoint` instead. + * @deprecated Use `documentAPIEndpoint` instead. */ // TODO: Remove in v8. set endpoint(value: string) { - this.apiEndpoint = value + this.documentAPIEndpoint = value } /** * The Prismic REST API V2 endpoint for the repository (use * `prismic.getRepositoryEndpoint` for the default endpoint). * - * @deprecated Use `apiEndpoint` instead. + * @deprecated Use `documentAPIEndpoint` instead. */ // TODO: Remove in v8. get endpoint(): string { - return this.apiEndpoint + return this.documentAPIEndpoint } /** @@ -383,13 +383,15 @@ export class Client< super(options) if ( - (options.apiEndpoint || isRepositoryEndpoint(repositoryNameOrEndpoint)) && + (options.documentAPIEndpoint || + isRepositoryEndpoint(repositoryNameOrEndpoint)) && process.env.NODE_ENV === "development" ) { - const apiEndpoint = options.apiEndpoint || repositoryNameOrEndpoint + const documentAPIEndpoint = + options.documentAPIEndpoint || repositoryNameOrEndpoint // Matches non-API v2 `.prismic.io` endpoints, see: https://regex101.com/r/xRsavu/1 - if (/\.prismic\.io\/(?!api\/v2\/?)/i.test(apiEndpoint)) { + if (/\.prismic\.io\/(?!api\/v2\/?)/i.test(documentAPIEndpoint)) { throw new PrismicError( "@prismicio/client only supports Prismic Rest API V2. Please provide only the repository name to the first createClient() parameter or use the getRepositoryEndpoint() helper to generate a valid Rest API V2 endpoint URL.", undefined, @@ -397,46 +399,47 @@ export class Client< ) } - const hostname = new URL(apiEndpoint).hostname.toLowerCase() + const hostname = new URL(documentAPIEndpoint).hostname.toLowerCase() // Matches non-.cdn `.prismic.io` endpoints if ( hostname.endsWith(".prismic.io") && !hostname.endsWith(".cdn.prismic.io") ) { - const repositoryName = getRepositoryName(apiEndpoint) + const repositoryName = getRepositoryName(documentAPIEndpoint) const dotCDNEndpoint = getRepositoryEndpoint(repositoryName) console.warn( - `[@prismicio/client] A non-.cdn endpoint was provided to create a client with (\`${apiEndpoint}\`). Non-.cdn endpoints can have unexpected side-effects and cause performance issues when querying Prismic. Please convert it to the \`.cdn\` alternative (\`${dotCDNEndpoint}\`) or use the repository name directly instead (\`${repositoryName}\`). For more details, see ${devMsg( + `[@prismicio/client] A non-.cdn endpoint was provided to create a client with (\`${documentAPIEndpoint}\`). Non-.cdn endpoints can have unexpected side-effects and cause performance issues when querying Prismic. Please convert it to the \`.cdn\` alternative (\`${dotCDNEndpoint}\`) or use the repository name directly instead (\`${repositoryName}\`). For more details, see ${devMsg( "endpoint-must-use-cdn", )}`, ) } - // Warn if the user provided both a repository endpoint and an `apiEndpoint` and they are different + // Warn if the user provided both a repository endpoint and an `documentAPIEndpoint` and they are different if ( - options.apiEndpoint && + options.documentAPIEndpoint && isRepositoryEndpoint(repositoryNameOrEndpoint) && - repositoryNameOrEndpoint !== options.apiEndpoint + repositoryNameOrEndpoint !== options.documentAPIEndpoint ) { console.warn( - `[@prismicio/client] A repository endpoint was provided to create a client with (\`${repositoryNameOrEndpoint}\`) along with a different \`apiEndpoint\` options. The \`apiEndpoint\` option will be preferred over the repository endpoint to query content. Please create the client using your repository name, and the \`apiEndpoint\` option if necessary, instead. For more details, see ${devMsg("prefer-repository-name")}`, + `[@prismicio/client] A repository endpoint was provided to create a client with (\`${repositoryNameOrEndpoint}\`) along with a different \`documentAPIEndpoint\` options. The \`documentAPIEndpoint\` option will be preferred over the repository endpoint to query content. Please create the client using your repository name, and the \`documentAPIEndpoint\` option if necessary, instead. For more details, see ${devMsg("prefer-repository-name")}`, ) } } if (isRepositoryEndpoint(repositoryNameOrEndpoint)) { - this.apiEndpoint = repositoryNameOrEndpoint + this.documentAPIEndpoint = repositoryNameOrEndpoint try { this.repositoryName = getRepositoryName(repositoryNameOrEndpoint) } catch (error) { console.warn( - `[@prismicio/client] Could not infer the repository name from the repository endpoint that was provided to create the client with (\`${repositoryNameOrEndpoint}\`). Methods requiring the repository name to work will be disabled. Please create the client using your repository name, and the \`apiEndpoint\` option if necessary, to ensure all methods are enabled. For more details, see ${devMsg("prefer-repository-name")}`, + `[@prismicio/client] Could not infer the repository name from the repository endpoint that was provided to create the client with (\`${repositoryNameOrEndpoint}\`). Methods requiring the repository name to work will be disabled. Please create the client using your repository name, and the \`documentAPIEndpoint\` option if necessary, to ensure all methods are enabled. For more details, see ${devMsg("prefer-repository-name")}`, ) } } else { - this.apiEndpoint = - options.apiEndpoint || getRepositoryEndpoint(repositoryNameOrEndpoint) + this.documentAPIEndpoint = + options.documentAPIEndpoint || + getRepositoryEndpoint(repositoryNameOrEndpoint) this.repositoryName = repositoryNameOrEndpoint } @@ -1147,7 +1150,7 @@ export class Client< // TODO: Restore when Authorization header support works in browsers with CORS. // return await this.fetch(this.endpoint); - const url = new URL(this.apiEndpoint) + const url = new URL(this.documentAPIEndpoint) if (this.accessToken) { url.searchParams.set("access_token", this.accessToken) @@ -1290,7 +1293,7 @@ export class Client< .integrationFieldsRef || undefined - return buildQueryURL(this.apiEndpoint, { + return buildQueryURL(this.documentAPIEndpoint, { ...this.defaultParams, ...params, ref, @@ -1737,7 +1740,7 @@ export class Client< case 404: { if (res.json === undefined) { throw new RepositoryNotFoundError( - `Prismic repository not found. Check that "${this.apiEndpoint}" is pointing to the correct repository.`, + `Prismic repository not found. Check that "${this.documentAPIEndpoint}" is pointing to the correct repository.`, url, undefined, ) diff --git a/test/__testutils__/createClient.ts b/test/__testutils__/createClient.ts index 164e5a82..c436068a 100644 --- a/test/__testutils__/createClient.ts +++ b/test/__testutils__/createClient.ts @@ -9,11 +9,11 @@ import * as prismic from "../../src" type CreateTestClientArgs = ( | { repositoryName?: string - apiEndpoint?: never + documentAPIEndpoint?: never } | { repositoryName?: never - apiEndpoint?: string + documentAPIEndpoint?: string } ) & { ctx: TaskContext @@ -25,7 +25,7 @@ export const createTestClient = ( ): prismic.Client => { const repositoryName = args.repositoryName || createRepositoryName(args.ctx) - return prismic.createClient(args.apiEndpoint || repositoryName, { + return prismic.createClient(args.documentAPIEndpoint || repositoryName, { fetch, ...args.clientConfig, }) diff --git a/test/client.test.ts b/test/client.test.ts index d36b0793..132428fa 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -155,7 +155,7 @@ it("constructor warns if a non-.cdn prismic.io endpoint is given", () => { process.env.NODE_ENV = originalNodeEnv }) -it("contructor warns if an endpoint is given along the `apiEndpoint` option and they do not match", () => { +it("contructor warns if an endpoint is given along the `documentAPIEndpoint` option and they do not match", () => { const fetch = vi.fn() const originalNodeEnv = process.env.NODE_ENV @@ -166,7 +166,7 @@ it("contructor warns if an endpoint is given along the `apiEndpoint` option and .mockImplementation(() => void 0) prismic.createClient("https://example.com/my-repo-name", { - apiEndpoint: "https://example.com/my-repo-name/prismic", + documentAPIEndpoint: "https://example.com/my-repo-name/prismic", fetch, }) expect(consoleWarnSpy).toHaveBeenNthCalledWith( @@ -176,7 +176,7 @@ it("contructor warns if an endpoint is given along the `apiEndpoint` option and consoleWarnSpy.mockClear() prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2", { - apiEndpoint: "https://example.com/my-repo-name/prismic", + documentAPIEndpoint: "https://example.com/my-repo-name/prismic", fetch, }) expect(consoleWarnSpy).toHaveBeenNthCalledWith( @@ -186,7 +186,7 @@ it("contructor warns if an endpoint is given along the `apiEndpoint` option and consoleWarnSpy.mockClear() prismic.createClient("https://example.com/my-repo-name/prismic", { - apiEndpoint: "https://example.com/my-repo-name/prismic", + documentAPIEndpoint: "https://example.com/my-repo-name/prismic", fetch, }) expect(consoleWarnSpy).toHaveBeenNthCalledWith( @@ -196,7 +196,7 @@ it("contructor warns if an endpoint is given along the `apiEndpoint` option and consoleWarnSpy.mockClear() prismic.createClient("my-repo-name", { - apiEndpoint: "https://example.com/my-repo-name/prismic", + documentAPIEndpoint: "https://example.com/my-repo-name/prismic", fetch, }) expect(consoleWarnSpy).not.toHaveBeenCalledWith( @@ -297,7 +297,7 @@ it("throws is `repositoryName` is not available but accessed", () => { const client2 = prismic.createClient("my-repo-name", { fetch, - apiEndpoint: "https://example.com/my-repo-name/prismic", + documentAPIEndpoint: "https://example.com/my-repo-name/prismic", }) expect(() => { @@ -308,7 +308,7 @@ it("throws is `repositoryName` is not available but accessed", () => { }) // TODO: Remove when alias gets removed -it("aliases `endpoint` (deprecated) to `apiEndpoint`", () => { +it("aliases `endpoint` (deprecated) to `documentAPIEndpoint`", () => { const fetch = vi.fn() const client = prismic.createClient( @@ -316,12 +316,12 @@ it("aliases `endpoint` (deprecated) to `apiEndpoint`", () => { { fetch }, ) - expect(client.apiEndpoint).toBe(client.endpoint) + expect(client.documentAPIEndpoint).toBe(client.endpoint) const otherEndpoint = "https://other-prismic-repo.cdn.prismic.io/api/v2" client.endpoint = otherEndpoint - expect(client.apiEndpoint).toBe(otherEndpoint) + expect(client.documentAPIEndpoint).toBe(otherEndpoint) }) it("uses globalThis.fetch if available", async () => { From ecce4da54956abb8ef5f3b0f91b20915aeb21871 Mon Sep 17 00:00:00 2001 From: lihbr Date: Tue, 10 Sep 2024 12:07:56 +0200 Subject: [PATCH 41/61] docs: wording Co-authored-by: Angelo Ashmore --- src/Client.ts | 10 +++------ src/WriteClient.ts | 18 ++++++++-------- src/createWriteClient.ts | 4 ++-- src/types/api/asset/asset.ts | 32 ++++++++++++++--------------- src/types/api/asset/tag.ts | 16 +++++++-------- src/types/api/migration/document.ts | 4 ++-- test/writeClient.test.ts | 2 +- 7 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index 6f0973d0..a9d4e778 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -406,12 +406,8 @@ export class Client< hostname.endsWith(".prismic.io") && !hostname.endsWith(".cdn.prismic.io") ) { - const repositoryName = getRepositoryName(documentAPIEndpoint) - const dotCDNEndpoint = getRepositoryEndpoint(repositoryName) console.warn( - `[@prismicio/client] A non-.cdn endpoint was provided to create a client with (\`${documentAPIEndpoint}\`). Non-.cdn endpoints can have unexpected side-effects and cause performance issues when querying Prismic. Please convert it to the \`.cdn\` alternative (\`${dotCDNEndpoint}\`) or use the repository name directly instead (\`${repositoryName}\`). For more details, see ${devMsg( - "endpoint-must-use-cdn", - )}`, + `[@prismicio/client] The client was created with a non-CDN endpoint. Convert it to the CDN endpoint for better performance. For more details, see ${devMsg("endpoint-must-use-cdn")}`, ) } @@ -422,7 +418,7 @@ export class Client< repositoryNameOrEndpoint !== options.documentAPIEndpoint ) { console.warn( - `[@prismicio/client] A repository endpoint was provided to create a client with (\`${repositoryNameOrEndpoint}\`) along with a different \`documentAPIEndpoint\` options. The \`documentAPIEndpoint\` option will be preferred over the repository endpoint to query content. Please create the client using your repository name, and the \`documentAPIEndpoint\` option if necessary, instead. For more details, see ${devMsg("prefer-repository-name")}`, + `[@prismicio/client] Multiple incompatible endpoints were provided. Create the client using a repository name to prevent this error. For more details, see ${devMsg("prefer-repository-name")}`, ) } } @@ -433,7 +429,7 @@ export class Client< this.repositoryName = getRepositoryName(repositoryNameOrEndpoint) } catch (error) { console.warn( - `[@prismicio/client] Could not infer the repository name from the repository endpoint that was provided to create the client with (\`${repositoryNameOrEndpoint}\`). Methods requiring the repository name to work will be disabled. Please create the client using your repository name, and the \`documentAPIEndpoint\` option if necessary, to ensure all methods are enabled. For more details, see ${devMsg("prefer-repository-name")}`, + `[@prismicio/client] A repository name could not be inferred from the provided endpoint (\`${repositoryNameOrEndpoint}\`). Some methods will be disabled. Create the client using a repository name to prevent this warning. For more details, see ${devMsg("prefer-repository-name")}`, ) } } else { diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 6edc5701..e2f73b19 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -159,7 +159,7 @@ export type MigrateReporterEvents = { }[MigrateReporterEventTypes] /** - * A query response from the Prismic asset API. The response contains pagination + * A query response from the Prismic Asset API. The response contains pagination * metadata and a list of matching results for the query. */ type GetAssetsReturnType = { @@ -244,7 +244,7 @@ const MIGRATION_API_DEMO_KEYS = [ * @returns `true` if the string is an asset tag ID, `false` otherwise. */ const isAssetTagID = (maybeAssetTagID: string): boolean => { - // Taken from @sinclair/typebox which is the uuid type checker of the asset API + // Taken from @sinclair/typebox which is the uuid type checker of the Asset API // See: https://github.com/sinclairzx81/typebox/blob/e36f5658e3a56d8c32a711aa616ec8bb34ca14b4/test/runtime/compiler/validate.ts#L15 // Tag is already a tag ID return /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test( @@ -333,11 +333,11 @@ export type WriteClientConfig = { migrationAPIKey?: string /** - * The Prismic asset API endpoint. + * The Prismic Asset API endpoint. * * @defaultValue `"https://asset-api.prismic.io/"` * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ assetAPIEndpoint?: string @@ -1102,7 +1102,7 @@ export class WriteClient< } /** - * Creates a tag in the asset API. + * Creates a tag in the Asset API. * * @remarks * Tags should be at least 3 characters long and 20 characters at most. @@ -1137,7 +1137,7 @@ export class WriteClient< } /** - * Queries existing tags from the asset API. + * Queries existing tags from the Asset API. * * @param params - Additional fetch parameters. * @@ -1238,15 +1238,15 @@ export class WriteClient< } /** - * Builds fetch parameters for the asset API. + * Builds fetch parameters for the Asset API. * * @typeParam TBody - Type of the body to send in the fetch request. * * @param params - Method, body, and additional fetch parameters. * - * @returns An object that can be fetched to interact with the asset API. + * @returns An object that can be fetched to interact with the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ private buildAssetAPIQueryParams>({ method, diff --git a/src/createWriteClient.ts b/src/createWriteClient.ts index bd6d7190..f1e61a95 100644 --- a/src/createWriteClient.ts +++ b/src/createWriteClient.ts @@ -18,12 +18,12 @@ export interface CreateWriteClient { * repository. * * @remarks - * This client only works with Node 20 or later. + * This client works in environments supporting File, Blob, and FormData, + * including Node.js 20 and later. * * @example * * ```ts - * // With a repository name. * createWriteClient("qwerty", { writeToken: "***" }) * ``` * diff --git a/src/types/api/asset/asset.ts b/src/types/api/asset/asset.ts index 24ca8b4d..6a739a00 100644 --- a/src/types/api/asset/asset.ts +++ b/src/types/api/asset/asset.ts @@ -12,9 +12,9 @@ export const AssetType = { } as const /** - * An object representing an asset returned by the asset API. + * An object representing an asset returned by the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type Asset = { /** @@ -112,9 +112,9 @@ export type Asset = { } /** - * Available query parameters when querying assets from the asset API. + * Available query parameters when querying assets from the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type GetAssetsParams = { // Pagination @@ -150,9 +150,9 @@ export type GetAssetsParams = { } /** - * An object representing the result of querying assets from the asset API. + * An object representing the result of querying assets from the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type GetAssetsResult = { items: Asset[] @@ -163,9 +163,9 @@ export type GetAssetsResult = { } /** - * Parameters for uploading an asset to the asset API. + * Parameters for uploading an asset to the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostAssetParams = { file: BlobPart @@ -175,16 +175,16 @@ export type PostAssetParams = { } /** - * Result of uploading an asset to the asset API. + * Result of uploading an asset to the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostAssetResult = Asset /** - * Parameters for updating an asset in the asset API. + * Parameters for updating an asset in the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PatchAssetParams = { notes?: string @@ -195,16 +195,16 @@ export type PatchAssetParams = { } /** - * Result of updating an asset in the asset API. + * Result of updating an asset in the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PatchAssetResult = Asset /** - * Parameters for deleting an asset from the asset API. + * Parameters for deleting an asset from the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type BulkDeleteAssetsParams = { ids: string[] diff --git a/src/types/api/asset/tag.ts b/src/types/api/asset/tag.ts index 23f8995b..4df5b551 100644 --- a/src/types/api/asset/tag.ts +++ b/src/types/api/asset/tag.ts @@ -1,7 +1,7 @@ /** - * An object representing an tag used by the asset API. + * An object representing an tag used by the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type AssetTag = { /** @@ -36,24 +36,24 @@ export type AssetTag = { } /** - * An object representing the result of querying tags from the asset API. + * An object representing the result of querying tags from the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type GetAssetTagsResult = { items: AssetTag[] } /** - * Parameters for creating a tag in the asset API. + * Parameters for creating a tag in the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostAssetTagParams = { name: string } /** - * Result of creating a tag in the asset API. + * Result of creating a tag in the Asset API. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostAssetTagResult = AssetTag diff --git a/src/types/api/migration/document.ts b/src/types/api/migration/document.ts index 27434225..656fc142 100644 --- a/src/types/api/migration/document.ts +++ b/src/types/api/migration/document.ts @@ -30,7 +30,7 @@ export type PostDocumentParams< * * @typeParam TDocument - Type of the created Prismic document. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostDocumentResult< TDocument extends PrismicDocument = PrismicDocument, @@ -73,7 +73,7 @@ export type PutDocumentParams< * * @typeParam TDocument - Type of the updated Prismic document. * - * @see Prismic asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PutDocumentResult< TDocument extends PrismicDocument = PrismicDocument, diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts index 9e793b2a..88ad8704 100644 --- a/test/writeClient.test.ts +++ b/test/writeClient.test.ts @@ -32,7 +32,7 @@ it("constructor warns if running in a browser-like environment", () => { globalThis.window = originalWindow }) -it("uses provided asset API endpoint and adds `/` suffix", () => { +it("uses provided Asset API endpoint and adds `/` suffix", () => { const client = prismic.createWriteClient("qwerty", { fetch: vi.fn(), assetAPIEndpoint: "https://example.com", From d93d23f359c5500aae76d94799593900f52fc157 Mon Sep 17 00:00:00 2001 From: lihbr Date: Tue, 10 Sep 2024 16:19:31 +0200 Subject: [PATCH 42/61] refactor: per review (API, wording, tests) Co-authored-by: Angelo Ashmore --- src/Migration.ts | 23 +- src/WriteClient.ts | 138 ++-------- src/lib/isAssetTagID.ts | 15 ++ src/lib/isMigrationField.ts | 3 +- src/lib/patchMigrationDocumentData.ts | 31 +-- src/lib/validateAssetMetadata.ts | 86 ++++++ src/types/api/asset/asset.ts | 16 +- src/types/api/asset/tag.ts | 8 +- src/types/api/migration/document.ts | 18 +- src/types/migration/document.ts | 2 +- src/types/migration/fields.ts | 1 + ...t-migrateUpdateDocuments-link.test.ts.snap | 247 +++++++++++++++++ test/__testutils__/mockPrismicAssetAPI.ts | 251 ++++++++++-------- test/__testutils__/mockPrismicMigrationAPI.ts | 120 +++++---- .../testMigrationFieldPatching.ts | 24 +- test/client.test.ts | 22 +- test/migration-createAsset.test.ts | 58 +--- test/migration-createDocument.test.ts | 28 +- test/migration-getByUID.test.ts | 4 +- test/migration-getSingle.test.ts | 4 +- test/writeClient-createAsset.test.ts | 67 ----- test/writeClient-createAssetTag.test.ts | 25 -- test/writeClient-fetchForeignAsset.test.ts | 18 +- test/writeClient-getAssets.test.ts | 42 +-- test/writeClient-migrateCreateAssets.test.ts | 4 +- ...writeClient-migrateCreateDocuments.test.ts | 10 +- ...Client-migrateUpdateDocuments-link.test.ts | 12 +- ...migrateUpdateDocuments-simpleField.test.ts | 46 ++-- test/writeClient.test.ts | 4 +- 29 files changed, 730 insertions(+), 597 deletions(-) create mode 100644 src/lib/isAssetTagID.ts create mode 100644 src/lib/validateAssetMetadata.ts diff --git a/src/Migration.ts b/src/Migration.ts index 53b72ab1..648e42d2 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -1,4 +1,5 @@ import * as is from "./lib/isMigrationField" +import { validateAssetMetadata } from "./lib/validateAssetMetadata" import type { Asset } from "./types/api/asset/asset" import type { MigrationAsset } from "./types/migration/asset" @@ -23,8 +24,6 @@ import type { AnyRegularField } from "./types/value/types" import * as isFilled from "./helpers/isFilled" -import { validateAssetMetadata } from "./WriteClient" - /** * Discovers assets in a record of Prismic fields. * @@ -116,7 +115,10 @@ type CreateAssetReturnType = ImageMigrationField & { linkToMedia: LinkToMediaMigrationField } -const SINGLE_KEY = "__SINGLE__" +/** + * The symbol used to index documents that are singletons. + */ +const SINGLE_INDEX = "__SINGLE__" /** * A helper that allows preparing your migration to Prismic. @@ -182,9 +184,7 @@ export class Migration< const filename = "name" in fileOrAsset ? fileOrAsset.name - : url.split("/").pop()?.split("_").pop() || - fileOrAsset.alt || - "unknown" + : url.split("/").pop()!.split("_").pop()! const credits = "copyright" in fileOrAsset && fileOrAsset.copyright ? fileOrAsset.copyright @@ -259,19 +259,20 @@ export class Migration< createDocument( document: ExtractMigrationDocumentType, - documentName: PrismicMigrationDocumentParams["documentName"], - params: Omit = {}, + documentTitle: PrismicMigrationDocumentParams["documentTitle"], + params: Omit = {}, ): ExtractMigrationDocumentType { this.documents.push({ document, - params: { documentName, ...params }, + params: { documentTitle, ...params }, }) // Index document if (!(document.type in this.#indexedDocuments)) { this.#indexedDocuments[document.type] = {} } - this.#indexedDocuments[document.type][document.uid || SINGLE_KEY] = document + this.#indexedDocuments[document.type][document.uid || SINGLE_INDEX] = + document // Find other assets in document discoverAssets(document.data, this.createAsset.bind(this)) @@ -298,7 +299,7 @@ export class Migration< { type: TType } > = Extract, >(documentType: TType): TMigrationDocument | undefined | undefined { - return this.#indexedDocuments[documentType]?.[SINGLE_KEY] as + return this.#indexedDocuments[documentType]?.[SINGLE_INDEX] as | TMigrationDocument | undefined } diff --git a/src/WriteClient.ts b/src/WriteClient.ts index e2f73b19..25b07a3c 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -1,4 +1,5 @@ import { devMsg } from "./lib/devMsg" +import { isAssetTagID } from "./lib/isAssetTagID" import { pLimit } from "./lib/pLimit" import type { AssetMap, DocumentMap } from "./lib/patchMigrationDocumentData" import { patchMigrationDocumentData } from "./lib/patchMigrationDocumentData" @@ -187,7 +188,7 @@ type GetAssetsReturnType = { /** * Additional parameters for creating an asset in the Prismic media library. */ -type CreateAssetParams = { +export type CreateAssetParams = { /** * Asset notes. */ @@ -209,21 +210,6 @@ type CreateAssetParams = { tags?: string[] } -/** - * Max length for asset notes accepted by the API. - */ -const ASSET_NOTES_MAX_LENGTH = 500 - -/** - * Max length for asset credits accepted by the API. - */ -const ASSET_CREDITS_MAX_LENGTH = 500 - -/** - * Max length for asset alt text accepted by the API. - */ -const ASSET_ALT_MAX_LENGTH = 500 - /** * Prismic Migration API demo keys. */ @@ -236,75 +222,6 @@ const MIGRATION_API_DEMO_KEYS = [ "CCNIlI0Vz41J66oFwsHUXaZa6NYFIY6z7aDF62Bc", ] -/** - * Checks if a string is an asset tag ID. - * - * @param maybeAssetTagID - A string that's maybe an asset tag ID. - * - * @returns `true` if the string is an asset tag ID, `false` otherwise. - */ -const isAssetTagID = (maybeAssetTagID: string): boolean => { - // Taken from @sinclair/typebox which is the uuid type checker of the Asset API - // See: https://github.com/sinclairzx81/typebox/blob/e36f5658e3a56d8c32a711aa616ec8bb34ca14b4/test/runtime/compiler/validate.ts#L15 - // Tag is already a tag ID - return /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test( - maybeAssetTagID, - ) -} - -/** - * Validates an asset's metadata, throwing an error if any of the metadata are - * invalid. - * - * @param assetMetadata - The asset metadata to validate. - * - * @internal - */ -export const validateAssetMetadata = ({ - notes, - credits, - alt, - tags, -}: CreateAssetParams): void => { - const errors: string[] = [] - - if (notes && notes.length > ASSET_NOTES_MAX_LENGTH) { - errors.push( - `\`notes\` must be at most ${ASSET_NOTES_MAX_LENGTH} characters`, - ) - } - - if (credits && credits.length > ASSET_CREDITS_MAX_LENGTH) { - errors.push( - `\`credits\` must be at most ${ASSET_CREDITS_MAX_LENGTH} characters`, - ) - } - - if (alt && alt.length > ASSET_ALT_MAX_LENGTH) { - errors.push(`\`alt\` must be at most ${ASSET_ALT_MAX_LENGTH} characters`) - } - - if ( - tags && - tags.length && - tags.some( - (tag) => !isAssetTagID(tag) && (tag.length < 3 || tag.length > 20), - ) - ) { - errors.push( - `all \`tags\`'s tag must be at least 3 characters long and 20 characters at most`, - ) - } - - if (errors.length) { - throw new PrismicError( - `Errors validating asset metadata: ${errors.join(", ")}`, - undefined, - { notes, credits, alt, tags }, - ) - } -} - /** * Configuration for clients that determine how content is queried. */ @@ -320,7 +237,7 @@ export type WriteClientConfig = { * authenticate your requests. * * @remarks - * Those keys are the same for all Prismic users. They are only useful while + * These keys are the same for all Prismic users. They are only useful while * the Migration API is in beta to reduce load. It should be one of: * * - `cSaZlfkQlF9C6CEAM2Del6MNX9WonlV86HPbeEJL` @@ -337,7 +254,7 @@ export type WriteClientConfig = { * * @defaultValue `"https://asset-api.prismic.io/"` * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ assetAPIEndpoint?: string @@ -346,7 +263,7 @@ export type WriteClientConfig = { * * @defaultValue `"https://migration.prismic.io/"` * - * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ migrationAPIEndpoint?: string } & ClientConfig @@ -416,7 +333,7 @@ export class WriteClient< * @param migration - A migration prepared with {@link createMigration}. * @param params - An event listener and additional fetch parameters. * - * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ async migrate( migration: Migration, @@ -589,7 +506,6 @@ export class WriteClient< ...fetchParams }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise> { - // Should this be a function of `Client`? // Resolve master locale const repository = await this.getRepository(fetchParams) const masterLocale = repository.languages.find((lang) => lang.is_master)!.id @@ -697,7 +613,7 @@ export class WriteClient< const { id } = await this.createDocument( // We'll upload docuements data later on. { ...document, data: {} }, - params.documentName, + params.documentTitle, { masterLanguageDocumentID, ...fetchParams, @@ -767,7 +683,7 @@ export class WriteClient< id, // We need to forward again document name and tags to update them // in case the document already existed during the previous step. - { documentName: params.documentName, uid, tags: document.tags, data }, + { documentTitle: params.documentTitle, uid, tags: document.tags, data }, fetchParams, ) } @@ -876,8 +792,6 @@ export class WriteClient< ...params }: CreateAssetParams & FetchParams = {}, ): Promise { - validateAssetMetadata({ notes, credits, alt, tags }) - const url = new URL("assets", this.assetAPIEndpoint) const formData = new FormData() @@ -935,8 +849,6 @@ export class WriteClient< ...params }: PatchAssetParams & FetchParams = {}, ): Promise { - validateAssetMetadata({ notes, credits, alt, tags }) - const url = new URL(`assets/${id}`, this.assetAPIEndpoint) // Resolve tags if any and create missing ones @@ -1024,7 +936,7 @@ export class WriteClient< private async fetchForeignAsset( url: string, params: FetchParams = {}, - ): Promise { + ): Promise { const requestInit: RequestInitLike = { ...this.fetchOptions, ...params.fetchOptions, @@ -1044,11 +956,7 @@ export class WriteClient< throw new PrismicError("Could not fetch foreign asset", url, undefined) } - const blob = await res.blob() - - return new File([blob], "", { - type: res.headers.get("content-type") || undefined, - }) + return res.blob() } /** @@ -1116,14 +1024,6 @@ export class WriteClient< name: string, params?: FetchParams, ): Promise { - if (name.length < 3 || name.length > 20) { - throw new PrismicError( - "Asset tag name must be at least 3 characters long and 20 characters at most", - undefined, - { name }, - ) - } - const url = new URL("tags", this.assetAPIEndpoint) return this.fetch( @@ -1160,18 +1060,18 @@ export class WriteClient< * @typeParam TType - Type of Prismic documents to create. * * @param document - The document data to create. - * @param documentName - The name of the document to create which will be + * @param documentTitle - The name of the document to create which will be * displayed in the editor. * @param params - Document master language document ID and additional fetch * parameters. * * @returns The ID of the created document. * - * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ private async createDocument( document: PrismicMigrationDocument>, - documentName: PrismicMigrationDocumentParams["documentName"], + documentTitle: PrismicMigrationDocumentParams["documentTitle"], { masterLanguageDocumentID, ...params @@ -1184,7 +1084,7 @@ export class WriteClient< this.buildMigrationAPIQueryParams({ method: "POST", body: { - title: documentName, + title: documentTitle, type: document.type, uid: document.uid || undefined, lang: document.lang, @@ -1208,7 +1108,7 @@ export class WriteClient< * @param document - The document data to update. * @param params - Additional fetch parameters. * - * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ private async updateDocument( id: string, @@ -1216,7 +1116,7 @@ export class WriteClient< PrismicMigrationDocument>, "uid" | "tags" | "data" > & { - documentName?: PrismicMigrationDocumentParams["documentName"] + documentTitle?: PrismicMigrationDocumentParams["documentTitle"] }, params?: FetchParams, ): Promise { @@ -1227,7 +1127,7 @@ export class WriteClient< this.buildMigrationAPIQueryParams({ method: "PUT", body: { - title: document.documentName, + title: document.documentTitle, uid: document.uid || undefined, tags: document.tags, data: document.data, @@ -1246,7 +1146,7 @@ export class WriteClient< * * @returns An object that can be fetched to interact with the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ private buildAssetAPIQueryParams>({ method, @@ -1291,7 +1191,7 @@ export class WriteClient< * * @returns An object that can be fetched to interact with the Migration API. * - * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ private buildMigrationAPIQueryParams< TBody extends PostDocumentParams | PutDocumentParams, diff --git a/src/lib/isAssetTagID.ts b/src/lib/isAssetTagID.ts new file mode 100644 index 00000000..ef4d5234 --- /dev/null +++ b/src/lib/isAssetTagID.ts @@ -0,0 +1,15 @@ +/** + * Checks if a string is an asset tag ID. + * + * @param maybeAssetTagID - A string that's maybe an asset tag ID. + * + * @returns `true` if the string is an asset tag ID, `false` otherwise. + */ +export const isAssetTagID = (maybeAssetTagID: string): boolean => { + // Taken from @sinclair/typebox which is the uuid type checker of the Asset API + // See: https://github.com/sinclairzx81/typebox/blob/e36f5658e3a56d8c32a711aa616ec8bb34ca14b4/test/runtime/compiler/validate.ts#L15 + // Tag is already a tag ID + return /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test( + maybeAssetTagID, + ) +} diff --git a/src/lib/isMigrationField.ts b/src/lib/isMigrationField.ts index a10174c2..38ab5624 100644 --- a/src/lib/isMigrationField.ts +++ b/src/lib/isMigrationField.ts @@ -107,8 +107,7 @@ export const link = ( } else if ( "type" in field && "lang" in field && - typeof field.lang === "string" && - field.id + typeof field.lang === "string" ) { // Content relationship field declared using another repository document return true diff --git a/src/lib/patchMigrationDocumentData.ts b/src/lib/patchMigrationDocumentData.ts index 6c1d520f..d99b59b7 100644 --- a/src/lib/patchMigrationDocumentData.ts +++ b/src/lib/patchMigrationDocumentData.ts @@ -247,18 +247,7 @@ const patchLink = async ( documents: DocumentMap, ): Promise => { if (link) { - if (typeof link === "function") { - const resolved = await link() - - if (resolved) { - // Documents and migration documents are indexed. - const maybeRelationship = documents.get(resolved) - - if (maybeRelationship) { - return to.contentRelationship(maybeRelationship) - } - } - } else if ("migrationType" in link) { + if ("migrationType" in link) { // Migration link field const asset = assets.get(link.id) if (asset) { @@ -290,10 +279,22 @@ const patchLink = async ( return link } } else { - const maybeRelationship = documents.get(link.id) + const resolved = typeof link === "function" ? await link() : link - if (maybeRelationship) { - return to.contentRelationship(maybeRelationship) + if (resolved) { + // Link might be represented by a document or a migration document... + let maybeRelationship = resolved.id + ? documents.get(resolved.id) + : undefined + + // ...or by a migration document + if (!maybeRelationship) { + maybeRelationship = documents.get(resolved) + } + + if (maybeRelationship) { + return to.contentRelationship(maybeRelationship) + } } } } diff --git a/src/lib/validateAssetMetadata.ts b/src/lib/validateAssetMetadata.ts new file mode 100644 index 00000000..e5ee272b --- /dev/null +++ b/src/lib/validateAssetMetadata.ts @@ -0,0 +1,86 @@ +import { PrismicError } from "../errors/PrismicError" + +import type { CreateAssetParams } from "../WriteClient" + +import { isAssetTagID } from "./isAssetTagID" + +/** + * Max length for asset notes accepted by the API. + */ +const ASSET_NOTES_MAX_LENGTH = 500 + +/** + * Max length for asset credits accepted by the API. + */ +const ASSET_CREDITS_MAX_LENGTH = 500 + +/** + * Max length for asset alt text accepted by the API. + */ +const ASSET_ALT_MAX_LENGTH = 500 + +/** + * Min length for asset tags accepted by the API. + */ +const ASSET_TAG_MIN_LENGTH = 3 + +/** + * Max length for asset tags accepted by the API. + */ +const ASSET_TAG_MAX_LENGTH = 20 + +/** + * Validates an asset's metadata, throwing an error if any of the metadata are + * invalid. + * + * @param assetMetadata - The asset metadata to validate. + * + * @internal + */ +export const validateAssetMetadata = ({ + notes, + credits, + alt, + tags, +}: CreateAssetParams): void => { + const errors: string[] = [] + + if (notes && notes.length > ASSET_NOTES_MAX_LENGTH) { + errors.push( + `\`notes\` must be at most ${ASSET_NOTES_MAX_LENGTH} characters`, + ) + } + + if (credits && credits.length > ASSET_CREDITS_MAX_LENGTH) { + errors.push( + `\`credits\` must be at most ${ASSET_CREDITS_MAX_LENGTH} characters`, + ) + } + + if (alt && alt.length > ASSET_ALT_MAX_LENGTH) { + errors.push(`\`alt\` must be at most ${ASSET_ALT_MAX_LENGTH} characters`) + } + + if ( + tags && + tags.length && + tags.some( + (tag) => + !isAssetTagID(tag) && + (tag.length < ASSET_TAG_MIN_LENGTH || + tag.length > ASSET_TAG_MAX_LENGTH), + ) + ) { + errors.push( + `tags must be at least 3 characters long and 20 characters at most`, + ) + } + + if (errors.length) { + throw new PrismicError( + `Errors validating asset metadata: ${errors.join(", ")}`, + undefined, + { notes, credits, alt, tags }, + ) + } +} diff --git a/src/types/api/asset/asset.ts b/src/types/api/asset/asset.ts index 6a739a00..5115474f 100644 --- a/src/types/api/asset/asset.ts +++ b/src/types/api/asset/asset.ts @@ -14,7 +14,7 @@ export const AssetType = { /** * An object representing an asset returned by the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type Asset = { /** @@ -114,7 +114,7 @@ export type Asset = { /** * Available query parameters when querying assets from the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type GetAssetsParams = { // Pagination @@ -152,7 +152,7 @@ export type GetAssetsParams = { /** * An object representing the result of querying assets from the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type GetAssetsResult = { items: Asset[] @@ -165,7 +165,7 @@ export type GetAssetsResult = { /** * Parameters for uploading an asset to the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostAssetParams = { file: BlobPart @@ -177,14 +177,14 @@ export type PostAssetParams = { /** * Result of uploading an asset to the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostAssetResult = Asset /** * Parameters for updating an asset in the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PatchAssetParams = { notes?: string @@ -197,14 +197,14 @@ export type PatchAssetParams = { /** * Result of updating an asset in the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PatchAssetResult = Asset /** * Parameters for deleting an asset from the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type BulkDeleteAssetsParams = { ids: string[] diff --git a/src/types/api/asset/tag.ts b/src/types/api/asset/tag.ts index 4df5b551..e89a8c03 100644 --- a/src/types/api/asset/tag.ts +++ b/src/types/api/asset/tag.ts @@ -1,7 +1,7 @@ /** * An object representing an tag used by the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type AssetTag = { /** @@ -38,14 +38,14 @@ export type AssetTag = { /** * An object representing the result of querying tags from the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type GetAssetTagsResult = { items: AssetTag[] } /** * Parameters for creating a tag in the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostAssetTagParams = { name: string @@ -54,6 +54,6 @@ export type PostAssetTagParams = { /** * Result of creating a tag in the Asset API. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostAssetTagResult = AssetTag diff --git a/src/types/api/migration/document.ts b/src/types/api/migration/document.ts index 656fc142..347854c2 100644 --- a/src/types/api/migration/document.ts +++ b/src/types/api/migration/document.ts @@ -1,4 +1,7 @@ -import type { PrismicDocument } from "../../value/document" +import type { + PrismicDocument, + PrismicDocumentWithUID, +} from "../../value/document" /** * An object representing the parameters required when creating a document @@ -6,7 +9,7 @@ import type { PrismicDocument } from "../../value/document" * * @typeParam TDocument - Type of the Prismic document to create. * - * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ export type PostDocumentParams< TDocument extends PrismicDocument = PrismicDocument, @@ -16,13 +19,14 @@ export type PostDocumentParams< title: string type: TType - uid?: string lang: TLang alternate_language_id?: string tags?: string[] data: TData | Record - } + } & (TDocument extends PrismicDocumentWithUID + ? { uid: TDocument["uid"] } + : { uid?: TDocument["uid"] }) : never /** @@ -30,7 +34,7 @@ export type PostDocumentParams< * * @typeParam TDocument - Type of the created Prismic document. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PostDocumentResult< TDocument extends PrismicDocument = PrismicDocument, @@ -53,7 +57,7 @@ export type PostDocumentResult< * * @typeParam TDocument - Type of the Prismic document to update. * - * @see Prismic Migration API technical references: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ export type PutDocumentParams< TDocument extends PrismicDocument = PrismicDocument, @@ -73,7 +77,7 @@ export type PutDocumentParams< * * @typeParam TDocument - Type of the updated Prismic document. * - * @see Prismic Asset API technical references: {@link https://prismic.io/docs/asset-api-technical-reference} + * @see Prismic Asset API technical reference: {@link https://prismic.io/docs/asset-api-technical-reference} */ export type PutDocumentResult< TDocument extends PrismicDocument = PrismicDocument, diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts index 68123ee9..4d030122 100644 --- a/src/types/migration/document.ts +++ b/src/types/migration/document.ts @@ -204,7 +204,7 @@ export type PrismicMigrationDocumentParams = { /** * Name of the document displayed in the editor. */ - documentName: string + documentTitle: string /** * A link to the master language document. diff --git a/src/types/migration/fields.ts b/src/types/migration/fields.ts index 8b99b405..6b83c53b 100644 --- a/src/types/migration/fields.ts +++ b/src/types/migration/fields.ts @@ -41,6 +41,7 @@ export type LinkToMediaMigrationField = */ export type ContentRelationshipMigrationField = | PrismicDocument + | PrismicMigrationDocument | (() => | Promise | PrismicDocument diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap b/test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap index 9bef61ca..114003d9 100644 --- a/test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap +++ b/test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap @@ -245,6 +245,253 @@ exports[`patches link fields > existing > static zone 1`] = ` } `; +exports[`patches link fields > lazyExisting > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "laoreet", + "link_type": "Document", + "tags": [], + "type": "orci", + }, + }, + ], +} +`; + +exports[`patches link fields > lazyExisting > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + }, + ], + "primary": { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "group": [ + { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > lazyExisting > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + }, + ], + "primary": { + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > lazyExisting > static zone 1`] = ` +{ + "field": { + "id": "id-existing", + "isBroken": false, + "lang": "eget", + "link_type": "Document", + "tags": [], + "type": "phasellus", + }, +} +`; + +exports[`patches link fields > lazyMigration > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "a", + "link_type": "Document", + "tags": [ + "Odio Eu", + ], + "type": "amet", + "uid": "Non sodales neque", + }, + }, + ], +} +`; + +exports[`patches link fields > lazyMigration > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + }, + ], + "primary": { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + "group": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "gravida", + "link_type": "Document", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches link fields > lazyMigration > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "ultrices", + "link_type": "Document", + "tags": [ + "Ipsum Consequat", + ], + "type": "eget", + "uid": "Amet dictum sit", + }, + }, + ], + "primary": { + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "ultrices", + "link_type": "Document", + "tags": [ + "Ipsum Consequat", + ], + "type": "eget", + "uid": "Amet dictum sit", + }, + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches link fields > lazyMigration > static zone 1`] = ` +{ + "field": { + "id": "id-migration", + "isBroken": false, + "lang": "nisi,", + "link_type": "Document", + "tags": [], + "type": "eros", + "uid": "Scelerisque mauris pellentesque", + }, +} +`; + exports[`patches link fields > migration > group 1`] = ` { "group": [ diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index 4d4a144a..23df9904 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -22,7 +22,7 @@ type MockPrismicAssetAPIArgs = { client: WriteClient writeToken?: string requiredHeaders?: Record - getRequiredParams?: Record + requiredGetParams?: Record existingAssets?: Asset[][] | number[] newAssets?: Asset[] existingTags?: AssetTag[] @@ -97,146 +97,161 @@ export const mockPrismicAssetAPI = ( } args.ctx.server.use( - rest.get(`${assetAPIEndpoint}assets`, async (req, res, ctx) => { - if ( - req.headers.get("authorization") !== `Bearer ${writeToken}` || - req.headers.get("repository") !== repositoryName - ) { - return res(ctx.status(401), ctx.json({ error: "unauthorized" })) - } - - if (args.getRequiredParams) { - for (const paramKey in args.getRequiredParams) { - const requiredValue = args.getRequiredParams[paramKey] - - args.ctx - .expect(req.url.searchParams.getAll(paramKey)) - .toStrictEqual( - Array.isArray(requiredValue) ? requiredValue : [requiredValue], - ) + rest.get( + new URL("assets", assetAPIEndpoint).toString(), + async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } - } - validateHeaders(req) + if (args.requiredGetParams) { + for (const paramKey in args.requiredGetParams) { + const requiredValue = args.requiredGetParams[paramKey] - const index = Number.parseInt(req.url.searchParams.get("cursor") ?? "0") - const items: Asset[] = assetsDatabase[index] || [] - - let missing_ids: string[] | undefined - const ids = req.url.searchParams.getAll("ids") - if (ids.length) { - missing_ids = ids.filter((id) => - assetsDatabase.flat().find((item) => item.id === id), - ) - } + args.ctx + .expect(req.url.searchParams.getAll(paramKey)) + .toStrictEqual( + Array.isArray(requiredValue) ? requiredValue : [requiredValue], + ) + } + } - const response: GetAssetsResult = { - total: items.length, - items, - is_opensearch_result: false, - cursor: assetsDatabase[index + 1] ? `${index + 1}` : undefined, - missing_ids, - } + validateHeaders(req) - return res(ctx.json(response)) - }), - rest.post(`${assetAPIEndpoint}assets`, async (req, res, ctx) => { - if ( - req.headers.get("authorization") !== `Bearer ${writeToken}` || - req.headers.get("repository") !== repositoryName - ) { - return res(ctx.status(401), ctx.json({ error: "unauthorized" })) - } + const index = Number.parseInt(req.url.searchParams.get("cursor") ?? "0") + const items: Asset[] = assetsDatabase[index] || [] - validateHeaders(req) + let missing_ids: string[] | undefined + const ids = req.url.searchParams.getAll("ids") + if (ids.length) { + missing_ids = ids.filter((id) => + assetsDatabase.flat().find((item) => item.id === id), + ) + } - const response: PostAssetResult = - args.newAssets?.shift() ?? mockAsset(args.ctx) + const response: GetAssetsResult = { + total: items.length, + items, + is_opensearch_result: false, + cursor: assetsDatabase[index + 1] ? `${index + 1}` : undefined, + missing_ids, + } - // Save the asset in DB - assetsDatabase.push([response]) + return res(ctx.json(response)) + }, + ), + rest.post( + new URL("assets", assetAPIEndpoint).toString(), + async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) + } - return res(ctx.json(response)) - }), - rest.patch(`${assetAPIEndpoint}assets/:id`, async (req, res, ctx) => { - if ( - req.headers.get("authorization") !== `Bearer ${writeToken}` || - req.headers.get("repository") !== repositoryName - ) { - return res(ctx.status(401), ctx.json({ error: "unauthorized" })) - } + validateHeaders(req) + + const response: PostAssetResult = + args.newAssets?.shift() ?? mockAsset(args.ctx) + + // Save the asset in DB + assetsDatabase.push([response]) + + return res(ctx.json(response)) + }, + ), + rest.patch( + new URL("assets/:id", assetAPIEndpoint).toString(), + async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) + } - validateHeaders(req) + validateHeaders(req) - const { tags, ...body } = await req.json() - const asset = assetsDatabase - .flat() - .find((asset) => asset.id === req.params.id) + const { tags, ...body } = await req.json() + const asset = assetsDatabase + .flat() + .find((asset) => asset.id === req.params.id) - if (!asset) { - return res(ctx.status(404), ctx.json({ error: "not found" })) - } + if (!asset) { + return res(ctx.status(404), ctx.json({ error: "not found" })) + } - const response: PostAssetResult = { - ...asset, - ...body, - tags: tags?.length - ? tagsDatabase.filter((tag) => tags.includes(tag.id)) - : asset.tags, - } + const response: PostAssetResult = { + ...asset, + ...body, + tags: tags?.length + ? tagsDatabase.filter((tag) => tags.includes(tag.id)) + : asset.tags, + } - // Update asset in DB - for (const cursor in assetsDatabase) { - for (const asset in assetsDatabase[cursor]) { - if (assetsDatabase[cursor][asset].id === req.params.id) { - assetsDatabase[cursor][asset] = response + // Update asset in DB + for (const cursor in assetsDatabase) { + for (const asset in assetsDatabase[cursor]) { + if (assetsDatabase[cursor][asset].id === req.params.id) { + assetsDatabase[cursor][asset] = response + } } } - } - - return res(ctx.json(response)) - }), - rest.get(`${assetAPIEndpoint}tags`, async (req, res, ctx) => { - if ( - req.headers.get("authorization") !== `Bearer ${writeToken}` || - req.headers.get("repository") !== repositoryName - ) { - return res(ctx.status(401), ctx.json({ error: "unauthorized" })) - } - - validateHeaders(req) - const items: AssetTag[] = tagsDatabase - const response: GetAssetTagsResult = { items } + return res(ctx.json(response)) + }, + ), + rest.get( + new URL("tags", assetAPIEndpoint).toString(), + async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) + } - return res(ctx.json(response)) - }), - rest.post(`${assetAPIEndpoint}tags`, async (req, res, ctx) => { - if ( - req.headers.get("authorization") !== `Bearer ${writeToken}` || - req.headers.get("repository") !== repositoryName - ) { - return res(ctx.status(401), ctx.json({ error: "unauthorized" })) - } + validateHeaders(req) + + const items: AssetTag[] = tagsDatabase + const response: GetAssetTagsResult = { items } + + return res(ctx.json(response)) + }, + ), + rest.post( + new URL("tags", assetAPIEndpoint).toString(), + async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) + } - validateHeaders(req) + validateHeaders(req) - const body = await req.json() - const tag: AssetTag = { - id: `${`${Date.now()}`.slice(-8)}-4444-4444-4444-121212121212`, - created_at: Date.now(), - last_modified: Date.now(), - name: body.name, - ...args.newTags?.find((tag) => tag.name === body.name), - } + const body = await req.json() + const tag: AssetTag = { + id: `${`${Date.now()}`.slice(-8)}-4444-4444-4444-121212121212`, + created_at: Date.now(), + last_modified: Date.now(), + name: body.name, + ...args.newTags?.find((tag) => tag.name === body.name), + } - // Save the tag in DB - tagsDatabase.push(tag) + // Save the tag in DB + tagsDatabase.push(tag) - const response: PostAssetTagResult = tag + const response: PostAssetTagResult = tag - return res(ctx.status(201), ctx.json(response)) - }), + return res(ctx.status(201), ctx.json(response)) + }, + ), ) return { diff --git a/test/__testutils__/mockPrismicMigrationAPI.ts b/test/__testutils__/mockPrismicMigrationAPI.ts index d1b54a1d..f81914cd 100644 --- a/test/__testutils__/mockPrismicMigrationAPI.ts +++ b/test/__testutils__/mockPrismicMigrationAPI.ts @@ -83,76 +83,82 @@ export const mockPrismicMigrationAPI = ( } args.ctx.server.use( - rest.post(`${migrationAPIEndpoint}documents`, async (req, res, ctx) => { - if ( - req.headers.get("authorization") !== `Bearer ${writeToken}` || - req.headers.get("x-api-key") !== migrationAPIKey || - req.headers.get("repository") !== repositoryName - ) { - return res(ctx.status(403), ctx.json({ Message: "forbidden" })) - } + rest.post( + new URL("documents", migrationAPIEndpoint).toString(), + async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("x-api-key") !== migrationAPIKey || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(403), ctx.json({ Message: "forbidden" })) + } - validateHeaders(req) + validateHeaders(req) - const body = await req.json() + const body = await req.json() - const newDocument = args.newDocuments?.shift() - const id = newDocument?.id || args.ctx.mock.value.document().id + const newDocument = args.newDocuments?.shift() + const id = newDocument?.id || args.ctx.mock.value.document().id - if (newDocument?.masterLanguageDocumentID) { - args.ctx - .expect(body.alternate_language_id) - .toBe(newDocument.masterLanguageDocumentID) - } + if (newDocument?.masterLanguageDocumentID) { + args.ctx + .expect(body.alternate_language_id) + .toBe(newDocument.masterLanguageDocumentID) + } - const response: PostDocumentResult = { - title: body.title, - id, - lang: body.lang, - type: body.type, - uid: body.uid, - } + const response: PostDocumentResult = { + title: body.title, + id, + lang: body.lang, + type: body.type, + uid: body.uid, + } - // Save the document in DB - documentsDatabase[id] = { ...response, data: body.data } - - return res(ctx.status(201), ctx.json(response)) - }), - rest.put(`${migrationAPIEndpoint}documents/:id`, async (req, res, ctx) => { - if ( - req.headers.get("authorization") !== `Bearer ${writeToken}` || - req.headers.get("x-api-key") !== migrationAPIKey || - req.headers.get("repository") !== repositoryName - ) { - return res(ctx.status(403), ctx.json({ Message: "forbidden" })) - } + // Save the document in DB + documentsDatabase[id] = { ...response, data: body.data } + + return res(ctx.status(201), ctx.json(response)) + }, + ), + rest.put( + new URL("documents/:id", migrationAPIEndpoint).toString(), + async (req, res, ctx) => { + if ( + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("x-api-key") !== migrationAPIKey || + req.headers.get("repository") !== repositoryName + ) { + return res(ctx.status(403), ctx.json({ Message: "forbidden" })) + } - validateHeaders(req) + validateHeaders(req) - const document = documentsDatabase[req.params.id as string] + const document = documentsDatabase[req.params.id as string] - if (!document) { - return res(ctx.status(404)) - } + if (!document) { + return res(ctx.status(404)) + } - const body = await req.json() + const body = await req.json() - const response: PutDocumentResult = { - title: body.title || document.title, - id: req.params.id as string, - lang: document.lang, - type: document.type, - uid: body.uid, - } + const response: PutDocumentResult = { + title: body.title || document.title, + id: req.params.id as string, + lang: document.lang, + type: document.type, + uid: body.uid, + } - // Update the document in DB - documentsDatabase[req.params.id as string] = { - ...response, - data: body.data, - } + // Update the document in DB + documentsDatabase[req.params.id as string] = { + ...response, + data: body.data, + } - return res(ctx.json(response)) - }), + return res(ctx.json(response)) + }, + ), ) return { documentsDatabase } diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index a51f5e01..be1a42a7 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -30,7 +30,7 @@ type GetDataArgs = { type InternalTestMigrationFieldPatchingArgs = { getData: (args: GetDataArgs) => prismic.PrismicMigrationDocument["data"] - expectStrictEqual: boolean + expectStrictEqual?: boolean } const internalTestMigrationFieldPatching = ( @@ -116,10 +116,10 @@ type TestMigrationFieldPatchingFactoryCases = Record< (args: GetDataArgs) => prismic.PrismicMigrationDocument["data"][string] > -const testMigrationFieldPatchingFactory = ( +export const testMigrationFieldPatching = ( description: string, cases: TestMigrationFieldPatchingFactoryCases, - args: Omit, + args: Omit = {}, ): void => { describe(description, () => { for (const name in cases) { @@ -180,21 +180,3 @@ const testMigrationFieldPatchingFactory = ( } }) } - -export const testMigrationSimpleFieldPatching = ( - description: string, - cases: TestMigrationFieldPatchingFactoryCases, -): void => { - testMigrationFieldPatchingFactory(description, cases, { - expectStrictEqual: true, - }) -} - -export const testMigrationFieldPatching = ( - description: string, - cases: TestMigrationFieldPatchingFactoryCases, -): void => { - testMigrationFieldPatchingFactory(description, cases, { - expectStrictEqual: false, - }) -} diff --git a/test/client.test.ts b/test/client.test.ts index 132428fa..ac9bf7e9 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -10,7 +10,7 @@ import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" import * as prismic from "../src" -it("createClient creates a Client", () => { +it("creates a Client with `createClient`", () => { const client = prismic.createClient("qwerty", { fetch: vi.fn(), }) @@ -36,7 +36,7 @@ it("creates a client with a Rest API V2 endpoint", () => { expect(client.endpoint).toBe(endpoint) }) -it("client has correct default state", () => { +it("has correct default state", () => { const endpoint = prismic.getRepositoryEndpoint("qwerty") const options: prismic.ClientConfig = { accessToken: "accessToken", @@ -54,7 +54,7 @@ it("client has correct default state", () => { expect(client.defaultParams).toBe(options.defaultParams) }) -it("constructor throws if an invalid repository name is provided", () => { +it("throws in constructor if an invalid repository name is provided", () => { expect(() => { prismic.createClient("invalid repository name", { fetch: vi.fn(), @@ -67,7 +67,7 @@ it("constructor throws if an invalid repository name is provided", () => { }).toThrowError(prismic.PrismicError) }) -it("constructor throws if an invalid repository endpoint is provided", () => { +it("throws in constructor if an invalid repository endpoint is provided", () => { expect(() => { prismic.createClient("https://invalid url.cdn.prismic.io/api/v2", { fetch: vi.fn(), @@ -80,7 +80,7 @@ it("constructor throws if an invalid repository endpoint is provided", () => { }).toThrowError(prismic.PrismicError) }) -it("constructor throws if a prismic.io endpoint is given that is not for Rest API V2", () => { +it("throws in constructor if a prismic.io endpoint is given that is not for Rest API V2", () => { const fetch = vi.fn() const originalNodeEnv = process.env.NODE_ENV @@ -114,7 +114,7 @@ it("constructor throws if a prismic.io endpoint is given that is not for Rest AP process.env.NODE_ENV = originalNodeEnv }) -it("constructor warns if a non-.cdn prismic.io endpoint is given", () => { +it("warns in constructor if a non-.cdn prismic.io endpoint is given", () => { const fetch = vi.fn() const originalNodeEnv = process.env.NODE_ENV @@ -155,7 +155,7 @@ it("constructor warns if a non-.cdn prismic.io endpoint is given", () => { process.env.NODE_ENV = originalNodeEnv }) -it("contructor warns if an endpoint is given along the `documentAPIEndpoint` option and they do not match", () => { +it("warns in constructor if an endpoint is given along the `documentAPIEndpoint` option and they do not match", () => { const fetch = vi.fn() const originalNodeEnv = process.env.NODE_ENV @@ -208,7 +208,7 @@ it("contructor warns if an endpoint is given along the `documentAPIEndpoint` opt process.env.NODE_ENV = originalNodeEnv }) -it("contructor warns if an endpoint is given and the repository name could not be inferred", () => { +it("warns in constructor if an endpoint is given and the repository name could not be inferred", () => { const fetch = vi.fn() const consoleWarnSpy = vi @@ -231,7 +231,7 @@ it("contructor warns if an endpoint is given and the repository name could not b consoleWarnSpy.mockRestore() }) -it("constructor throws if fetch is unavailable", () => { +it("throws in constructor if fetch is unavailable", () => { const endpoint = prismic.getRepositoryEndpoint("qwerty") const originalFetch = globalThis.fetch @@ -248,7 +248,7 @@ it("constructor throws if fetch is unavailable", () => { globalThis.fetch = originalFetch }) -it("constructor throws if provided fetch is not a function", () => { +it("throws in constructor if provided fetch is not a function", () => { const endpoint = prismic.getRepositoryEndpoint("qwerty") const fetch = "not a function" @@ -279,7 +279,7 @@ it("constructor throws if provided fetch is not a function", () => { globalThis.fetch = originalFetch }) -it("throws is `repositoryName` is not available but accessed", () => { +it("throws if `repositoryName` is not available but accessed", () => { const fetch = vi.fn() const consoleWarnSpy = vi diff --git a/test/migration-createAsset.test.ts b/test/migration-createAsset.test.ts index e22da05d..16c42ec8 100644 --- a/test/migration-createAsset.test.ts +++ b/test/migration-createAsset.test.ts @@ -101,44 +101,6 @@ it("creates an asset from an image field", () => { alt: image.alt, credits: image.copyright, }) - - const image2: prismic.FilledImageFieldImage = { - ...image, - id: "bar", - url: "https://example.com/", - } - - migration.createAsset(image2) - - expect( - migration.assets.get(image2.id), - "uses alt as filename fallback when filename cannot be inferred from URL", - ).toEqual({ - id: image2.id, - file: image2.url, - filename: image2.alt, - alt: image2.alt, - credits: image2.copyright, - }) - - const image3: prismic.FilledImageFieldImage = { - ...image, - id: "qux", - alt: "", - url: "https://example.com/", - } - - migration.createAsset(image3) - - expect( - migration.assets.get(image3.id), - "uses default filename when filename cannot be inferred from URL and `alt` is not available", - ).toEqual({ - id: image3.id, - file: image3.url, - filename: "unknown", - credits: image3.copyright, - }) }) it("creates an asset from a link to media field", () => { @@ -172,32 +134,26 @@ it("throws if asset has invalid metadata", () => { expect(() => { migration.createAsset(file, filename, { notes: "0".repeat(501) }) - }, "notes").toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: \`notes\` must be at most 500 characters]`, - ) + }, "notes").toThrowError(/`notes` must be at most 500 characters/i) expect(() => { migration.createAsset(file, filename, { credits: "0".repeat(501) }) - }, "credits").toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: \`credits\` must be at most 500 characters]`, - ) + }, "credits").toThrowError(/`credits` must be at most 500 characters/i) expect(() => { migration.createAsset(file, filename, { alt: "0".repeat(501) }) - }, "alt").toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: \`alt\` must be at most 500 characters]`, - ) + }, "alt").toThrowError(/`alt` must be at most 500 characters/i) expect(() => { migration.createAsset(file, filename, { tags: ["0"] }) - }, "tags").toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, + }, "tags").toThrowError( + /tags must be at least 3 characters long and 20 characters at most/i, ) expect(() => { migration.createAsset(file, filename, { tags: ["0".repeat(21)] }) - }, "tags").toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, + }, "tags").toThrowError( + /tags must be at least 3 characters long and 20 characters at most/i, ) expect(() => { diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index 63d533f8..311bd8f9 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -14,13 +14,13 @@ it("creates a document", () => { lang: "lang", data: {}, } - const documentName = "documentName" + const documentTitle = "documentTitle" - migration.createDocument(document, documentName) + migration.createDocument(document, documentTitle) expect(migration.documents[0]).toStrictEqual({ document, - params: { documentName }, + params: { documentTitle }, }) }) @@ -28,13 +28,13 @@ it("creates a document from an existing Prismic document", (ctx) => { const migration = prismic.createMigration() const document = ctx.mock.value.document() - const documentName = "documentName" + const documentTitle = "documentTitle" - migration.createDocument(document, documentName) + migration.createDocument(document, documentTitle) expect(migration.documents[0]).toStrictEqual({ document, - params: { documentName }, + params: { documentTitle }, }) }) @@ -191,11 +191,11 @@ describe.each<{ lang: "lang", data: { field }, } - const documentName = "documentName" + const documentTitle = "documentTitle" expect(migration.assets.size).toBe(0) - migration.createDocument(document, documentName) + migration.createDocument(document, documentTitle) expect(migration.assets.get(id)).toStrictEqual(expected) }) @@ -211,11 +211,11 @@ describe.each<{ lang: "lang", data: { group: [{ field }] }, } - const documentName = "documentName" + const documentTitle = "documentTitle" expect(migration.assets.size).toBe(0) - migration.createDocument(document, documentName) + migration.createDocument(document, documentTitle) expect(migration.assets.get(id)).toStrictEqual(expected) }) @@ -241,11 +241,11 @@ describe.each<{ lang: "lang", data: { slices }, } - const documentName = "documentName" + const documentTitle = "documentTitle" expect(migration.assets.size).toBe(0) - migration.createDocument(document, documentName) + migration.createDocument(document, documentTitle) expect(migration.assets.get(id)).toStrictEqual(expected) }) @@ -271,11 +271,11 @@ describe.each<{ lang: "lang", data: { slices }, } - const documentName = "documentName" + const documentTitle = "documentTitle" expect(migration.assets.size).toBe(0) - migration.createDocument(document, documentName) + migration.createDocument(document, documentTitle) expect(migration.assets.get(id)).toStrictEqual(expected) }) diff --git a/test/migration-getByUID.test.ts b/test/migration-getByUID.test.ts index 015d7dce..0ac4d3ad 100644 --- a/test/migration-getByUID.test.ts +++ b/test/migration-getByUID.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest" import * as prismic from "../src" -it("returns indexed document", () => { +it("returns a document with a matching UID", () => { const migration = prismic.createMigration() const document = { @@ -20,7 +20,7 @@ it("returns indexed document", () => { ) }) -it("returns `undefined` is document is not found", () => { +it("returns `undefined` if a document is not found", () => { const migration = prismic.createMigration() const document = { diff --git a/test/migration-getSingle.test.ts b/test/migration-getSingle.test.ts index 19b301bc..d2ff7359 100644 --- a/test/migration-getSingle.test.ts +++ b/test/migration-getSingle.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest" import * as prismic from "../src" -it("returns indexed document", () => { +it("returns a document of a given singleton type", () => { const migration = prismic.createMigration() const document = { @@ -17,7 +17,7 @@ it("returns indexed document", () => { expect(migration.getSingle(document.type)).toStrictEqual(document) }) -it("returns `undefined` is document is not found", () => { +it("returns `undefined` if a document is not found", () => { const migration = prismic.createMigration() const document = { diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts index b5f5d213..5e8fb3b1 100644 --- a/test/writeClient-createAsset.test.ts +++ b/test/writeClient-createAsset.test.ts @@ -112,73 +112,6 @@ it.concurrent("creates an asset with a new tag name", async (ctx) => { expect(asset.tags?.[0].id).toEqual(tag.id) }) -it.concurrent("throws if asset has invalid metadata", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - mockPrismicAssetAPI({ ctx, client }) - - const filename = "foo.jpg" - const file = "https://example.com/foo.jpg" - - await expect( - () => - // @ts-expect-error - testing purposes - client.createAsset(file, filename, { notes: "0".repeat(501) }), - "notes", - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: \`notes\` must be at most 500 characters]`, - ) - - await expect( - () => - // @ts-expect-error - testing purposes - client.createAsset(file, filename, { credits: "0".repeat(501) }), - "credits", - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: \`credits\` must be at most 500 characters]`, - ) - - await expect( - () => - // @ts-expect-error - testing purposes - client.createAsset(file, filename, { alt: "0".repeat(501) }), - "alt", - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: \`alt\` must be at most 500 characters]`, - ) - - await expect( - () => - // @ts-expect-error - testing purposes - client.createAsset(file, filename, { tags: ["0"] }), - "tags", - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, - ) - - await expect( - () => - // @ts-expect-error - testing purposes - client.createAsset(file, filename, { tags: ["0".repeat(21)] }), - "tags", - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, - ) - - await expect( - // @ts-expect-error - testing purposes - client.createAsset(file, filename, { - tags: [ - // Tag name - "012", - // Tag ID - "00000000-4444-4444-4444-121212121212", - ], - }), - "tags", - ).resolves.not.toBeUndefined() -}) - it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-createAssetTag.test.ts b/test/writeClient-createAssetTag.test.ts index 78c532c1..674773f0 100644 --- a/test/writeClient-createAssetTag.test.ts +++ b/test/writeClient-createAssetTag.test.ts @@ -29,31 +29,6 @@ it.concurrent("creates a tag", async (ctx) => { expect(tag).toStrictEqual(newTag) }) -it.concurrent("throws if tag is invalid", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - mockPrismicAssetAPI({ ctx, client }) - - await expect(() => - // @ts-expect-error - testing purposes - client.createAssetTag("0"), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Asset tag name must be at least 3 characters long and 20 characters at most]`, - ) - - await expect(() => - // @ts-expect-error - testing purposes - client.createAssetTag("0".repeat(21)), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Asset tag name must be at least 3 characters long and 20 characters at most]`, - ) - - await expect( - // @ts-expect-error - testing purposes - client.createAssetTag("valid"), - ).resolves.not.toBeUndefined() -}) - it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-fetchForeignAsset.test.ts b/test/writeClient-fetchForeignAsset.test.ts index 551194c2..10249ec8 100644 --- a/test/writeClient-fetchForeignAsset.test.ts +++ b/test/writeClient-fetchForeignAsset.test.ts @@ -16,14 +16,14 @@ it.concurrent("fetches a foreign asset with content type", async (ctx) => { ctx.server.use( rest.get(url, (_req, res, ctx) => { - return res(ctx.text("foo")) + return res(ctx.set("content-type", "image/png"), ctx.body("foo")) }), ) // @ts-expect-error - testing purposes - const file = await client.fetchForeignAsset(url) + const blob = await client.fetchForeignAsset(url) - expect(file.type).toBe("text/plain") + expect(blob.type).toBe("image/png") }) it.concurrent("fetches a foreign asset with no content type", async (ctx) => { @@ -38,9 +38,9 @@ it.concurrent("fetches a foreign asset with no content type", async (ctx) => { ) // @ts-expect-error - testing purposes - const file = await client.fetchForeignAsset(url) + const blob = await client.fetchForeignAsset(url) - expect(file.type).toBe("") + expect(blob.type).toBe("") }) it.concurrent("is abortable with an AbortController", async (ctx) => { @@ -101,11 +101,11 @@ it.concurrent("supports custom headers", async (ctx) => { ) // @ts-expect-error - testing purposes - const file = await client.fetchForeignAsset(url, { + const blob = await client.fetchForeignAsset(url, { fetchOptions: { headers }, }) - ctx.expect(file.type).toBe("text/plain") + ctx.expect(blob.type).toBe("text/plain") ctx.expect.assertions(2) }) @@ -127,8 +127,8 @@ it.concurrent("supports global custom headers", async (ctx) => { ) // @ts-expect-error - testing purposes - const file = await client.fetchForeignAsset(url) + const blob = await client.fetchForeignAsset(url) - ctx.expect(file.type).toBe("text/plain") + ctx.expect(blob.type).toBe("text/plain") ctx.expect.assertions(2) }) diff --git a/test/writeClient-getAssets.test.ts b/test/writeClient-getAssets.test.ts index e2c2de4c..e16dd854 100644 --- a/test/writeClient-getAssets.test.ts +++ b/test/writeClient-getAssets.test.ts @@ -26,7 +26,7 @@ it.concurrent("supports `pageSize` parameter", async (ctx) => { mockPrismicAssetAPI({ ctx, client, - getRequiredParams: { + requiredGetParams: { pageSize: "10", }, }) @@ -42,13 +42,13 @@ it.concurrent("supports `pageSize` parameter", async (ctx) => { it.concurrent("supports `cursor` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const getRequiredParams = { + const requiredGetParams = { cursor: "foo", } - mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + mockPrismicAssetAPI({ ctx, client, requiredGetParams }) - const { results } = await client.getAssets(getRequiredParams) + const { results } = await client.getAssets(requiredGetParams) ctx.expect(results).toBeInstanceOf(Array) ctx.expect.assertions(2) @@ -57,13 +57,13 @@ it.concurrent("supports `cursor` parameter", async (ctx) => { it.concurrent("supports `assetType` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const getRequiredParams = { + const requiredGetParams = { assetType: AssetType.Image, } - mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + mockPrismicAssetAPI({ ctx, client, requiredGetParams }) - const { results } = await client.getAssets(getRequiredParams) + const { results } = await client.getAssets(requiredGetParams) ctx.expect(results).toBeInstanceOf(Array) ctx.expect.assertions(2) @@ -72,13 +72,13 @@ it.concurrent("supports `assetType` parameter", async (ctx) => { it.concurrent("supports `keyword` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const getRequiredParams = { + const requiredGetParams = { keyword: "foo", } - mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + mockPrismicAssetAPI({ ctx, client, requiredGetParams }) - const { results } = await client.getAssets(getRequiredParams) + const { results } = await client.getAssets(requiredGetParams) ctx.expect(results).toBeInstanceOf(Array) ctx.expect.assertions(2) @@ -87,13 +87,13 @@ it.concurrent("supports `keyword` parameter", async (ctx) => { it.concurrent("supports `ids` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const getRequiredParams = { + const requiredGetParams = { ids: ["foo", "bar"], } - mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + mockPrismicAssetAPI({ ctx, client, requiredGetParams }) - const { results } = await client.getAssets(getRequiredParams) + const { results } = await client.getAssets(requiredGetParams) ctx.expect(results).toBeInstanceOf(Array) ctx.expect.assertions(2) @@ -102,7 +102,7 @@ it.concurrent("supports `ids` parameter", async (ctx) => { it.concurrent("supports `tags` parameter (id)", async (ctx) => { const client = createTestWriteClient({ ctx }) - const getRequiredParams = { + const requiredGetParams = { tags: [ "00000000-4444-4444-4444-121212121212", "10000000-4444-4444-4444-121212121212", @@ -126,10 +126,10 @@ it.concurrent("supports `tags` parameter (id)", async (ctx) => { last_modified: 0, }, ], - getRequiredParams, + requiredGetParams, }) - const { results } = await client.getAssets(getRequiredParams) + const { results } = await client.getAssets(requiredGetParams) ctx.expect(results).toBeInstanceOf(Array) ctx.expect.assertions(2) @@ -138,7 +138,7 @@ it.concurrent("supports `tags` parameter (id)", async (ctx) => { it.concurrent("supports `tags` parameter (name)", async (ctx) => { const client = createTestWriteClient({ ctx }) - const getRequiredParams = { + const requiredGetParams = { tags: [ "00000000-4444-4444-4444-121212121212", "10000000-4444-4444-4444-121212121212", @@ -162,7 +162,7 @@ it.concurrent("supports `tags` parameter (name)", async (ctx) => { last_modified: 0, }, ], - getRequiredParams, + requiredGetParams, }) const { results } = await client.getAssets({ tags: ["foo", "bar"] }) @@ -174,7 +174,7 @@ it.concurrent("supports `tags` parameter (name)", async (ctx) => { it.concurrent("supports `tags` parameter (missing)", async (ctx) => { const client = createTestWriteClient({ ctx }) - const getRequiredParams = { + const requiredGetParams = { tags: ["00000000-4444-4444-4444-121212121212"], } @@ -189,7 +189,7 @@ it.concurrent("supports `tags` parameter (missing)", async (ctx) => { last_modified: 0, }, ], - getRequiredParams, + requiredGetParams, }) const { results } = await client.getAssets({ tags: ["foo", "bar"] }) @@ -213,7 +213,7 @@ it.concurrent("returns `next` when next `cursor` is available", async (ctx) => { ctx.expect(next2).toBeInstanceOf(Function) - mockPrismicAssetAPI({ ctx, client, getRequiredParams: { cursor: "1" } }) + mockPrismicAssetAPI({ ctx, client, requiredGetParams: { cursor: "1" } }) const { results } = await next2!() ctx.expect(results).toBeInstanceOf(Array) diff --git a/test/writeClient-migrateCreateAssets.test.ts b/test/writeClient-migrateCreateAssets.test.ts index c3417200..39b3d3b1 100644 --- a/test/writeClient-migrateCreateAssets.test.ts +++ b/test/writeClient-migrateCreateAssets.test.ts @@ -382,9 +382,7 @@ it.concurrent( await expect(() => client.migrate(migration, { reporter }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Could not fetch foreign asset]`, - ) + ).rejects.toThrowError(/could not fetch foreign asset/i) expect(reporter).toHaveBeenLastCalledWith({ type: "assets:creating", diff --git a/test/writeClient-migrateCreateDocuments.test.ts b/test/writeClient-migrateCreateDocuments.test.ts index f5507c2e..28ce988d 100644 --- a/test/writeClient-migrateCreateDocuments.test.ts +++ b/test/writeClient-migrateCreateDocuments.test.ts @@ -108,7 +108,7 @@ it.concurrent("skips creating existing documents", async (ctx) => { total: 1, document, documentParams: { - documentName: "foo", + documentTitle: "foo", }, }, }) @@ -158,7 +158,7 @@ it.concurrent("creates new documents", async (ctx) => { total: 1, document, documentParams: { - documentName: "foo", + documentTitle: "foo", }, }, }) @@ -204,7 +204,7 @@ it.concurrent( total: 1, document, documentParams: { - documentName: "foo", + documentTitle: "foo", masterLanguageDocument, }, }, @@ -261,7 +261,7 @@ it.concurrent( total: 1, document, documentParams: { - documentName: "foo", + documentTitle: "foo", masterLanguageDocument: expect.any(Function), }, }, @@ -372,7 +372,7 @@ it.concurrent( total: 1, document, documentParams: { - documentName: "foo", + documentTitle: "foo", }, }, }) diff --git a/test/writeClient-migrateUpdateDocuments-link.test.ts b/test/writeClient-migrateUpdateDocuments-link.test.ts index 9c5a6e19..51cb64ef 100644 --- a/test/writeClient-migrateUpdateDocuments-link.test.ts +++ b/test/writeClient-migrateUpdateDocuments-link.test.ts @@ -4,7 +4,17 @@ import { RichTextNodeType } from "../src" testMigrationFieldPatching("patches link fields", { existing: ({ existingDocuments }) => existingDocuments[0], - migration: ({ migration, migrationDocuments }) => { + migration: ({ migrationDocuments }) => { + delete migrationDocuments[0].id + + return migrationDocuments[0] + }, + lazyExisting: ({ existingDocuments }) => { + return () => existingDocuments[0] + }, + lazyMigration: ({ migration, migrationDocuments }) => { + delete migrationDocuments[0].id + return () => migration.getByUID(migrationDocuments[0].type, migrationDocuments[0].uid) }, diff --git a/test/writeClient-migrateUpdateDocuments-simpleField.test.ts b/test/writeClient-migrateUpdateDocuments-simpleField.test.ts index fce0c932..0c771e9f 100644 --- a/test/writeClient-migrateUpdateDocuments-simpleField.test.ts +++ b/test/writeClient-migrateUpdateDocuments-simpleField.test.ts @@ -1,22 +1,26 @@ -import { testMigrationSimpleFieldPatching } from "./__testutils__/testMigrationFieldPatching" +import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" -testMigrationSimpleFieldPatching("does not patch simple fields", { - embed: ({ ctx }) => ctx.mock.value.embed({ state: "filled" }), - date: ({ ctx }) => ctx.mock.value.date({ state: "filled" }), - timestamp: ({ ctx }) => ctx.mock.value.timestamp({ state: "filled" }), - color: ({ ctx }) => ctx.mock.value.color({ state: "filled" }), - number: ({ ctx }) => ctx.mock.value.number({ state: "filled" }), - keyText: ({ ctx }) => ctx.mock.value.keyText({ state: "filled" }), - richTextSimple: ({ ctx }) => - ctx.mock.value.richText({ state: "filled", pattern: "long" }), - select: ({ ctx }) => - ctx.mock.value.select({ - model: ctx.mock.model.select({ options: ["foo", "bar"] }), - state: "filled", - }), - boolean: ({ ctx }) => ctx.mock.value.boolean(), - geoPoint: ({ ctx }) => ctx.mock.value.geoPoint({ state: "filled" }), - integration: ({ ctx }) => ctx.mock.value.integration({ state: "filled" }), - linkToWeb: ({ ctx }) => - ctx.mock.value.link({ type: "Web", withTargetBlank: true }), -}) +testMigrationFieldPatching( + "does not patch simple fields", + { + embed: ({ ctx }) => ctx.mock.value.embed({ state: "filled" }), + date: ({ ctx }) => ctx.mock.value.date({ state: "filled" }), + timestamp: ({ ctx }) => ctx.mock.value.timestamp({ state: "filled" }), + color: ({ ctx }) => ctx.mock.value.color({ state: "filled" }), + number: ({ ctx }) => ctx.mock.value.number({ state: "filled" }), + keyText: ({ ctx }) => ctx.mock.value.keyText({ state: "filled" }), + richTextSimple: ({ ctx }) => + ctx.mock.value.richText({ state: "filled", pattern: "long" }), + select: ({ ctx }) => + ctx.mock.value.select({ + model: ctx.mock.model.select({ options: ["foo", "bar"] }), + state: "filled", + }), + boolean: ({ ctx }) => ctx.mock.value.boolean(), + geoPoint: ({ ctx }) => ctx.mock.value.geoPoint({ state: "filled" }), + integration: ({ ctx }) => ctx.mock.value.integration({ state: "filled" }), + linkToWeb: ({ ctx }) => + ctx.mock.value.link({ type: "Web", withTargetBlank: true }), + }, + { expectStrictEqual: true }, +) diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts index 88ad8704..b599e30b 100644 --- a/test/writeClient.test.ts +++ b/test/writeClient.test.ts @@ -2,7 +2,7 @@ import { expect, it, vi } from "vitest" import * as prismic from "../src" -it("`createWriteClient` creates a write client", () => { +it("creates a write client with `createWriteClient`", () => { const client = prismic.createWriteClient("qwerty", { fetch: vi.fn(), writeToken: "xxx", @@ -11,7 +11,7 @@ it("`createWriteClient` creates a write client", () => { expect(client).toBeInstanceOf(prismic.WriteClient) }) -it("constructor warns if running in a browser-like environment", () => { +it("warns constructor in if running in a browser-like environment", () => { const originalWindow = globalThis.window globalThis.window = {} as Window & typeof globalThis From 83ccf1f92f12f9685e5679de8d2bbb243e08eaaa Mon Sep 17 00:00:00 2001 From: lihbr Date: Tue, 10 Sep 2024 18:07:05 +0200 Subject: [PATCH 43/61] fix: stronger `is.richText` helper --- src/lib/isMigrationField.ts | 38 +++++++++++++++---- ...migrateUpdateDocuments-simpleField.test.ts | 1 + 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/lib/isMigrationField.ts b/src/lib/isMigrationField.ts index 38ab5624..320f1fa6 100644 --- a/src/lib/isMigrationField.ts +++ b/src/lib/isMigrationField.ts @@ -13,7 +13,7 @@ import type { GroupField } from "../types/value/group" import type { ImageField } from "../types/value/image" import type { LinkField } from "../types/value/link" import { LinkType } from "../types/value/link" -import type { RichTextField } from "../types/value/richText" +import { type RichTextField, RichTextNodeType } from "../types/value/richText" import type { SliceZone } from "../types/value/sliceZone" import type { AnyRegularField } from "../types/value/types" @@ -54,12 +54,36 @@ export const richText = ( ): field is RichTextFieldToMigrationField => { return ( Array.isArray(field) && - field.every( - (item) => - ("type" in item && typeof item.type === "string") || - ("migrationType" in item && - item.migrationType === MigrationFieldType.Image), - ) + field.every((item) => { + if ( + "migrationType" in item && + item.migrationType === MigrationFieldType.Image + ) { + return true + } else if ("type" in item && typeof item.type === "string") { + switch (item.type) { + case RichTextNodeType.heading1: + case RichTextNodeType.heading2: + case RichTextNodeType.heading3: + case RichTextNodeType.heading4: + case RichTextNodeType.heading5: + case RichTextNodeType.heading6: + case RichTextNodeType.paragraph: + case RichTextNodeType.preformatted: + case RichTextNodeType.listItem: + case RichTextNodeType.oListItem: + return "spans" in item && Array.isArray(item.spans) + + case RichTextNodeType.image: + return "dimensions" in item + + case RichTextNodeType.embed: + return "oembed" in item + } + } + + return false + }) ) } diff --git a/test/writeClient-migrateUpdateDocuments-simpleField.test.ts b/test/writeClient-migrateUpdateDocuments-simpleField.test.ts index 0c771e9f..d21619f3 100644 --- a/test/writeClient-migrateUpdateDocuments-simpleField.test.ts +++ b/test/writeClient-migrateUpdateDocuments-simpleField.test.ts @@ -21,6 +21,7 @@ testMigrationFieldPatching( integration: ({ ctx }) => ctx.mock.value.integration({ state: "filled" }), linkToWeb: ({ ctx }) => ctx.mock.value.link({ type: "Web", withTargetBlank: true }), + richTextMisleadingGroup: () => [{ type: "paragraph" }], }, { expectStrictEqual: true }, ) From 143943bbedf589660f09046291daca5a124fbee9 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 11 Sep 2024 11:42:27 +0200 Subject: [PATCH 44/61] refactor: per review (API, tests) --- src/Migration.ts | 12 +- src/WriteClient.ts | 123 +- src/createClient.ts | 3 +- src/createMigration.ts | 3 +- src/createWriteClient.ts | 3 +- src/getRepositoryName.ts | 22 +- src/lib/isAssetTagID.ts | 15 - src/lib/validateAssetMetadata.ts | 6 +- ...teClient-migrate-patch-image.test.ts.snap} | 0 ...iteClient-migrate-patch-link.test.ts.snap} | 0 ...nt-migrate-patch-linkToMedia.test.ts.snap} | 0 ...migrateUpdateDocuments-images.test.ts.snap | 1800 ----------------- test/__testutils__/mockPrismicAssetAPI.ts | 13 - test/__testutils__/mockPrismicMigrationAPI.ts | 3 + test/getRepositoryName.test.ts | 15 +- test/migration-createAsset.test.ts | 27 +- test/migration-createDocument.test.ts | 20 +- test/writeClient-createAsset.test.ts | 21 +- test/writeClient-getAssets.test.ts | 222 +- ....ts => writeClient-migrate-assets.test.ts} | 54 +- ... => writeClient-migrate-documents.test.ts} | 0 ...> writeClient-migrate-patch-image.test.ts} | 0 ...=> writeClient-migrate-patch-link.test.ts} | 0 ...eClient-migrate-patch-linkToMedia.test.ts} | 0 ...eClient-migrate-patch-simpleField.test.ts} | 0 test/writeClient-migrate.test.ts | 108 +- test/writeClient.test.ts | 2 +- 27 files changed, 397 insertions(+), 2075 deletions(-) delete mode 100644 src/lib/isAssetTagID.ts rename test/__snapshots__/{writeClient-migrateUpdateDocuments-image.test.ts.snap => writeClient-migrate-patch-image.test.ts.snap} (100%) rename test/__snapshots__/{writeClient-migrateUpdateDocuments-link.test.ts.snap => writeClient-migrate-patch-link.test.ts.snap} (100%) rename test/__snapshots__/{writeClient-migrateUpdateDocuments-linkToMedia.test.ts.snap => writeClient-migrate-patch-linkToMedia.test.ts.snap} (100%) delete mode 100644 test/__snapshots__/writeClient-migrateUpdateDocuments-images.test.ts.snap rename test/{writeClient-migrateCreateAssets.test.ts => writeClient-migrate-assets.test.ts} (88%) rename test/{writeClient-migrateCreateDocuments.test.ts => writeClient-migrate-documents.test.ts} (100%) rename test/{writeClient-migrateUpdateDocuments-image.test.ts => writeClient-migrate-patch-image.test.ts} (100%) rename test/{writeClient-migrateUpdateDocuments-link.test.ts => writeClient-migrate-patch-link.test.ts} (100%) rename test/{writeClient-migrateUpdateDocuments-linkToMedia.test.ts => writeClient-migrate-patch-linkToMedia.test.ts} (100%) rename test/{writeClient-migrateUpdateDocuments-simpleField.test.ts => writeClient-migrate-patch-simpleField.test.ts} (100%) diff --git a/src/Migration.ts b/src/Migration.ts index 648e42d2..35b989b1 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -134,7 +134,7 @@ export class Migration< /** * @internal */ - documents: { + _documents: { document: TMigrationDocuments params: PrismicMigrationDocumentParams }[] = [] @@ -143,7 +143,7 @@ export class Migration< /** * @internal */ - assets: Map = new Map() + _assets: Map = new Map() createAsset( asset: Asset | FilledImageFieldImage | FilledLinkToMediaField, @@ -226,11 +226,11 @@ export class Migration< validateAssetMetadata(asset) - const maybeAsset = this.assets.get(asset.id) + const maybeAsset = this._assets.get(asset.id) if (maybeAsset) { // Consolidate existing asset with new asset value if possible - this.assets.set(asset.id, { + this._assets.set(asset.id, { ...maybeAsset, notes: asset.notes || maybeAsset.notes, credits: asset.credits || maybeAsset.credits, @@ -240,7 +240,7 @@ export class Migration< ), }) } else { - this.assets.set(asset.id, asset) + this._assets.set(asset.id, asset) } return { @@ -262,7 +262,7 @@ export class Migration< documentTitle: PrismicMigrationDocumentParams["documentTitle"], params: Omit = {}, ): ExtractMigrationDocumentType { - this.documents.push({ + this._documents.push({ document, params: { documentTitle, ...params }, }) diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 25b07a3c..be5934e8 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -1,5 +1,4 @@ import { devMsg } from "./lib/devMsg" -import { isAssetTagID } from "./lib/isAssetTagID" import { pLimit } from "./lib/pLimit" import type { AssetMap, DocumentMap } from "./lib/patchMigrationDocumentData" import { patchMigrationDocumentData } from "./lib/patchMigrationDocumentData" @@ -232,20 +231,11 @@ export type WriteClientConfig = { writeToken: string /** - * An explicit Prismic Migration API key that allows working with the - * Migration API. If none is provided, the client will pick a random one to - * authenticate your requests. + * A Prismic Migration API key that allows working with the Migration API. * * @remarks - * These keys are the same for all Prismic users. They are only useful while - * the Migration API is in beta to reduce load. It should be one of: - * - * - `cSaZlfkQlF9C6CEAM2Del6MNX9WonlV86HPbeEJL` - * - `pZCexCajUQ4jriYwIGSxA1drZrFxDyFf1S0D1K0P` - * - `Yc0mfrkGDw8gaaGKTrzwC3QUZDajv6k73DA99vWN` - * - `ySzSEbVMAb5S1oSCQfbVG4mbh9Cb8wlF7BCvKI0L` - * - `g2DA3EKWvx8uxVYcNFrmT5nJpon1Vi9V4XcOibJD` - * - `CCNIlI0Vz41J66oFwsHUXaZa6NYFIY6z7aDF62Bc` + * If no key is provided, the client will use one of the demo key available + * which has stricter rate limiting rules enforced. */ migrationAPIKey?: string @@ -345,8 +335,8 @@ export class WriteClient< type: "start", data: { pending: { - documents: migration.documents.length, - assets: migration.assets.size, + documents: migration._documents.length, + assets: migration._assets.size, }, }, }) @@ -361,8 +351,8 @@ export class WriteClient< type: "end", data: { migrated: { - documents: migration.documents.length, - assets: migration.assets.size, + documents: migration._documents.length, + assets: migration._assets.size, }, }, }) @@ -414,7 +404,7 @@ export class WriteClient< // Create assets let i = 0 let created = 0 - for (const [_, migrationAsset] of migration.assets) { + for (const [_, migrationAsset] of migration._assets) { if ( typeof migrationAsset.id === "string" && assets.has(migrationAsset.id) @@ -424,8 +414,8 @@ export class WriteClient< data: { reason: "already exists", current: ++i, - remaining: migration.assets.size - i, - total: migration.assets.size, + remaining: migration._assets.size - i, + total: migration._assets.size, asset: migrationAsset, }, }) @@ -435,8 +425,8 @@ export class WriteClient< type: "assets:creating", data: { current: ++i, - remaining: migration.assets.size - i, - total: migration.assets.size, + remaining: migration._assets.size - i, + total: migration._assets.size, asset: migrationAsset, }, }) @@ -538,7 +528,7 @@ export class WriteClient< // We create an array with non-master locale documents last because // we need their master locale document to be created first. - for (const { document, params } of migration.documents) { + for (const { document, params } of migration._documents) { if (document.lang === masterLocale) { sortedDocuments.unshift({ document, params }) } else { @@ -660,13 +650,13 @@ export class WriteClient< }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise { let i = 0 - for (const { document, params } of migration.documents) { + for (const { document, params } of migration._documents) { reporter?.({ type: "documents:updating", data: { current: ++i, - remaining: migration.documents.length - i, - total: migration.documents.length, + remaining: migration._documents.length - i, + total: migration._documents.length, document, documentParams: params, }, @@ -691,7 +681,7 @@ export class WriteClient< reporter?.({ type: "documents:updated", data: { - updated: migration.documents.length, + updated: migration._documents.length, }, }) } @@ -703,19 +693,20 @@ export class WriteClient< * * @returns A paginated response containing the result of the query. */ - async getAssets({ + private async getAssets({ pageSize, cursor, - assetType, - keyword, - ids, - tags, + // assetType, + // keyword, + // ids, + // tags, ...params - }: GetAssetsParams & FetchParams = {}): Promise { + }: Pick & + FetchParams = {}): Promise { // Resolve tags if any - if (tags && tags.length) { - tags = await this.resolveAssetTagIDs(tags, params) - } + // if (tags && tags.length) { + // tags = await this.resolveAssetTagIDs(tags, params) + // } const url = new URL("assets", this.assetAPIEndpoint) @@ -727,26 +718,26 @@ export class WriteClient< url.searchParams.set("cursor", cursor) } - if (assetType) { - url.searchParams.set("assetType", assetType) - } + // if (assetType) { + // url.searchParams.set("assetType", assetType) + // } - if (keyword) { - url.searchParams.set("keyword", keyword) - } + // if (keyword) { + // url.searchParams.set("keyword", keyword) + // } - if (ids) { - ids.forEach((id) => url.searchParams.append("ids", id)) - } + // if (ids) { + // ids.forEach((id) => url.searchParams.append("ids", id)) + // } - if (tags) { - tags.forEach((tag) => url.searchParams.append("tags", tag)) - } + // if (tags) { + // tags.forEach((tag) => url.searchParams.append("tags", tag)) + // } const { items, total, - missing_ids, + // missing_ids, cursor: nextCursor, } = await this.fetch( url.toString(), @@ -756,16 +747,16 @@ export class WriteClient< return { results: items, total_results_size: total, - missing_ids: missing_ids || [], + // missing_ids: missing_ids || [], next: nextCursor ? () => this.getAssets({ pageSize, cursor: nextCursor, - assetType, - keyword, - ids, - tags, + // assetType, + // keyword, + // ids, + // tags, ...params, }) : undefined, @@ -965,16 +956,16 @@ export class WriteClient< private _resolveAssetTagIDsLimit = pLimit() /** - * Resolves asset tag IDs from tag names or IDs. + * Resolves asset tag IDs from tag names. * - * @param tagNamesOrIDs - An array of tag names or IDs to resolve. + * @param tagNames - An array of tag names to resolve. * @param params - Whether or not missing tags should be created and * additional fetch parameters. * * @returns An array of resolved tag IDs. */ private async resolveAssetTagIDs( - tagNamesOrIDs: string[] = [], + tagNames: string[] = [], { createTags, ...params }: { createTags?: boolean } & FetchParams = {}, ): Promise { return this._resolveAssetTagIDsLimit(async () => { @@ -985,23 +976,15 @@ export class WriteClient< } const resolvedTagIDs = [] - for (const tagNameOrID of tagNamesOrIDs) { - if (isAssetTagID(tagNameOrID)) { - resolvedTagIDs.push(tagNameOrID) - continue - } - + for (const tagName of tagNames) { // Tag does not exists yet, we create it if `createTags` is set - if (!existingTagMap[tagNameOrID] && createTags) { - existingTagMap[tagNameOrID] = await this.createAssetTag( - tagNameOrID, - params, - ) + if (!existingTagMap[tagName] && createTags) { + existingTagMap[tagName] = await this.createAssetTag(tagName, params) } // Add tag if found - if (existingTagMap[tagNameOrID]) { - resolvedTagIDs.push(existingTagMap[tagNameOrID].id) + if (existingTagMap[tagName]) { + resolvedTagIDs.push(existingTagMap[tagName].id) } } diff --git a/src/createClient.ts b/src/createClient.ts index 4d64b703..a3c16f9c 100644 --- a/src/createClient.ts +++ b/src/createClient.ts @@ -26,8 +26,7 @@ export interface CreateClient { * createClient("https://qwerty.cdn.prismic.io/api/v2") * ``` * - * @typeParam TDocuments - A map of Prismic document type IDs mapped to their - * TypeScript type. + * @typeParam TDocuments - A union of Prismic document types for the repository. * * @param repositoryNameOrEndpoint - The Prismic repository name or full Rest * API V2 endpoint for the repository. diff --git a/src/createMigration.ts b/src/createMigration.ts index 3fc63125..b12df736 100644 --- a/src/createMigration.ts +++ b/src/createMigration.ts @@ -22,8 +22,7 @@ export interface CreateMigration { * createMigration() * ``` * - * @typeParam TDocuments - A map of Prismic document type IDs mapped to their - * TypeScript type. + * @typeParam TDocuments - A union of Prismic document types for the repository. * * @returns A migration instance to prepare your migration. */ diff --git a/src/createWriteClient.ts b/src/createWriteClient.ts index f1e61a95..d713050f 100644 --- a/src/createWriteClient.ts +++ b/src/createWriteClient.ts @@ -27,8 +27,7 @@ export interface CreateWriteClient { * createWriteClient("qwerty", { writeToken: "***" }) * ``` * - * @typeParam TDocuments - A map of Prismic document type IDs mapped to their - * TypeScript type. + * @typeParam TDocuments - A union of Prismic document types for the repository. * * @param repositoryName - The Prismic repository name for the repository. * @param options - Configuration that determines how content will be queried diff --git a/src/getRepositoryName.ts b/src/getRepositoryName.ts index a78c19f9..74d04ce8 100644 --- a/src/getRepositoryName.ts +++ b/src/getRepositoryName.ts @@ -1,31 +1,31 @@ import { PrismicError } from "./errors/PrismicError" /** - * Get a Prismic repository's name from its standard Prismic Rest API V2 or + * Get a Prismic repository's name from its standard Prismic Document API or * GraphQL endpoint. * - * @typeParam RepositoryEndpoint - Prismic Rest API V2 endpoint for the - * repository. - * - * @param repositoryEndpoint - Prismic Rest API V2 endpoint for the repository. + * @param repositoryEndpoint - Prismic Document API endpoint for the repository. * * @returns The Prismic repository's name. * - * @throws {@link Error} Thrown if an invalid Prismic Rest API V2 endpoint is + * @throws {@link Error} Thrown if an invalid Prismic Document API endpoint is * provided. */ export const getRepositoryName = (repositoryEndpoint: string): string => { try { - const parts = new URL(repositoryEndpoint).hostname.split(".") + const hostname = new URL(repositoryEndpoint).hostname - // [subdomain, domain, tld] - if (parts.length > 2) { - return parts[0] + if ( + hostname.endsWith("prismic.io") || // Production + hostname.endsWith("wroom.io") || // Staging + hostname.endsWith("wroom.test") // Dev + ) { + return hostname.split(".")[0] } } catch {} throw new PrismicError( - `An invalid Prismic Rest API V2 endpoint was provided: ${repositoryEndpoint}`, + `An invalid Prismic Document API endpoint was provided: ${repositoryEndpoint}`, undefined, undefined, ) diff --git a/src/lib/isAssetTagID.ts b/src/lib/isAssetTagID.ts deleted file mode 100644 index ef4d5234..00000000 --- a/src/lib/isAssetTagID.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Checks if a string is an asset tag ID. - * - * @param maybeAssetTagID - A string that's maybe an asset tag ID. - * - * @returns `true` if the string is an asset tag ID, `false` otherwise. - */ -export const isAssetTagID = (maybeAssetTagID: string): boolean => { - // Taken from @sinclair/typebox which is the uuid type checker of the Asset API - // See: https://github.com/sinclairzx81/typebox/blob/e36f5658e3a56d8c32a711aa616ec8bb34ca14b4/test/runtime/compiler/validate.ts#L15 - // Tag is already a tag ID - return /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test( - maybeAssetTagID, - ) -} diff --git a/src/lib/validateAssetMetadata.ts b/src/lib/validateAssetMetadata.ts index e5ee272b..19720f8d 100644 --- a/src/lib/validateAssetMetadata.ts +++ b/src/lib/validateAssetMetadata.ts @@ -2,8 +2,6 @@ import { PrismicError } from "../errors/PrismicError" import type { CreateAssetParams } from "../WriteClient" -import { isAssetTagID } from "./isAssetTagID" - /** * Max length for asset notes accepted by the API. */ @@ -66,9 +64,7 @@ export const validateAssetMetadata = ({ tags.length && tags.some( (tag) => - !isAssetTagID(tag) && - (tag.length < ASSET_TAG_MIN_LENGTH || - tag.length > ASSET_TAG_MAX_LENGTH), + tag.length < ASSET_TAG_MIN_LENGTH || tag.length > ASSET_TAG_MAX_LENGTH, ) ) { errors.push( diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-image.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap similarity index 100% rename from test/__snapshots__/writeClient-migrateUpdateDocuments-image.test.ts.snap rename to test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-link.test.ts.snap similarity index 100% rename from test/__snapshots__/writeClient-migrateUpdateDocuments-link.test.ts.snap rename to test/__snapshots__/writeClient-migrate-patch-link.test.ts.snap diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-linkToMedia.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap similarity index 100% rename from test/__snapshots__/writeClient-migrateUpdateDocuments-linkToMedia.test.ts.snap rename to test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap diff --git a/test/__snapshots__/writeClient-migrateUpdateDocuments-images.test.ts.snap b/test/__snapshots__/writeClient-migrateUpdateDocuments-images.test.ts.snap deleted file mode 100644 index daa3e7f0..00000000 --- a/test/__snapshots__/writeClient-migrateUpdateDocuments-images.test.ts.snap +++ /dev/null @@ -1,1800 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`patches image fields > existing > group 1`] = ` -{ - "group": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", - }, - }, - ], -} -`; - -exports[`patches image fields > existing > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - }, - ], - "primary": { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - "group": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > existing > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - }, - ], - "primary": { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > existing > static zone 1`] = ` -{ - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", - }, -} -`; - -exports[`patches image fields > new > group 1`] = ` -{ - "group": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "ef95d5daa4d", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", - }, - }, - ], -} -`; - -exports[`patches image fields > new > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - }, - ], - "primary": { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - "group": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > new > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "45306297c5e", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - }, - ], - "primary": { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "45306297c5e", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > new > static zone 1`] = ` -{ - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "c5c95f8d3ac", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, -} -`; - -exports[`patches image fields > otherRepository > group 1`] = ` -{ - "group": [ - { - "field": { - "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", - "copyright": null, - "dimensions": { - "height": 1705, - "width": 2560, - }, - "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, - }, - "id": "4dcf07cfc26", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", - }, - }, - ], -} -`; - -exports[`patches image fields > otherRepository > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", - }, - }, - ], - "primary": { - "field": { - "alt": "Ut consequat semper viverra nam libero justo laoreet", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", - }, - "group": [ - { - "field": { - "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > otherRepository > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, - }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c", - }, - }, - ], - "primary": { - "field": { - "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, - }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > otherRepository > static zone 1`] = ` -{ - "field": { - "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, - }, - "id": "9abffab1d17", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f", - }, -} -`; - -exports[`patches image fields > otherRepositoryEmpty > group 1`] = ` -{ - "group": [ - { - "field": {}, - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryEmpty > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": {}, - }, - ], - "primary": { - "field": {}, - "group": [ - { - "field": {}, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryEmpty > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": {}, - }, - ], - "primary": { - "field": {}, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryEmpty > static zone 1`] = ` -{ - "field": {}, -} -`; - -exports[`patches image fields > otherRepositoryWithThumbnails > group 1`] = ` -{ - "group": [ - { - "field": { - "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", - "copyright": null, - "dimensions": { - "height": 1705, - "width": 2560, - }, - "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, - }, - "id": "4dcf07cfc26", - "square": { - "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", - "copyright": null, - "dimensions": { - "height": 1705, - "width": 2560, - }, - "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, - }, - "id": "4dcf07cfc26", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", - }, - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, - }, - "id": "f5dbf0d9ed2", - "square": { - "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", - }, - }, - ], - "primary": { - "field": { - "alt": "Ut consequat semper viverra nam libero justo laoreet", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, - }, - "id": "f5dbf0d9ed2", - "square": { - "alt": "Ut consequat semper viverra nam libero justo laoreet", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", - }, - "group": [ - { - "field": { - "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, - }, - "id": "f5dbf0d9ed2", - "square": { - "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, - }, - "id": "4f4a69ff72d", - "square": { - "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, - }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", - }, - }, - ], - "primary": { - "field": { - "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, - }, - "id": "4f4a69ff72d", - "square": { - "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, - }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryWithThumbnails > static zone 1`] = ` -{ - "field": { - "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, - }, - "id": "9abffab1d17", - "square": { - "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, - }, - "id": "9abffab1d17", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", - }, -} -`; - -exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = ` -{ - "group": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1705, - "width": 2560, - }, - "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, - }, - "id": "4dcf07cfc26", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1705, - "width": 2560, - }, - "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, - }, - "id": "4dcf07cfc26", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", - }, - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, - }, - "id": "f5dbf0d9ed2", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", - }, - }, - ], - "primary": { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, - }, - "id": "f5dbf0d9ed2", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", - }, - "group": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, - }, - "id": "f5dbf0d9ed2", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, - }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, - }, - "id": "4f4a69ff72d", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, - }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", - }, - }, - ], - "primary": { - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, - }, - "id": "4f4a69ff72d", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, - }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone 1`] = ` -{ - "field": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, - }, - "id": "9abffab1d17", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 2832, - "width": 4240, - }, - "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, - }, - "id": "9abffab1d17", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", - }, - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", - }, -} -`; - -exports[`patches image fields > richTextExisting > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", - }, - ], - }, - ], -} -`; - -exports[`patches image fields > richTextExisting > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Eu Mi", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - ], - "group": [ - { - "field": [ - { - "spans": [], - "text": "Arcu", - "type": "heading1", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > richTextExisting > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - ], - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > richTextExisting > static zone 1`] = ` -{ - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", - }, - ], -} -`; - -exports[`patches image fields > richTextNew > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "cdfdd322ca9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", - }, - ], - }, - ], -} -`; - -exports[`patches image fields > richTextNew > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Eu Mi", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - "group": [ - { - "field": [ - { - "spans": [], - "text": "Arcu", - "type": "heading1", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > richTextNew > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "7963dc361cc", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "7963dc361cc", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > richTextNew > static zone 1`] = ` -{ - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "0bbad670dad", - "type": "image", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - }, - ], -} -`; - -exports[`patches image fields > richTextOtherRepository > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", - }, - { - "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#7cfc26", - "x": 2977, - "y": -1163, - "zoom": 1.0472585898934068, - }, - "id": "e8d0985c099", - "type": "image", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", - }, - ], - }, - ], -} -`; - -exports[`patches image fields > richTextOtherRepository > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Facilisis", - "type": "heading3", - }, - { - "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#37ae3f", - "x": -1505, - "y": 902, - "zoom": 1.8328975606320652, - }, - "id": "3fc0dfa9fe9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", - }, - { - "alt": "Proin libero nunc consequat interdum varius", - "copyright": null, - "dimensions": { - "height": 3036, - "width": 4554, - }, - "edit": { - "background": "#904f4f", - "x": 1462, - "y": 1324, - "zoom": 1.504938844941775, - }, - "id": "3fc0dfa9fe9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - "group": [ - { - "field": [ - { - "spans": [], - "text": "Egestas Integer Eget Aliquet Nibh", - "type": "heading5", - }, - { - "alt": "Arcu cursus vitae congue mauris", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#5f7a9b", - "x": -119, - "y": -2667, - "zoom": 1.9681315715350518, - }, - "id": "3fc0dfa9fe9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > richTextOtherRepository > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Amet", - "type": "heading5", - }, - { - "alt": "Auctor neque vitae tempus quam", - "copyright": null, - "dimensions": { - "height": 1440, - "width": 2560, - }, - "edit": { - "background": "#5bc5aa", - "x": -1072, - "y": -281, - "zoom": 1.3767766101231744, - }, - "id": "f70ca27104d", - "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", - }, - { - "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", - "copyright": null, - "dimensions": { - "height": 3036, - "width": 4554, - }, - "edit": { - "background": "#4860cb", - "x": 280, - "y": -379, - "zoom": 1.2389796902982004, - }, - "id": "f70ca27104d", - "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", - }, - ], - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > richTextOtherRepository > static zone 1`] = ` -{ - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", - }, - { - "alt": "Interdum velit euismod in pellentesque", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#ab1d17", - "x": 3605, - "y": 860, - "zoom": 1.9465488211593005, - }, - "id": "04a95cc61c3", - "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", - }, - ], -} -`; diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index 23df9904..02187d2b 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -22,7 +22,6 @@ type MockPrismicAssetAPIArgs = { client: WriteClient writeToken?: string requiredHeaders?: Record - requiredGetParams?: Record existingAssets?: Asset[][] | number[] newAssets?: Asset[] existingTags?: AssetTag[] @@ -107,18 +106,6 @@ export const mockPrismicAssetAPI = ( return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } - if (args.requiredGetParams) { - for (const paramKey in args.requiredGetParams) { - const requiredValue = args.requiredGetParams[paramKey] - - args.ctx - .expect(req.url.searchParams.getAll(paramKey)) - .toStrictEqual( - Array.isArray(requiredValue) ? requiredValue : [requiredValue], - ) - } - } - validateHeaders(req) const index = Number.parseInt(req.url.searchParams.get("cursor") ?? "0") diff --git a/test/__testutils__/mockPrismicMigrationAPI.ts b/test/__testutils__/mockPrismicMigrationAPI.ts index f81914cd..25e1d275 100644 --- a/test/__testutils__/mockPrismicMigrationAPI.ts +++ b/test/__testutils__/mockPrismicMigrationAPI.ts @@ -98,6 +98,9 @@ export const mockPrismicMigrationAPI = ( const body = await req.json() + // New document is used when we need to have predictable document ID + // It's also used when we need to test for correct `alternate_language_id` + // because the API doesn't return it in the response. const newDocument = args.newDocuments?.shift() const id = newDocument?.id || args.ctx.mock.value.document().id diff --git a/test/getRepositoryName.test.ts b/test/getRepositoryName.test.ts index 8f97db0a..6341f5e0 100644 --- a/test/getRepositoryName.test.ts +++ b/test/getRepositoryName.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest" import * as prismic from "../src" -it("returns the repository name from a valid Prismic Rest API V2 endpoint", () => { +it("returns the repository name from a valid Prismic Document API endpoint", () => { const repositoryName = prismic.getRepositoryName( "https://qwerty.cdn.prismic.io/api/v2", ) @@ -10,11 +10,22 @@ it("returns the repository name from a valid Prismic Rest API V2 endpoint", () = expect(repositoryName).toBe("qwerty") }) +it("throws if the input is not a valid Document API endpoint", () => { + expect(() => { + prismic.getRepositoryName("https://example.com") + }).toThrowError( + /An invalid Prismic Document API endpoint was provided: https:\/\/example\.com/i, + ) + expect(() => { + prismic.getRepositoryName("https://example.com") + }).toThrowError(prismic.PrismicError) +}) + it("throws if the input is not a valid URL", () => { expect(() => { prismic.getRepositoryName("qwerty") }).toThrowError( - /An invalid Prismic Rest API V2 endpoint was provided: qwerty/i, + /An invalid Prismic Document API endpoint was provided: qwerty/i, ) expect(() => { prismic.getRepositoryName("qwerty") diff --git a/test/migration-createAsset.test.ts b/test/migration-createAsset.test.ts index 16c42ec8..ce3749e5 100644 --- a/test/migration-createAsset.test.ts +++ b/test/migration-createAsset.test.ts @@ -17,7 +17,7 @@ it("creates an asset from a url", () => { migration.createAsset(file, filename) - expect(migration.assets.get(file)).toEqual({ + expect(migration._assets.get(file)).toEqual({ id: file, file, filename, @@ -32,7 +32,7 @@ it("creates an asset from a file", () => { migration.createAsset(file, filename) - expect(migration.assets.get(file)).toEqual({ + expect(migration._assets.get(file)).toEqual({ id: file, file, filename, @@ -69,7 +69,7 @@ it("creates an asset from an existing asset", () => { migration.createAsset(asset) - expect(migration.assets.get(asset.id)).toStrictEqual({ + expect(migration._assets.get(asset.id)).toStrictEqual({ id: asset.id, file: asset.url, filename: asset.filename, @@ -94,7 +94,7 @@ it("creates an asset from an image field", () => { migration.createAsset(image) - expect(migration.assets.get(image.id)).toEqual({ + expect(migration._assets.get(image.id)).toEqual({ id: image.id, file: image.url, filename: image.url.split("/").pop(), @@ -119,7 +119,7 @@ it("creates an asset from a link to media field", () => { migration.createAsset(link) - expect(migration.assets.get(link.id)).toEqual({ + expect(migration._assets.get(link.id)).toEqual({ id: link.id, file: link.url, filename: link.name, @@ -157,14 +157,7 @@ it("throws if asset has invalid metadata", () => { ) expect(() => { - migration.createAsset(file, filename, { - tags: [ - // Tag name - "012", - // Tag ID - "123e4567-e89b-12d3-a456-426614174000", - ], - }) + migration.createAsset(file, filename, { tags: ["012"] }) }, "tags").not.toThrowError() }) @@ -176,7 +169,7 @@ it("consolidates existing assets with additional metadata", () => { migration.createAsset(file, filename) - expect(migration.assets.get(file)).toStrictEqual({ + expect(migration._assets.get(file)).toStrictEqual({ id: file, file, filename, @@ -193,7 +186,7 @@ it("consolidates existing assets with additional metadata", () => { tags: ["tag"], }) - expect(migration.assets.get(file)).toStrictEqual({ + expect(migration._assets.get(file)).toStrictEqual({ id: file, file, filename, @@ -210,7 +203,7 @@ it("consolidates existing assets with additional metadata", () => { tags: ["tag", "tag 2"], }) - expect(migration.assets.get(file)).toStrictEqual({ + expect(migration._assets.get(file)).toStrictEqual({ id: file, file, filename, @@ -222,7 +215,7 @@ it("consolidates existing assets with additional metadata", () => { migration.createAsset(file, filename) - expect(migration.assets.get(file)).toStrictEqual({ + expect(migration._assets.get(file)).toStrictEqual({ id: file, file, filename, diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index 311bd8f9..df7b98dc 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -18,7 +18,7 @@ it("creates a document", () => { migration.createDocument(document, documentTitle) - expect(migration.documents[0]).toStrictEqual({ + expect(migration._documents[0]).toStrictEqual({ document, params: { documentTitle }, }) @@ -32,7 +32,7 @@ it("creates a document from an existing Prismic document", (ctx) => { migration.createDocument(document, documentTitle) - expect(migration.documents[0]).toStrictEqual({ + expect(migration._documents[0]).toStrictEqual({ document, params: { documentTitle }, }) @@ -193,11 +193,11 @@ describe.each<{ } const documentTitle = "documentTitle" - expect(migration.assets.size).toBe(0) + expect(migration._assets.size).toBe(0) migration.createDocument(document, documentTitle) - expect(migration.assets.get(id)).toStrictEqual(expected) + expect(migration._assets.get(id)).toStrictEqual(expected) }) it("group fields", ({ mock }) => { @@ -213,11 +213,11 @@ describe.each<{ } const documentTitle = "documentTitle" - expect(migration.assets.size).toBe(0) + expect(migration._assets.size).toBe(0) migration.createDocument(document, documentTitle) - expect(migration.assets.get(id)).toStrictEqual(expected) + expect(migration._assets.get(id)).toStrictEqual(expected) }) it("slice's primary zone", ({ mock }) => { @@ -243,11 +243,11 @@ describe.each<{ } const documentTitle = "documentTitle" - expect(migration.assets.size).toBe(0) + expect(migration._assets.size).toBe(0) migration.createDocument(document, documentTitle) - expect(migration.assets.get(id)).toStrictEqual(expected) + expect(migration._assets.get(id)).toStrictEqual(expected) }) it("slice's repeatable zone", ({ mock }) => { @@ -273,10 +273,10 @@ describe.each<{ } const documentTitle = "documentTitle" - expect(migration.assets.size).toBe(0) + expect(migration._assets.size).toBe(0) migration.createDocument(document, documentTitle) - expect(migration.assets.get(id)).toStrictEqual(expected) + expect(migration._assets.get(id)).toStrictEqual(expected) }) }) diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts index 5e8fb3b1..cc05bdd0 100644 --- a/test/writeClient-createAsset.test.ts +++ b/test/writeClient-createAsset.test.ts @@ -49,29 +49,10 @@ it.concurrent("creates an asset with metadata", async (ctx) => { notes: "baz", }) + // TODO: Check that data are properly passed when we update MSW and get FormData support expect(asset.id).toBeTypeOf("string") }) -it.concurrent("creates an asset with an existing tag ID", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const tag: AssetTag = { - id: "00000000-4444-4444-4444-121212121212", - name: "foo", - created_at: 0, - last_modified: 0, - } - - mockPrismicAssetAPI({ ctx, client, existingTags: [tag] }) - - // @ts-expect-error - testing purposes - const asset = await client.createAsset("file", "foo.jpg", { - tags: [tag.id], - }) - - expect(asset.tags?.[0].id).toEqual(tag.id) -}) - it.concurrent("creates an asset with an existing tag name", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-getAssets.test.ts b/test/writeClient-getAssets.test.ts index e16dd854..34186fe4 100644 --- a/test/writeClient-getAssets.test.ts +++ b/test/writeClient-getAssets.test.ts @@ -13,141 +13,139 @@ const it = _it.skipIf(isNode16) it.concurrent("get assets", async (ctx) => { const client = createTestWriteClient({ ctx }) - mockPrismicAssetAPI({ ctx, client }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [2], + }) + // @ts-expect-error - testing purposes const { results } = await client.getAssets() - expect(results).toBeInstanceOf(Array) + expect(results).toStrictEqual(assetsDatabase[0]) }) it.concurrent("supports `pageSize` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - mockPrismicAssetAPI({ + const { assetsDatabase } = mockPrismicAssetAPI({ ctx, client, - requiredGetParams: { - pageSize: "10", - }, + existingAssets: [2], }) + ctx.server.events.on("request:start", (req) => { + if (req.url.hostname.startsWith(client.repositoryName)) { + ctx.expect(req.url.searchParams.get("pageSize")).toBe("10") + } + }) + + // @ts-expect-error - testing purposes const { results } = await client.getAssets({ pageSize: 10, }) - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[0]) ctx.expect.assertions(2) }) it.concurrent("supports `cursor` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const requiredGetParams = { - cursor: "foo", - } - - mockPrismicAssetAPI({ ctx, client, requiredGetParams }) - - const { results } = await client.getAssets(requiredGetParams) - - ctx.expect(results).toBeInstanceOf(Array) - ctx.expect.assertions(2) -}) - -it.concurrent("supports `assetType` parameter", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const requiredGetParams = { - assetType: AssetType.Image, - } + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [2, 2], + }) - mockPrismicAssetAPI({ ctx, client, requiredGetParams }) + ctx.server.events.on("request:start", (req) => { + if (req.url.hostname.startsWith(client.repositoryName)) { + ctx.expect(req.url.searchParams.get("cursor")).toBe("1") + } + }) - const { results } = await client.getAssets(requiredGetParams) + // @ts-expect-error - testing purposes + const { results } = await client.getAssets({ cursor: "1" }) - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[1]) ctx.expect.assertions(2) }) -it.concurrent("supports `keyword` parameter", async (ctx) => { +it.concurrent.skip("supports `assetType` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const requiredGetParams = { - keyword: "foo", - } + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [2], + }) - mockPrismicAssetAPI({ ctx, client, requiredGetParams }) + ctx.server.events.on("request:start", (req) => { + if (req.url.hostname.startsWith(client.repositoryName)) { + ctx.expect(req.url.searchParams.get("assetType")).toBe(AssetType.Image) + } + }) - const { results } = await client.getAssets(requiredGetParams) + // @ts-expect-error - testing purposes + const { results } = await client.getAssets({ assetType: AssetType.Image }) - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[0]) ctx.expect.assertions(2) }) -it.concurrent("supports `ids` parameter", async (ctx) => { +it.concurrent.skip("supports `keyword` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const requiredGetParams = { - ids: ["foo", "bar"], - } + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [2], + }) - mockPrismicAssetAPI({ ctx, client, requiredGetParams }) + ctx.server.events.on("request:start", (req) => { + if (req.url.hostname.startsWith(client.repositoryName)) { + ctx.expect(req.url.searchParams.get("keyword")).toBe("foo") + } + }) - const { results } = await client.getAssets(requiredGetParams) + // @ts-expect-error - testing purposes + const { results } = await client.getAssets({ keyword: "foo" }) - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[0]) ctx.expect.assertions(2) }) -it.concurrent("supports `tags` parameter (id)", async (ctx) => { +it.concurrent.skip("supports `ids` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const requiredGetParams = { - tags: [ - "00000000-4444-4444-4444-121212121212", - "10000000-4444-4444-4444-121212121212", - ], - } - - mockPrismicAssetAPI({ + const { assetsDatabase } = mockPrismicAssetAPI({ ctx, client, - existingTags: [ - { - id: "00000000-4444-4444-4444-121212121212", - name: "foo", - created_at: 0, - last_modified: 0, - }, - { - id: "10000000-4444-4444-4444-121212121212", - name: "bar", - created_at: 0, - last_modified: 0, - }, - ], - requiredGetParams, + existingAssets: [2], + }) + + const ids = ["foo", "bar"] + + ctx.server.events.on("request:start", (req) => { + if (req.url.hostname.startsWith(client.repositoryName)) { + ctx.expect(req.url.searchParams.getAll("ids")).toStrictEqual(ids) + } }) - const { results } = await client.getAssets(requiredGetParams) + // @ts-expect-error - testing purposes + const { results } = await client.getAssets({ ids }) - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[0]) ctx.expect.assertions(2) }) -it.concurrent("supports `tags` parameter (name)", async (ctx) => { +it.concurrent.skip("supports `tags` parameter", async (ctx) => { const client = createTestWriteClient({ ctx }) - const requiredGetParams = { - tags: [ - "00000000-4444-4444-4444-121212121212", - "10000000-4444-4444-4444-121212121212", - ], - } - - mockPrismicAssetAPI({ + const { assetsDatabase } = mockPrismicAssetAPI({ ctx, client, + existingAssets: [2], existingTags: [ { id: "00000000-4444-4444-4444-121212121212", @@ -162,25 +160,33 @@ it.concurrent("supports `tags` parameter (name)", async (ctx) => { last_modified: 0, }, ], - requiredGetParams, }) + ctx.server.events.on("request:start", (req) => { + if (req.url.hostname.startsWith(client.repositoryName)) { + ctx + .expect(req.url.searchParams.getAll("tags")) + .toStrictEqual([ + "00000000-4444-4444-4444-121212121212", + "10000000-4444-4444-4444-121212121212", + ]) + } + }) + + // @ts-expect-error - testing purposes const { results } = await client.getAssets({ tags: ["foo", "bar"] }) - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[0]) ctx.expect.assertions(2) }) -it.concurrent("supports `tags` parameter (missing)", async (ctx) => { +it.concurrent.skip("supports `tags` parameter (missing)", async (ctx) => { const client = createTestWriteClient({ ctx }) - const requiredGetParams = { - tags: ["00000000-4444-4444-4444-121212121212"], - } - - mockPrismicAssetAPI({ + const { assetsDatabase } = mockPrismicAssetAPI({ ctx, client, + existingAssets: [2], existingTags: [ { id: "00000000-4444-4444-4444-121212121212", @@ -189,12 +195,20 @@ it.concurrent("supports `tags` parameter (missing)", async (ctx) => { last_modified: 0, }, ], - requiredGetParams, }) + ctx.server.events.on("request:start", (req) => { + if (req.url.hostname.startsWith(client.repositoryName)) { + ctx + .expect(req.url.searchParams.getAll("tags")) + .toStrictEqual(["00000000-4444-4444-4444-121212121212"]) + } + }) + + // @ts-expect-error - testing purposes const { results } = await client.getAssets({ tags: ["foo", "bar"] }) - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[0]) ctx.expect.assertions(2) }) @@ -203,20 +217,36 @@ it.concurrent("returns `next` when next `cursor` is available", async (ctx) => { mockPrismicAssetAPI({ ctx, client }) + // @ts-expect-error - testing purposes const { next: next1 } = await client.getAssets() ctx.expect(next1).toBeUndefined() - mockPrismicAssetAPI({ ctx, client, existingAssets: [[], []] }) + mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [2, 2], + }) + // @ts-expect-error - testing purposes const { next: next2 } = await client.getAssets() ctx.expect(next2).toBeInstanceOf(Function) - mockPrismicAssetAPI({ ctx, client, requiredGetParams: { cursor: "1" } }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [2, 2], + }) + + ctx.server.events.on("request:start", (req) => { + if (req.url.hostname.startsWith(client.repositoryName)) { + ctx.expect(req.url.searchParams.get("cursor")).toBe("1") + } + }) const { results } = await next2!() - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[1]) ctx.expect.assertions(4) }) @@ -225,6 +255,7 @@ it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { mockPrismicAssetAPI({ ctx, client, writeToken: "invalid" }) + // @ts-expect-error - testing purposes await expect(() => client.getAssets()).rejects.toThrow(ForbiddenError) }) @@ -237,6 +268,7 @@ it.concurrent("is abortable with an AbortController", async (ctx) => { mockPrismicAssetAPI({ ctx, client }) await expect(() => + // @ts-expect-error - testing purposes client.getAssets({ fetchOptions: { signal: controller.signal }, }), @@ -248,10 +280,16 @@ it.concurrent("supports custom headers", async (ctx) => { const headers = { "x-custom": "foo" } - mockPrismicAssetAPI({ ctx, client, requiredHeaders: headers }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + existingAssets: [2], + requiredHeaders: headers, + }) + // @ts-expect-error - testing purposes const { results } = await client.getAssets({ fetchOptions: { headers } }) - ctx.expect(results).toBeInstanceOf(Array) + ctx.expect(results).toStrictEqual(assetsDatabase[0]) ctx.expect.assertions(2) }) diff --git a/test/writeClient-migrateCreateAssets.test.ts b/test/writeClient-migrate-assets.test.ts similarity index 88% rename from test/writeClient-migrateCreateAssets.test.ts rename to test/writeClient-migrate-assets.test.ts index 39b3d3b1..298fe6e5 100644 --- a/test/writeClient-migrateCreateAssets.test.ts +++ b/test/writeClient-migrate-assets.test.ts @@ -101,7 +101,11 @@ it.concurrent("creates new asset from string file data", async (ctx) => { const dummyFileData = "foo" mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + newAssets: [asset], + }) mockPrismicMigrationAPI({ ctx, client }) const migration = prismic.createMigration() @@ -109,6 +113,8 @@ it.concurrent("creates new asset from string file data", async (ctx) => { const reporter = vi.fn() + expect(assetsDatabase.flat()).toHaveLength(0) + await client.migrate(migration, { reporter }) expect(reporter).toHaveBeenCalledWith({ @@ -123,6 +129,7 @@ it.concurrent("creates new asset from string file data", async (ctx) => { }), }, }) + expect(assetsDatabase.flat()).toHaveLength(1) }) it.concurrent("creates new asset from a File instance", async (ctx) => { @@ -132,7 +139,11 @@ it.concurrent("creates new asset from a File instance", async (ctx) => { const dummyFile = new File(["foo"], asset.filename) mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + newAssets: [asset], + }) mockPrismicMigrationAPI({ ctx, client }) const migration = prismic.createMigration() @@ -140,6 +151,8 @@ it.concurrent("creates new asset from a File instance", async (ctx) => { const reporter = vi.fn() + expect(assetsDatabase.flat()).toHaveLength(0) + await client.migrate(migration, { reporter }) expect(reporter).toHaveBeenCalledWith({ @@ -154,6 +167,7 @@ it.concurrent("creates new asset from a File instance", async (ctx) => { }), }, }) + expect(assetsDatabase.flat()).toHaveLength(1) }) it.concurrent( @@ -164,7 +178,11 @@ it.concurrent( const asset = mockAsset(ctx) mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + newAssets: [asset], + }) mockPrismicMigrationAPI({ ctx, client }) ctx.server.use( @@ -178,6 +196,8 @@ it.concurrent( const reporter = vi.fn() + expect(assetsDatabase.flat()).toHaveLength(0) + await client.migrate(migration, { reporter }) expect(reporter).toHaveBeenCalledWith({ @@ -192,6 +212,7 @@ it.concurrent( }), }, }) + expect(assetsDatabase.flat()).toHaveLength(1) }, ) @@ -203,7 +224,11 @@ it.concurrent( const asset = mockAsset(ctx) mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + newAssets: [asset], + }) mockPrismicMigrationAPI({ ctx, client }) ctx.server.use( @@ -217,6 +242,8 @@ it.concurrent( const reporter = vi.fn() + expect(assetsDatabase.flat()).toHaveLength(0) + await client.migrate(migration, { reporter }) expect(reporter).toHaveBeenCalledWith({ @@ -231,6 +258,7 @@ it.concurrent( }), }, }) + expect(assetsDatabase.flat()).toHaveLength(1) }, ) @@ -242,7 +270,11 @@ it.concurrent( const asset = mockAsset(ctx) mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + newAssets: [asset], + }) mockPrismicMigrationAPI({ ctx, client }) ctx.server.use( @@ -256,6 +288,8 @@ it.concurrent( const reporter = vi.fn() + expect(assetsDatabase.flat()).toHaveLength(0) + await client.migrate(migration, { reporter }) expect(reporter).toHaveBeenCalledWith({ @@ -270,6 +304,7 @@ it.concurrent( }), }, }) + expect(assetsDatabase.flat()).toHaveLength(1) }, ) @@ -281,7 +316,11 @@ it.concurrent( const asset = mockAsset(ctx) mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client, newAssets: [asset] }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + newAssets: [asset], + }) mockPrismicMigrationAPI({ ctx, client }) ctx.server.use( @@ -295,6 +334,8 @@ it.concurrent( const reporter = vi.fn() + expect(assetsDatabase.flat()).toHaveLength(0) + await client.migrate(migration, { reporter }) expect(reporter).toHaveBeenCalledWith({ @@ -309,6 +350,7 @@ it.concurrent( }), }, }) + expect(assetsDatabase.flat()).toHaveLength(1) }, ) diff --git a/test/writeClient-migrateCreateDocuments.test.ts b/test/writeClient-migrate-documents.test.ts similarity index 100% rename from test/writeClient-migrateCreateDocuments.test.ts rename to test/writeClient-migrate-documents.test.ts diff --git a/test/writeClient-migrateUpdateDocuments-image.test.ts b/test/writeClient-migrate-patch-image.test.ts similarity index 100% rename from test/writeClient-migrateUpdateDocuments-image.test.ts rename to test/writeClient-migrate-patch-image.test.ts diff --git a/test/writeClient-migrateUpdateDocuments-link.test.ts b/test/writeClient-migrate-patch-link.test.ts similarity index 100% rename from test/writeClient-migrateUpdateDocuments-link.test.ts rename to test/writeClient-migrate-patch-link.test.ts diff --git a/test/writeClient-migrateUpdateDocuments-linkToMedia.test.ts b/test/writeClient-migrate-patch-linkToMedia.test.ts similarity index 100% rename from test/writeClient-migrateUpdateDocuments-linkToMedia.test.ts rename to test/writeClient-migrate-patch-linkToMedia.test.ts diff --git a/test/writeClient-migrateUpdateDocuments-simpleField.test.ts b/test/writeClient-migrate-patch-simpleField.test.ts similarity index 100% rename from test/writeClient-migrateUpdateDocuments-simpleField.test.ts rename to test/writeClient-migrate-patch-simpleField.test.ts diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index 896fd005..db2a5137 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -1,16 +1,122 @@ import { it as _it, expect, vi } from "vitest" import { createTestWriteClient } from "./__testutils__/createWriteClient" -import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" +import { + mockAsset, + mockPrismicAssetAPI, +} from "./__testutils__/mockPrismicAssetAPI" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" import * as prismic from "../src" +import type { + AssetMap, + DocumentMap, +} from "../src/lib/patchMigrationDocumentData" // Skip test on Node 16 and 18 (File and FormData support) const isNode16 = process.version.startsWith("v16") const isNode18 = process.version.startsWith("v18") const it = _it.skipIf(isNode16 || isNode18) +it.concurrent("performs migration", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const asset = mockAsset(ctx) + const newDocuments = [{ id: "foo" }, { id: "bar" }] + + mockPrismicRestAPIV2({ ctx }) + const { assetsDatabase } = mockPrismicAssetAPI({ + ctx, + client, + newAssets: [asset], + }) + const { documentsDatabase } = mockPrismicMigrationAPI({ + ctx, + client, + newDocuments, + }) + + const migration = prismic.createMigration() + + const documentFoo: prismic.PrismicMigrationDocument = + ctx.mock.value.document() + documentFoo.data = { + image: migration.createAsset("foo", "foo.png"), + link: () => migration.getByUID("bar", "bar"), + } + + const documentBar = ctx.mock.value.document() + documentBar.type = "bar" + documentBar.uid = "bar" + + migration.createDocument(documentFoo, "foo") + migration.createDocument(documentBar, "bar") + + let documents: DocumentMap | undefined + let assets: AssetMap | undefined + const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( + (event) => { + if (event.type === "assets:created") { + assets = event.data.assets + } else if (event.type === "documents:created") { + documents = event.data.documents + } + }, + ) + + await client.migrate(migration, { reporter }) + + expect(assets?.size).toBe(1) + expect(assetsDatabase.flat()).toHaveLength(1) + // Documents are indexed twice, on ID, and on reference + expect(documents?.size).toBe(4) + expect(Object.keys(documentsDatabase)).toHaveLength(2) + + expect(reporter).toHaveBeenCalledWith({ + type: "start", + data: { + pending: { + documents: 2, + assets: 1, + }, + }, + }) + + expect(reporter).toHaveBeenCalledWith({ + type: "assets:created", + data: { + created: 1, + assets: expect.any(Map), + }, + }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:created", + data: { + created: 2, + documents: expect.any(Map), + }, + }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:updated", + data: { + updated: 2, + }, + }) + + expect(reporter).toHaveBeenCalledWith({ + type: "end", + data: { + migrated: { + documents: 2, + assets: 1, + }, + }, + }) +}) + it.concurrent("migrates nothing when migration is empty", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts index b599e30b..966d96a6 100644 --- a/test/writeClient.test.ts +++ b/test/writeClient.test.ts @@ -11,7 +11,7 @@ it("creates a write client with `createWriteClient`", () => { expect(client).toBeInstanceOf(prismic.WriteClient) }) -it("warns constructor in if running in a browser-like environment", () => { +it("warns in constructor if running in a browser-like environment", () => { const originalWindow = globalThis.window globalThis.window = {} as Window & typeof globalThis From 45dd0019c146aae1450e2f79d04ade57055859ca Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 11 Sep 2024 22:52:47 +0200 Subject: [PATCH 45/61] refactor: use classes to detect migration field --- src/Client.ts | 2 +- src/Migration.ts | 213 +-- src/WriteClient.ts | 129 +- src/index.ts | 21 +- src/lib/isMigrationField.ts | 180 --- src/lib/isValue.ts | 215 +++ src/lib/patchMigrationDocumentData.ts | 447 ------ src/lib/prepareMigrationRecord.ts | 120 ++ src/lib/toField.ts | 106 -- src/types/migration/ContentRelationship.ts | 55 + src/types/migration/Field.ts | 48 + src/types/migration/asset.ts | 189 ++- src/types/migration/document.ts | 210 ++- src/types/migration/fields.ts | 57 - src/types/migration/richText.ts | 19 - ...te-patch-contentRelationship.test.ts.snap} | 723 +--------- ...iteClient-migrate-patch-image.test.ts.snap | 944 ++++--------- ...ent-migrate-patch-linkToMedia.test.ts.snap | 245 ++-- ...ent-migrate-patch-rtImageNode.test.ts.snap | 1253 +++++++++++++++++ test/__testutils__/mockPrismicAssetAPI.ts | 4 +- .../testMigrationFieldPatching.ts | 12 +- test/migration-createDocument.test.ts | 27 +- test/migration-getByUID.test.ts | 4 +- test/migration-getSingle.test.ts | 4 +- test/types/migration-document.types.ts | 44 +- test/types/migration.types.ts | 81 +- test/writeClient-migrate-documents.test.ts | 111 +- ...migrate-patch-contentRelationship.test.ts} | 39 +- test/writeClient-migrate-patch-image.test.ts | 34 +- ...teClient-migrate-patch-linkToMedia.test.ts | 17 +- ...teClient-migrate-patch-rtImageNode.test.ts | 43 + test/writeClient-migrate.test.ts | 6 +- 32 files changed, 2845 insertions(+), 2757 deletions(-) delete mode 100644 src/lib/isMigrationField.ts create mode 100644 src/lib/isValue.ts delete mode 100644 src/lib/patchMigrationDocumentData.ts create mode 100644 src/lib/prepareMigrationRecord.ts delete mode 100644 src/lib/toField.ts create mode 100644 src/types/migration/ContentRelationship.ts create mode 100644 src/types/migration/Field.ts delete mode 100644 src/types/migration/fields.ts delete mode 100644 src/types/migration/richText.ts rename test/__snapshots__/{writeClient-migrate-patch-link.test.ts.snap => writeClient-migrate-patch-contentRelationship.test.ts.snap} (54%) create mode 100644 test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap rename test/{writeClient-migrate-patch-link.test.ts => writeClient-migrate-patch-contentRelationship.test.ts} (57%) create mode 100644 test/writeClient-migrate-patch-rtImageNode.test.ts diff --git a/src/Client.ts b/src/Client.ts index a9d4e778..22fcd565 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1738,7 +1738,7 @@ export class Client< throw new RepositoryNotFoundError( `Prismic repository not found. Check that "${this.documentAPIEndpoint}" is pointing to the correct repository.`, url, - undefined, + url.startsWith(this.documentAPIEndpoint) ? undefined : res.text, ) } diff --git a/src/Migration.ts b/src/Migration.ts index 35b989b1..10faaf3e 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -1,119 +1,33 @@ -import * as is from "./lib/isMigrationField" +import { prepareMigrationRecord } from "./lib/prepareMigrationRecord" import { validateAssetMetadata } from "./lib/validateAssetMetadata" import type { Asset } from "./types/api/asset/asset" -import type { MigrationAsset } from "./types/migration/asset" +import type { MigrationAssetConfig } from "./types/migration/Asset" +import { MigrationImage } from "./types/migration/Asset" +import { MigrationDocument } from "./types/migration/Document" import type { - FieldsToMigrationFields, PrismicMigrationDocument, PrismicMigrationDocumentParams, -} from "./types/migration/document" -import { - type ImageMigrationField, - type LinkToMediaMigrationField, - MigrationFieldType, -} from "./types/migration/fields" +} from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" -import type { GroupField } from "./types/value/group" import type { FilledImageFieldImage } from "./types/value/image" -import { LinkType } from "./types/value/link" import type { FilledLinkToMediaField } from "./types/value/linkToMedia" -import { RichTextNodeType } from "./types/value/richText" -import type { SliceZone } from "./types/value/sliceZone" -import type { AnyRegularField } from "./types/value/types" - -import * as isFilled from "./helpers/isFilled" - -/** - * Discovers assets in a record of Prismic fields. - * - * @param record - Record of Prismic fields to loook for assets in. - * @param onAsset - Callback that is called for each asset found. - */ -const discoverAssets = ( - record: FieldsToMigrationFields< - Record - >, - onAsset: (asset: FilledImageFieldImage | FilledLinkToMediaField) => void, -) => { - for (const field of Object.values(record)) { - if (is.sliceZone(field)) { - for (const slice of field) { - discoverAssets(slice.primary, onAsset) - for (const item of slice.items) { - discoverAssets(item, onAsset) - } - } - } else if (is.richText(field)) { - for (const node of field) { - if ("type" in node) { - if (node.type === RichTextNodeType.image) { - onAsset(node) - if ( - node.linkTo && - "link_type" in node.linkTo && - node.linkTo.link_type === LinkType.Media - ) { - onAsset(node.linkTo) - } - } else if (node.type !== RichTextNodeType.embed) { - for (const span of node.spans) { - if ( - span.type === "hyperlink" && - span.data && - "link_type" in span.data && - span.data.link_type === LinkType.Media - ) { - onAsset(span.data) - } - } - } - } - } - } else if (is.group(field)) { - for (const item of field) { - discoverAssets(item, onAsset) - } - } else if ( - is.image(field) && - field && - "dimensions" in field && - isFilled.image(field) - ) { - onAsset(field) - } else if ( - is.link(field) && - field && - "link_type" in field && - field.link_type === LinkType.Media && - isFilled.linkToMedia(field) - ) { - onAsset(field) - } - } -} /** * 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 TMigrationDocuments - Prismic migration document types from which - * to extract. - * @typeParam TType - Type(s) to match `TMigrationDocuments` against. + * @typeParam TDocuments - Prismic document types from which to extract. + * @typeParam TDocumentType - Type(s) to match `TDocuments` against. */ -type ExtractMigrationDocumentType< - TMigrationDocuments extends PrismicMigrationDocument, - TType extends TMigrationDocuments["type"], +type ExtractDocumentType< + TDocuments extends PrismicDocument | PrismicMigrationDocument, + TDocumentType extends TDocuments["type"], > = - Extract extends never - ? TMigrationDocuments - : Extract - -type CreateAssetReturnType = ImageMigrationField & { - image: ImageMigrationField - linkToMedia: LinkToMediaMigrationField -} + Extract extends never + ? TDocuments + : Extract /** * The symbol used to index documents that are singletons. @@ -134,37 +48,37 @@ export class Migration< /** * @internal */ - _documents: { - document: TMigrationDocuments - params: PrismicMigrationDocumentParams - }[] = [] - #indexedDocuments: Record> = {} + _assets: Map = new Map() /** * @internal */ - _assets: Map = new Map() + _documents: MigrationDocument[] = [] + #indexedDocuments: Record< + string, + Record> + > = {} createAsset( asset: Asset | FilledImageFieldImage | FilledLinkToMediaField, - ): CreateAssetReturnType + ): MigrationImage createAsset( - file: MigrationAsset["file"], - filename: MigrationAsset["filename"], + file: MigrationAssetConfig["file"], + filename: MigrationAssetConfig["filename"], params?: { notes?: string credits?: string alt?: string tags?: string[] }, - ): CreateAssetReturnType + ): MigrationImage createAsset( fileOrAsset: - | MigrationAsset["file"] + | MigrationAssetConfig["file"] | Asset | FilledImageFieldImage | FilledLinkToMediaField, - filename?: MigrationAsset["filename"], + filename?: MigrationAssetConfig["filename"], { notes, credits, @@ -176,8 +90,9 @@ export class Migration< alt?: string tags?: string[] } = {}, - ): CreateAssetReturnType { - let asset: MigrationAsset + ): MigrationImage { + let asset: MigrationAssetConfig + let maybeInitialField: FilledImageFieldImage | undefined if (typeof fileOrAsset === "object" && "url" in fileOrAsset) { if ("dimensions" in fileOrAsset || "link_type" in fileOrAsset) { const url = fileOrAsset.url.split("?")[0] @@ -192,6 +107,10 @@ export class Migration< const alt = "alt" in fileOrAsset && fileOrAsset.alt ? fileOrAsset.alt : undefined + if ("dimensions" in fileOrAsset) { + maybeInitialField = fileOrAsset + } + asset = { id: fileOrAsset.id, file: url, @@ -243,64 +162,54 @@ export class Migration< this._assets.set(asset.id, asset) } - return { - migrationType: MigrationFieldType.Image, - ...asset, - image: { - migrationType: MigrationFieldType.Image, - ...asset, - }, - linkToMedia: { - migrationType: MigrationFieldType.LinkToMedia, - ...asset, - }, - } + return new MigrationImage(this._assets.get(asset.id)!, maybeInitialField) } createDocument( - document: ExtractMigrationDocumentType, + document: ExtractDocumentType, documentTitle: PrismicMigrationDocumentParams["documentTitle"], params: Omit = {}, - ): ExtractMigrationDocumentType { - this._documents.push({ - document, - params: { documentTitle, ...params }, - }) + ): MigrationDocument> { + const { record: data, dependencies } = prepareMigrationRecord( + document.data, + this.createAsset.bind(this), + ) + + const migrationDocument = new MigrationDocument( + { ...document, data }, + { + documentTitle, + ...params, + }, + dependencies, + ) + + this._documents.push(migrationDocument) // Index document if (!(document.type in this.#indexedDocuments)) { this.#indexedDocuments[document.type] = {} } this.#indexedDocuments[document.type][document.uid || SINGLE_INDEX] = - document - - // Find other assets in document - discoverAssets(document.data, this.createAsset.bind(this)) + migrationDocument - return document + return migrationDocument } - getByUID< - TType extends TMigrationDocuments["type"], - TMigrationDocument extends Extract< - TMigrationDocuments, - { type: TType } - > = Extract, - >(documentType: TType, uid: string): TMigrationDocument | undefined { + getByUID( + documentType: TType, + uid: string, + ): MigrationDocument> | undefined { return this.#indexedDocuments[documentType]?.[uid] as - | TMigrationDocument + | MigrationDocument | undefined } - getSingle< - TType extends TMigrationDocuments["type"], - TMigrationDocument extends Extract< - TMigrationDocuments, - { type: TType } - > = Extract, - >(documentType: TType): TMigrationDocument | undefined | undefined { + getSingle( + documentType: TType, + ): MigrationDocument> | undefined { return this.#indexedDocuments[documentType]?.[SINGLE_INDEX] as - | TMigrationDocument + | MigrationDocument | undefined } } diff --git a/src/WriteClient.ts b/src/WriteClient.ts index be5934e8..2dc6ac16 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -1,7 +1,5 @@ import { devMsg } from "./lib/devMsg" import { pLimit } from "./lib/pLimit" -import type { AssetMap, DocumentMap } from "./lib/patchMigrationDocumentData" -import { patchMigrationDocumentData } from "./lib/patchMigrationDocumentData" import type { Asset, @@ -24,11 +22,14 @@ import { type PostDocumentResult, type PutDocumentParams, } from "./types/api/migration/document" -import type { MigrationAsset } from "./types/migration/asset" +import type { AssetMap, MigrationAssetConfig } from "./types/migration/Asset" +import { MigrationContentRelationship } from "./types/migration/ContentRelationship" import type { + DocumentMap, + MigrationDocument, PrismicMigrationDocument, PrismicMigrationDocumentParams, -} from "./types/migration/document" +} from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" import { PrismicError } from "./errors/PrismicError" @@ -93,13 +94,13 @@ type MigrateReporterEventMap = { current: number remaining: number total: number - asset: MigrationAsset + asset: MigrationAssetConfig } "assets:creating": { current: number remaining: number total: number - asset: MigrationAsset + asset: MigrationAssetConfig } "assets:created": { created: number @@ -521,89 +522,77 @@ export class WriteClient< documents.set(document.id, document) } - const sortedDocuments: { - document: PrismicMigrationDocument - params: PrismicMigrationDocumentParams - }[] = [] + const sortedMigrationDocuments: MigrationDocument[] = [] // We create an array with non-master locale documents last because // we need their master locale document to be created first. - for (const { document, params } of migration._documents) { - if (document.lang === masterLocale) { - sortedDocuments.unshift({ document, params }) + for (const migrationDocument of migration._documents) { + if (migrationDocument.document.lang === masterLocale) { + sortedMigrationDocuments.unshift(migrationDocument) } else { - sortedDocuments.push({ document, params }) + sortedMigrationDocuments.push(migrationDocument) } } let i = 0 let created = 0 - for (const { document, params } of sortedDocuments) { - if (document.id && documents.has(document.id)) { + for (const migrationDocument of sortedMigrationDocuments) { + if ( + migrationDocument.document.id && + documents.has(migrationDocument.document.id) + ) { reporter?.({ type: "documents:skipping", data: { reason: "already exists", current: ++i, - remaining: sortedDocuments.length - i, - total: sortedDocuments.length, - document, - documentParams: params, + remaining: sortedMigrationDocuments.length - i, + total: sortedMigrationDocuments.length, + document: migrationDocument.document, + documentParams: migrationDocument.params, }, }) // Index the migration document - documents.set(document, documents.get(document.id)!) + documents.set( + migrationDocument, + documents.get(migrationDocument.document.id)!, + ) } else { created++ reporter?.({ type: "documents:creating", data: { current: ++i, - remaining: sortedDocuments.length - i, - total: sortedDocuments.length, - document, - documentParams: params, + remaining: sortedMigrationDocuments.length - i, + total: sortedMigrationDocuments.length, + document: migrationDocument.document, + documentParams: migrationDocument.params, }, }) // Resolve master language document ID for non-master locale documents let masterLanguageDocumentID: string | undefined - if (document.lang !== masterLocale) { - if (params.masterLanguageDocument) { - if (typeof params.masterLanguageDocument === "function") { - const masterLanguageDocument = - await params.masterLanguageDocument() - - if (masterLanguageDocument) { - // `masterLanguageDocument` is an existing document - if (masterLanguageDocument.id) { - masterLanguageDocumentID = documents.get( - masterLanguageDocument.id, - )?.id - } - - // `masterLanguageDocument` is a new document - if (!masterLanguageDocumentID) { - masterLanguageDocumentID = documents.get( - masterLanguageDocument, - )?.id - } - } - } else { - masterLanguageDocumentID = params.masterLanguageDocument.id - } - } else if (document.alternate_languages) { - masterLanguageDocumentID = document.alternate_languages.find( - ({ lang }) => lang === masterLocale, - )?.id + if (migrationDocument.document.lang !== masterLocale) { + if (migrationDocument.params.masterLanguageDocument) { + const link = new MigrationContentRelationship( + migrationDocument.params.masterLanguageDocument, + ) + + await link._prepare({ documents }) + masterLanguageDocumentID = link._field?.id + } else if (migrationDocument.document.alternate_languages) { + masterLanguageDocumentID = + migrationDocument.document.alternate_languages.find( + ({ lang }) => lang === masterLocale, + )?.id } } const { id } = await this.createDocument( // We'll upload docuements data later on. - { ...document, data: {} }, - params.documentTitle, + { ...migrationDocument.document, data: {} }, + migrationDocument.params.documentTitle, { masterLanguageDocumentID, ...fetchParams, @@ -611,10 +600,13 @@ export class WriteClient< ) // Index old ID for Prismic to Prismic migration - if (document.id) { - documents.set(document.id, { ...document, id }) + if (migrationDocument.document.id) { + documents.set(migrationDocument.document.id, { + ...migrationDocument.document, + id, + }) } - documents.set(document, { ...document, id }) + documents.set(migrationDocument, { ...migrationDocument.document, id }) } } @@ -650,30 +642,31 @@ export class WriteClient< }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise { let i = 0 - for (const { document, params } of migration._documents) { + for (const migrationDocument of migration._documents) { reporter?.({ type: "documents:updating", data: { current: ++i, remaining: migration._documents.length - i, total: migration._documents.length, - document, - documentParams: params, + document: migrationDocument.document, + documentParams: migrationDocument.params, }, }) - const { id, uid } = documents.get(document)! - const data = await patchMigrationDocumentData( - document.data, - assets, - documents, - ) + const { id, uid } = documents.get(migrationDocument)! + await migrationDocument._prepare({ assets, documents }) await this.updateDocument( id, // We need to forward again document name and tags to update them // in case the document already existed during the previous step. - { documentTitle: params.documentTitle, uid, tags: document.tags, data }, + { + documentTitle: migrationDocument.params.documentTitle, + uid, + tags: migrationDocument.document.tags, + data: migrationDocument.document.data, + }, fetchParams, ) } diff --git a/src/index.ts b/src/index.ts index 45768631..8c1dfaf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -326,24 +326,19 @@ export type { } from "./types/model/types" // Migrations - Types representing Prismic Migration API content values. -export { MigrationFieldType } from "./types/migration/fields" - export type { + MigrationDocument, PrismicMigrationDocument, - RichTextFieldToMigrationField, -} from "./types/migration/document" + RichTextFieldWithMigrationField, +} from "./types/migration/Document" export type { - ImageMigrationField, - LinkToMediaMigrationField, - ContentRelationshipMigrationField, - LinkMigrationField, -} from "./types/migration/fields" + MigrationImage, + MigrationLinkToMedia, + MigrationRTImageNode, +} from "./types/migration/Asset" -export type { - RTImageMigrationNode, - RTLinkMigrationNode, -} from "./types/migration/richText" +export type { MigrationContentRelationship } from "./types/migration/ContentRelationship" // API - Types representing Prismic Rest API V2 responses. export type { Query } from "./types/api/query" diff --git a/src/lib/isMigrationField.ts b/src/lib/isMigrationField.ts deleted file mode 100644 index 320f1fa6..00000000 --- a/src/lib/isMigrationField.ts +++ /dev/null @@ -1,180 +0,0 @@ -import type { - FieldToMigrationField, - GroupFieldToMigrationField, - RichTextFieldToMigrationField, - SliceZoneToMigrationField, -} from "../types/migration/document" -import type { - ImageMigrationField, - LinkMigrationField, -} from "../types/migration/fields" -import { MigrationFieldType } from "../types/migration/fields" -import type { GroupField } from "../types/value/group" -import type { ImageField } from "../types/value/image" -import type { LinkField } from "../types/value/link" -import { LinkType } from "../types/value/link" -import { type RichTextField, RichTextNodeType } from "../types/value/richText" -import type { SliceZone } from "../types/value/sliceZone" -import type { AnyRegularField } from "../types/value/types" - -/** - * Checks if a field is a slice zone. - * - * @param field - Field to check. - * - * @returns `true` if `field` is a slice zone migration field, `false` - * otherwise. - * - * @internal - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -export const sliceZone = ( - field: FieldToMigrationField, -): field is SliceZoneToMigrationField => { - return ( - Array.isArray(field) && - field.every((item) => "slice_type" in item && "id" in item) - ) -} - -/** - * Checks if a field is a rich text field. - * - * @param field - Field to check. - * - * @returns `true` if `field` is a rich text migration field, `false` otherwise. - * - * @internal - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -export const richText = ( - field: FieldToMigrationField, -): field is RichTextFieldToMigrationField => { - return ( - Array.isArray(field) && - field.every((item) => { - if ( - "migrationType" in item && - item.migrationType === MigrationFieldType.Image - ) { - return true - } else if ("type" in item && typeof item.type === "string") { - switch (item.type) { - case RichTextNodeType.heading1: - case RichTextNodeType.heading2: - case RichTextNodeType.heading3: - case RichTextNodeType.heading4: - case RichTextNodeType.heading5: - case RichTextNodeType.heading6: - case RichTextNodeType.paragraph: - case RichTextNodeType.preformatted: - case RichTextNodeType.listItem: - case RichTextNodeType.oListItem: - return "spans" in item && Array.isArray(item.spans) - - case RichTextNodeType.image: - return "dimensions" in item - - case RichTextNodeType.embed: - return "oembed" in item - } - } - - return false - }) - ) -} - -/** - * Checks if a field is a group field. - * - * @param field - Field to check. - * - * @returns `true` if `field` is a group migration field, `false` otherwise. - * - * @internal - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -export const group = ( - field: FieldToMigrationField, -): field is GroupFieldToMigrationField => { - return !sliceZone(field) && !richText(field) && Array.isArray(field) -} - -/** - * Checks if a field is a link field. - * - * @param field - Field to check. - * - * @returns `true` if `field` is a link migration field, `false` otherwise. - * - * @internal - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -export const link = ( - field: FieldToMigrationField, -): field is LinkMigrationField | LinkField => { - if (typeof field === "function") { - // Lazy content relationship field - return true - } else if (field && typeof field === "object" && !("version" in field)) { - if ( - "migrationType" in field && - field.migrationType === MigrationFieldType.LinkToMedia - ) { - // Migration link to media field - return true - } else if ( - "type" in field && - "lang" in field && - typeof field.lang === "string" - ) { - // Content relationship field declared using another repository document - return true - } else if ( - "link_type" in field && - (field.link_type === LinkType.Document || - field.link_type === LinkType.Media || - field.link_type === LinkType.Web) - ) { - // Regular link field - return true - } - } - - return false -} - -/** - * Checks if a field is an image field. - * - * @param field - Field to check. - * - * @returns `true` if `field` is an image migration field, `false` otherwise. - * - * @internal - * This is not an official helper function and it's only designed to work in the - * case of migration fields. - */ -export const image = ( - field: FieldToMigrationField, -): field is ImageMigrationField | ImageField => { - if (field && typeof field === "object" && !("version" in field)) { - if ( - "migrationType" in field && - field.migrationType === MigrationFieldType.Image - ) { - // Migration image field - return true - } else if ("id" in field && "url" in field && "dimensions" in field) { - // Regular image field - return true - } - } - - return false -} diff --git a/src/lib/isValue.ts b/src/lib/isValue.ts new file mode 100644 index 00000000..71f563f0 --- /dev/null +++ b/src/lib/isValue.ts @@ -0,0 +1,215 @@ +import type { + FieldWithMigrationField, + RichTextBlockNodeWithMigrationField, +} from "../types/migration/Document" +import type { FilledContentRelationshipField } from "../types/value/contentRelationship" +import type { PrismicDocument } from "../types/value/document" +import type { ImageField } from "../types/value/image" +import { LinkType } from "../types/value/link" +import type { FilledLinkToMediaField } from "../types/value/linkToMedia" +import { type RTImageNode, RichTextNodeType } from "../types/value/richText" + +/** + * Checks if a value is a link to media field. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a link to media field, `false` otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const linkToMedia = ( + value: + | PrismicDocument + | FieldWithMigrationField + | RichTextBlockNodeWithMigrationField + | unknown, +): value is FilledLinkToMediaField => { + if (value && typeof value === "object" && !("version" in value)) { + if ( + "link_type" in value && + value.link_type === LinkType.Media && + "id" in value && + "name" in value && + "kind" in value && + "url" in value && + "size" in value + ) { + return true + } + } + + return false +} + +/** + * Checks if a value is like an image field. + * + * @param value - Value to check. + * + * @returns `true` if `value` is like an image field, `false` otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +const imageLike = ( + value: + | PrismicDocument + | FieldWithMigrationField + | RichTextBlockNodeWithMigrationField + | unknown, +): value is ImageField | RTImageNode => { + if ( + value && + typeof value === "object" && + (!("version" in value) || typeof value.version === "object") + ) { + if ( + "id" in value && + "url" in value && + typeof value.url === "string" && + "dimensions" in value && + "edit" in value && + "alt" in value && + "copyright" in value + ) { + return true + } + } + + return false +} + +/** + * Checks if a value is an image field. + * + * @param value - Value to check. + * + * @returns `true` if `value` is an image field, `false` otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const image = ( + value: + | PrismicDocument + | FieldWithMigrationField + | RichTextBlockNodeWithMigrationField + | unknown, +): value is ImageField => { + if ( + imageLike(value) && + (!("type" in value) || value.type !== RichTextNodeType.image) + ) { + value + + return true + } + + return false +} + +/** + * Checks if a value is a rich text image node. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a rich text image node, `false` otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const rtImageNode = ( + value: + | PrismicDocument + | FieldWithMigrationField + | RichTextBlockNodeWithMigrationField + | unknown, +): value is RTImageNode => { + if ( + imageLike(value) && + "type" in value && + value.type === RichTextNodeType.image + ) { + value + + return true + } + + return false +} + +/** + * Checks if a value is a content relationship field. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a content relationship, `false` otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const contentRelationship = ( + value: + | PrismicDocument + | FieldWithMigrationField + | RichTextBlockNodeWithMigrationField + | unknown, +): value is FilledContentRelationshipField => { + if (value && typeof value === "object" && !("version" in value)) { + if ( + "link_type" in value && + value.link_type === LinkType.Document && + "id" in value && + "type" in value && + "tags" in value && + "lang" in value + ) { + return true + } + } + + return false +} + +/** + * Checks if a value is a Prismic document. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a content relationship, `false` otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const document = ( + value: + | PrismicDocument + | FieldWithMigrationField + | RichTextBlockNodeWithMigrationField + | unknown, +): value is PrismicDocument => { + if (value && typeof value === "object" && !("version" in value)) { + if ( + "id" in value && + "uid" in value && + "url" in value && + "type" in value && + typeof value.type === "string" && + "href" in value && + "tags" in value && + "first_publication_date" in value && + "last_publication_date" in value && + "slugs" in value && + "linked_documents" in value && + "lang" in value && + "alternate_languages" in value && + "data" in value + ) { + return true + } + } + + return false +} diff --git a/src/lib/patchMigrationDocumentData.ts b/src/lib/patchMigrationDocumentData.ts deleted file mode 100644 index d99b59b7..00000000 --- a/src/lib/patchMigrationDocumentData.ts +++ /dev/null @@ -1,447 +0,0 @@ -import type { Asset } from "../types/api/asset/asset" -import type { MigrationAsset } from "../types/migration/asset" -import type { - FieldsToMigrationFields, - GroupFieldToMigrationField, - PrismicMigrationDocument, - RichTextFieldToMigrationField, - SliceZoneToMigrationField, -} from "../types/migration/document" -import type { - ImageMigrationField, - LinkMigrationField, -} from "../types/migration/fields" -import { MigrationFieldType } from "../types/migration/fields" -import type { PrismicDocument } from "../types/value/document" -import type { GroupField, NestedGroupField } from "../types/value/group" -import type { FilledImageFieldImage, ImageField } from "../types/value/image" -import { LinkType } from "../types/value/link" -import type { LinkField } from "../types/value/link" -import type { RTInlineNode } from "../types/value/richText" -import { type RichTextField, RichTextNodeType } from "../types/value/richText" -import type { SharedSlice } from "../types/value/sharedSlice" -import type { Slice } from "../types/value/slice" -import type { SliceZone } from "../types/value/sliceZone" -import type { AnyRegularField } from "../types/value/types" - -import * as isFilled from "../helpers/isFilled" - -import * as is from "./isMigrationField" -import * as to from "./toField" - -/** - * A map of asset IDs to asset used to resolve assets when patching migration - * Prismic documents. - * - * @internal - */ -export type AssetMap = Map - -/** - * A map of document IDs, documents, and migraiton documents to content - * relationship field used to resolve content relationships when patching - * migration Prismic documents. - * - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @internal - */ -export type DocumentMap = - Map< - | string - | TDocuments - | PrismicMigrationDocument - | PrismicMigrationDocument, - | PrismicDocument - | (Omit, "id"> & { id: string }) - > - -/** - * Inherits query parameters from an original URL to a new URL. - * - * @param url - The new URL. - * @param original - The original URL to inherit query parameters from. - * - * @returns The new URL with query parameters inherited from the original URL. - */ -const inheritQueryParams = (url: string, original: string): string => { - const queryParams = original.split("?")[1] || "" - - return `${url.split("?")[0]}${queryParams ? `?${queryParams}` : ""}` -} - -/** - * Patches references in a slice zone field. - * - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @param sliceZone - The slice zone to patch. - * @param assets - A map of assets available in the Prismic repository. - * @param documents - A map of documents available in the Prismic repository. - * - * @returns The patched slice zone. - */ -const patchSliceZone = async < - TDocuments extends PrismicDocument = PrismicDocument, ->( - sliceZone: SliceZoneToMigrationField, - assets: AssetMap, - documents: DocumentMap, -): Promise => { - const result = [] as unknown as SliceZone - - for (const slice of sliceZone) { - const { primary, items, ...properties } = slice - const patchedPrimary = await patchRecord( - // We cast to `Slice["primary"]` which is stricter than `SharedSlice["primary"]` - // otherwise TypeScript gets confused while creating the patched slice below. - primary as Slice["primary"], - assets, - documents, - ) - const patchedItems = await patchGroup(items, assets, documents) - - result.push({ - ...properties, - primary: patchedPrimary, - items: patchedItems, - }) - } - - return result -} - -/** - * Patches references in a rich text field. - * - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @param richText - The rich text field to patch. - * @param assets - A map of assets available in the Prismic repository. - * @param documents - A map of documents available in the Prismic repository. - * - * @returns The patched rich text field. - */ -const patchRichText = async < - TDocuments extends PrismicDocument = PrismicDocument, ->( - richText: RichTextFieldToMigrationField, - assets: AssetMap, - documents: DocumentMap, -): Promise => { - const result = [] as unknown as RichTextField<"filled"> - - for (const node of richText) { - if ("type" in node && typeof node.type === "string") { - if (node.type === RichTextNodeType.embed) { - result.push(node) - } else if (node.type === RichTextNodeType.image) { - const image = patchImage(node, assets) - - if (isFilled.image(image)) { - const linkTo = await patchLink(node.linkTo, assets, documents) - - result.push({ - ...image, - type: RichTextNodeType.image, - linkTo: isFilled.link(linkTo) ? linkTo : undefined, - }) - } - } else { - const { spans, ...properties } = node - - const patchedSpans: RTInlineNode[] = [] - - for (const span of spans) { - if (span.type === RichTextNodeType.hyperlink) { - const data = await patchLink(span.data, assets, documents) - - if (isFilled.link(data)) { - patchedSpans.push({ ...span, data }) - } - } else { - patchedSpans.push(span) - } - } - - result.push({ - ...properties, - spans: patchedSpans, - }) - } - } else { - // Migration image node - const asset = assets.get(node.id) - - const linkTo = await patchLink(node.linkTo, assets, documents) - - if (asset) { - result.push({ - ...to.rtImageNode(asset), - linkTo: isFilled.link(linkTo) ? linkTo : undefined, - }) - } - } - } - - return result -} - -/** - * Patches references in a group field. - * - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @param group - The group field to patch. - * @param assets - A map of assets available in the Prismic repository. - * @param documents - A map of documents available in the Prismic repository. - * - * @returns The patched group field. - */ -const patchGroup = async < - TMigrationGroup extends GroupFieldToMigrationField< - GroupField | Slice["items"] | SharedSlice["items"] - >, - TDocuments extends PrismicDocument = PrismicDocument, ->( - group: TMigrationGroup, - assets: AssetMap, - documents: DocumentMap, -): Promise< - TMigrationGroup extends GroupFieldToMigrationField - ? TGroup - : never -> => { - const result = [] as unknown as GroupField< - Record, - "filled" - > - - for (const item of group) { - const patched = await patchRecord(item, assets, documents) - - result.push(patched) - } - - return result as TMigrationGroup extends GroupFieldToMigrationField< - infer TGroup - > - ? TGroup - : never -} - -/** - * Patches references in a link field. - * - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @param link - The link field to patch. - * @param assets - A map of assets available in the Prismic repository. - * @param documents - A map of documents available in the Prismic repository. - * - * @returns The patched link field. - */ -const patchLink = async ( - link: LinkMigrationField | LinkField, - assets: AssetMap, - documents: DocumentMap, -): Promise => { - if (link) { - if ("migrationType" in link) { - // Migration link field - const asset = assets.get(link.id) - if (asset) { - return to.linkToMediaField(asset) - } - } else if ("link_type" in link) { - switch (link.link_type) { - case LinkType.Document: - // Existing content relationship - if (isFilled.contentRelationship(link)) { - const id = documents.get(link.id)?.id - if (id) { - return { ...link, id } - } else { - return { ...link, isBroken: true } - } - } - case LinkType.Media: - // Existing link to media - if (isFilled.linkToMedia(link)) { - const id = assets.get(link.id)?.id - if (id) { - return { ...link, id } - } - break - } - case LinkType.Web: - default: - return link - } - } else { - const resolved = typeof link === "function" ? await link() : link - - if (resolved) { - // Link might be represented by a document or a migration document... - let maybeRelationship = resolved.id - ? documents.get(resolved.id) - : undefined - - // ...or by a migration document - if (!maybeRelationship) { - maybeRelationship = documents.get(resolved) - } - - if (maybeRelationship) { - return to.contentRelationship(maybeRelationship) - } - } - } - } - - return { - link_type: LinkType.Any, - } -} - -/** - * Patches references in an image field. - * - * @param image - The image field to patch. - * @param assets - A map of assets available in the Prismic repository. - * - * @returns The patched image field. - */ -const patchImage = ( - image: ImageMigrationField | ImageField, - assets: AssetMap, -): ImageField => { - if (image) { - if ( - "migrationType" in image && - image.migrationType === MigrationFieldType.Image - ) { - // Migration image field - const asset = assets.get(image.id) - if (asset) { - return to.imageField(asset) - } - } else if ( - "dimensions" in image && - image.dimensions && - isFilled.image(image) - ) { - // Regular image field - const { - id, - url, - dimensions, - edit, - alt, - copyright: _, - ...thumbnails - } = image - const asset = assets.get(id) - - if (asset) { - const result = { - id: asset.id, - url: inheritQueryParams(asset.url, url), - dimensions, - edit, - alt: alt || null, - copyright: asset.credits || null, - } as ImageField - - if (Object.keys(thumbnails).length > 0) { - for (const name in thumbnails) { - const maybeThumbnail = ( - thumbnails as Record - )[name] - - if (is.image(maybeThumbnail)) { - const { url, dimensions, edit, alt } = ( - thumbnails as Record - )[name] - - result[name] = { - id: asset.id, - url: inheritQueryParams(asset.url, url), - dimensions, - edit, - alt: alt || null, - copyright: asset.credits || null, - } - } - } - } - - return result - } - } - } - - return {} -} - -/** - * Patches references in a record of Prismic field. - * - * @typeParam TFields - Type of the record of Prismic fields. - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @param record - The link field to patch. - * @param assets - A map of assets available in the Prismic repository. - * @param documents - A map of documents available in the Prismic repository. - * - * @returns The patched record. - */ -const patchRecord = async < - TFields extends Record, - TDocuments extends PrismicDocument = PrismicDocument, ->( - record: FieldsToMigrationFields, - assets: AssetMap, - documents: DocumentMap, -): Promise => { - const result: Record = {} - - for (const [key, field] of Object.entries(record)) { - if (is.sliceZone(field)) { - result[key] = await patchSliceZone(field, assets, documents) - } else if (is.richText(field)) { - result[key] = await patchRichText(field, assets, documents) - } else if (is.group(field)) { - result[key] = await patchGroup(field, assets, documents) - } else if (is.link(field)) { - result[key] = await patchLink(field, assets, documents) - } else if (is.image(field)) { - result[key] = patchImage(field, assets) - } else { - result[key] = field - } - } - - return result as TFields -} - -/** - * Patches references in a document's data. - * - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @param data - The document data to patch. - * @param assets - A map of assets available in the Prismic repository. - * @param documents - A map of documents available in the Prismic repository. - * - * @returns The patched document data. - */ -export const patchMigrationDocumentData = async < - TDocuments extends PrismicDocument = PrismicDocument, ->( - data: PrismicMigrationDocument["data"], - assets: AssetMap, - documents: DocumentMap, -): Promise => { - return patchRecord(data, assets, documents) -} diff --git a/src/lib/prepareMigrationRecord.ts b/src/lib/prepareMigrationRecord.ts new file mode 100644 index 00000000..26ea459e --- /dev/null +++ b/src/lib/prepareMigrationRecord.ts @@ -0,0 +1,120 @@ +import type { + MigrationImage, + MigrationLinkToMedia, +} from "../types/migration/Asset" +import type { UnresolvedMigrationContentRelationshipConfig } from "../types/migration/ContentRelationship" +import { MigrationContentRelationship } from "../types/migration/ContentRelationship" +import { MigrationDocument } from "../types/migration/Document" +import { MigrationField } from "../types/migration/Field" +import type { FilledImageFieldImage } from "../types/value/image" +import type { FilledLinkToWebField } from "../types/value/link" +import type { FilledLinkToMediaField } from "../types/value/linkToMedia" + +import * as is from "./isValue" + +/** + * Replaces existings assets and links in a record of Prismic fields get all + * dependencies to them. + * + * @typeParam TRecord - Record of values to work with. + * + * @param record - Record of Prismic fields to work with. + * @param onAsset - Callback that is called for each asset found. + * + * @returns An object containing the record with replaced assets and links and a + * list of dependencies found and/or created. + */ +export const prepareMigrationRecord = >( + record: TRecord, + onAsset: ( + asset: FilledImageFieldImage | FilledLinkToMediaField, + ) => MigrationImage, +): { record: TRecord; dependencies: MigrationField[] } => { + const result = {} as Record + const dependencies: MigrationField[] = [] + + for (const key in record) { + const field: unknown = record[key] + + if (field instanceof MigrationField) { + dependencies.push(field) + result[key] = field + } else if (is.linkToMedia(field)) { + const linkToMedia = onAsset(field).asLinkToMedia() + + dependencies.push(linkToMedia) + result[key] = linkToMedia + } else if (is.rtImageNode(field)) { + const rtImageNode = onAsset(field).asRTImageNode() + + if (field.linkTo) { + // Node `linkTo` dependency is tracked internally + rtImageNode.linkTo = prepareMigrationRecord( + { linkTo: field.linkTo }, + onAsset, + ).record.linkTo as + | MigrationContentRelationship + | MigrationLinkToMedia + | FilledLinkToWebField + } + + dependencies.push(rtImageNode) + result[key] = rtImageNode + } else if (is.image(field)) { + const image = onAsset(field).asImage() + + const { + id: _id, + url: _url, + dimensions: _dimensions, + edit: _edit, + alt: _alt, + copyright: _copyright, + ...thumbnails + } = field + + for (const name in thumbnails) { + if (is.image(thumbnails[name])) { + image.addThumbnail(name, onAsset(thumbnails[name]).asImage()) + } + } + + dependencies.push(image) + result[key] = image + } else if ( + is.contentRelationship(field) || + is.document(field) || + field instanceof MigrationDocument || + typeof field === "function" + ) { + const contentRelationship = new MigrationContentRelationship( + field as UnresolvedMigrationContentRelationshipConfig, + ) + + dependencies.push(contentRelationship) + result[key] = contentRelationship + } else if (Array.isArray(field)) { + const array = [] + + for (const item of field) { + const { record, dependencies: itemDependencies } = + prepareMigrationRecord({ item }, onAsset) + + array.push(record.item) + dependencies.push(...itemDependencies) + } + + result[key] = array + } else if (field && typeof field === "object") { + const { record, dependencies: fieldDependencies } = + prepareMigrationRecord({ ...field }, onAsset) + + dependencies.push(...fieldDependencies) + result[key] = record + } else { + result[key] = field + } + } + + return { record: result as TRecord, dependencies } +} diff --git a/src/lib/toField.ts b/src/lib/toField.ts deleted file mode 100644 index 84035a43..00000000 --- a/src/lib/toField.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { Asset } from "../types/api/asset/asset" -import type { PrismicMigrationDocument } from "../types/migration/document" -import type { FilledContentRelationshipField } from "../types/value/contentRelationship" -import type { PrismicDocument } from "../types/value/document" -import type { ImageField } from "../types/value/image" -import { LinkType } from "../types/value/link" -import type { LinkToMediaField } from "../types/value/linkToMedia" -import type { RTImageNode } from "../types/value/richText" -import { RichTextNodeType } from "../types/value/richText" - -/** - * Converts an asset to an image field. - * - * @param asset - Asset to convert. - * - * @returns Equivalent image field. - */ -export const imageField = ({ - id, - url, - width, - height, - alt, - credits, -}: Asset): ImageField => { - return { - id, - url, - dimensions: { width: width!, height: height! }, - edit: { x: 0, y: 0, zoom: 0, background: "transparent" }, - alt: alt || null, - copyright: credits || null, - } -} - -/** - * Converts an asset to a link to media field. - * - * @param asset - Asset to convert. - * - * @returns Equivalent link to media field. - */ -export const linkToMediaField = ({ - id, - filename, - kind, - url, - size, - width, - height, -}: Asset): LinkToMediaField<"filled"> => { - return { - id, - link_type: LinkType.Media, - name: filename, - kind, - url, - size: `${size}`, - width: width ? `${width}` : undefined, - height: height ? `${height}` : undefined, - } -} - -/** - * Converts an asset to an RT image node. - * - * @param asset - Asset to convert. - * - * @returns Equivalent rich text image node. - */ -export const rtImageNode = (asset: Asset): RTImageNode => { - return { - ...imageField(asset), - type: RichTextNodeType.image, - } -} - -/** - * Converts a document to a content relationship field. - * - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @param document - Document to convert. - * - * @returns Equivalent content relationship. - */ -export const contentRelationship = < - TDocuments extends PrismicDocument = PrismicDocument, ->( - document: - | TDocuments - | (Omit & { id: string }), -): FilledContentRelationshipField => { - return { - link_type: LinkType.Document, - id: document.id, - uid: document.uid || undefined, - type: document.type, - tags: document.tags || [], - lang: document.lang, - url: undefined, - slug: undefined, - isBroken: false, - data: undefined, - } -} diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts new file mode 100644 index 00000000..f9c0eff3 --- /dev/null +++ b/src/types/migration/ContentRelationship.ts @@ -0,0 +1,55 @@ +import type { FilledContentRelationshipField } from "../value/contentRelationship" +import type { PrismicDocument } from "../value/document" +import { LinkType } from "../value/link" + +import type { DocumentMap, MigrationDocument } from "./Document" +import { MigrationField } from "./Field" + +type MigrationContentRelationshipConfig = + | PrismicDocument + | MigrationDocument + | FilledContentRelationshipField + | undefined + +export type UnresolvedMigrationContentRelationshipConfig = + | MigrationContentRelationshipConfig + | (() => + | Promise + | MigrationContentRelationshipConfig) + +export class MigrationContentRelationship extends MigrationField { + unresolvedConfig: UnresolvedMigrationContentRelationshipConfig + + constructor( + unresolvedConfig: UnresolvedMigrationContentRelationshipConfig, + initialField?: FilledContentRelationshipField, + ) { + super(initialField) + + this.unresolvedConfig = unresolvedConfig + } + + async _prepare({ documents }: { documents: DocumentMap }): Promise { + const config = + typeof this.unresolvedConfig === "function" + ? await this.unresolvedConfig() + : this.unresolvedConfig + + if (config) { + const document = + "id" in config ? documents.get(config.id) : documents.get(config) + + if (document) { + this._field = { + link_type: LinkType.Document, + id: document.id, + uid: document.uid || undefined, + type: document.type, + tags: document.tags || [], + lang: document.lang, + isBroken: false, + } + } + } + } +} diff --git a/src/types/migration/Field.ts b/src/types/migration/Field.ts new file mode 100644 index 00000000..b70926a8 --- /dev/null +++ b/src/types/migration/Field.ts @@ -0,0 +1,48 @@ +import type { AnyRegularField } from "../value/types" + +import type { RTBlockNode, RTInlineNode } from "../value/richText" + +import type { AssetMap } from "./Asset" +import type { DocumentMap } from "./Document" + +interface Preparable { + /** + * @internal + */ + _prepare(args: { + assets: AssetMap + documents: DocumentMap + }): Promise | void +} + +export abstract class MigrationField< + TField extends AnyRegularField | RTBlockNode | RTInlineNode = + | AnyRegularField + | RTBlockNode + | RTInlineNode, + TInitialField = TField, +> implements Preparable +{ + /** + * @internal + */ + _field: TField | undefined + + /** + * @internal + */ + _initialField: TInitialField | undefined + + constructor(initialField?: TInitialField) { + this._initialField = initialField + } + + toJSON(): TField | undefined { + return this._field + } + + abstract _prepare(args: { + assets: AssetMap + documents: DocumentMap + }): Promise | void +} diff --git a/src/types/migration/asset.ts b/src/types/migration/asset.ts index f71ddd82..1bf3d934 100644 --- a/src/types/migration/asset.ts +++ b/src/types/migration/asset.ts @@ -1,7 +1,61 @@ +import type { Asset } from "../api/asset/asset" +import type { FilledImageFieldImage, ImageField } from "../value/image" +import { type FilledLinkToWebField, LinkType } from "../value/link" +import type { LinkToMediaField } from "../value/linkToMedia" +import { type RTImageNode, RichTextNodeType } from "../value/richText" + +import type { MigrationContentRelationship } from "./ContentRelationship" +import type { DocumentMap } from "./Document" +import { MigrationField } from "./Field" + +/** + * Converts an asset to an image field. + * + * @param asset - Asset to convert. + * @param maybeInitialField - Initial image field if available, used to preserve + * edits. + * + * @returns Equivalent image field. + */ +const assetToImage = ( + asset: Asset, + maybeInitialField?: + | FilledImageFieldImage + | LinkToMediaField<"filled"> + | RTImageNode, +): FilledImageFieldImage => { + const parameters = (maybeInitialField?.url || asset.url).split("?")[1] + const url = `${asset.url.split("?")[0]}${parameters ? `?${parameters}` : ""}` + const dimensions: FilledImageFieldImage["dimensions"] = { + width: asset.width!, + height: asset.height!, + } + const edit: FilledImageFieldImage["edit"] = + maybeInitialField && "edit" in maybeInitialField + ? maybeInitialField?.edit + : { x: 0, y: 0, zoom: 1, background: "transparent" } + + const alt = + (maybeInitialField && "alt" in maybeInitialField + ? maybeInitialField.alt + : undefined) || + asset.alt || + null + + return { + id: asset.id, + url, + dimensions, + edit, + alt: alt, + copyright: asset.credits || null, + } +} + /** * An asset to be uploaded to Prismic media library. */ -export type MigrationAsset = { +export type MigrationAssetConfig = { /** * ID of the asset used to reference it in Prismic documents. * @@ -43,3 +97,136 @@ export type MigrationAsset = { */ tags?: string[] } + +export abstract class MigrationAsset< + TField extends + | FilledImageFieldImage + | LinkToMediaField<"filled"> + | RTImageNode = + | FilledImageFieldImage + | LinkToMediaField<"filled"> + | RTImageNode, +> extends MigrationField< + TField, + FilledImageFieldImage | LinkToMediaField<"filled"> | RTImageNode +> { + config: MigrationAssetConfig + + constructor( + config: MigrationAssetConfig, + initialField?: + | FilledImageFieldImage + | LinkToMediaField<"filled"> + | RTImageNode, + ) { + super(initialField) + + this.config = config + } + + asImage(): MigrationImage { + return new MigrationImage(this.config, this._initialField) + } + + asLinkToMedia(): MigrationLinkToMedia { + return new MigrationLinkToMedia(this.config, this._initialField) + } + + asRTImageNode(): MigrationRTImageNode { + return new MigrationRTImageNode(this.config, this._initialField) + } +} + +export class MigrationImage extends MigrationAsset { + #thumbnails: Record = {} + + addThumbnail(name: string, thumbnail: MigrationImage): void { + this.#thumbnails[name] = thumbnail + } + + async _prepare({ + assets, + documents, + }: { + assets: AssetMap + documents: DocumentMap + }): Promise { + const asset = assets.get(this.config.id) + + if (asset) { + this._field = assetToImage(asset, this._initialField) + + for (const name in this.#thumbnails) { + await this.#thumbnails[name]._prepare({ assets, documents }) + + const thumbnail = this.#thumbnails[name]._field + if (thumbnail) { + ;(this._field as ImageField)[name] = thumbnail + } + } + } + } +} + +export class MigrationLinkToMedia extends MigrationAsset< + LinkToMediaField<"filled"> +> { + _prepare({ assets }: { assets: AssetMap }): void { + const asset = assets.get(this.config.id) + + if (asset) { + this._field = { + id: asset.id, + link_type: LinkType.Media, + name: asset.filename, + kind: asset.kind, + url: asset.url, + size: `${asset.size}`, + height: + typeof asset.height === "number" ? `${asset.height}` : undefined, + width: typeof asset.width === "number" ? `${asset.width}` : undefined, + } + } + } +} + +export class MigrationRTImageNode extends MigrationAsset { + linkTo: + | MigrationLinkToMedia + | MigrationContentRelationship + | FilledLinkToWebField + | undefined + + async _prepare({ + assets, + documents, + }: { + assets: AssetMap + documents: DocumentMap + }): Promise { + const asset = assets.get(this.config.id) + + if (this.linkTo instanceof MigrationField) { + await this.linkTo._prepare({ assets, documents }) + } + + if (asset) { + this._field = { + ...assetToImage(asset, this._initialField), + type: RichTextNodeType.image, + linkTo: + this.linkTo instanceof MigrationField + ? this.linkTo._field + : this.linkTo, + } + } + } +} + +/** + * A map of asset IDs to asset used to resolve assets when patching migration + * Prismic documents. + * + * @internal + */ +export type AssetMap = Map diff --git a/src/types/migration/document.ts b/src/types/migration/document.ts index 4d030122..aa381c77 100644 --- a/src/types/migration/document.ts +++ b/src/types/migration/document.ts @@ -1,13 +1,15 @@ import type { AnyRegularField } from "../value/types" +import type { FilledContentRelationshipField } from "../value/contentRelationship" import type { PrismicDocument } from "../value/document" import type { GroupField } from "../value/group" import type { ImageField } from "../value/image" -import type { LinkField } from "../value/link" +import type { FilledLinkToMediaField } from "../value/linkToMedia" import type { RTBlockNode, RTImageNode, RTInlineNode, + RTLinkNode, RTTextNode, RichTextField, } from "../value/richText" @@ -15,114 +17,147 @@ import type { SharedSlice } from "../value/sharedSlice" import type { Slice } from "../value/slice" import type { SliceZone } from "../value/sliceZone" -import type { ImageMigrationField, LinkMigrationField } from "./fields" -import type { RTImageMigrationNode, RTLinkMigrationNode } from "./richText" +import type { + AssetMap, + MigrationImage, + MigrationLinkToMedia, + MigrationRTImageNode, +} from "./Asset" +import type { + MigrationContentRelationship, + UnresolvedMigrationContentRelationshipConfig, +} from "./ContentRelationship" +import type { MigrationField } from "./Field" /** - * A utility type that converts a rich text text node to a rich text text - * migration node. + * A utility type that extends Rich text field node's spans with their migration + * node equivalent. * * @typeParam TRTNode - Rich text text node type to convert. */ -type RichTextTextNodeToMigrationField = Omit< - TRTNode, - "spans" -> & { - spans: (RTInlineNode | RTLinkMigrationNode)[] +type RichTextTextNodeWithMigrationField< + TRTNode extends RTTextNode = RTTextNode, +> = Omit & { + spans: ( + | RTInlineNode + | (Omit & { + data: + | MigrationLinkToMedia + | MigrationContentRelationship + | UnresolvedMigrationContentRelationshipConfig + }) + )[] } /** - * A utility type that converts a rich text block node to a rich text block - * migration node. + * A utility type that extends a Rich text field node with their migration node + * equivalent. * * @typeParam TRTNode - Rich text block node type to convert. */ -type RichTextBlockNodeToMigrationField = - TRTNode extends RTImageNode - ? - | RTImageMigrationNode - | (Omit & { - linkTo?: RTImageNode["linkTo"] | LinkMigrationField - }) - : TRTNode extends RTTextNode - ? RichTextTextNodeToMigrationField - : TRTNode +export type RichTextBlockNodeWithMigrationField< + TRTNode extends RTBlockNode = RTBlockNode, +> = TRTNode extends RTImageNode + ? RTImageNode | MigrationRTImageNode + : TRTNode extends RTTextNode + ? RichTextTextNodeWithMigrationField + : TRTNode /** - * A utility type that converts a rich text field to a rich text migration - * field. + * A utility type that extends a Rich text field's nodes with their migration + * node equivalent. * * @typeParam TField - Rich text field type to convert. */ -export type RichTextFieldToMigrationField = { - [Index in keyof TField]: RichTextBlockNodeToMigrationField +export type RichTextFieldWithMigrationField< + TField extends RichTextField = RichTextField, +> = { + [Index in keyof TField]: RichTextBlockNodeWithMigrationField } /** - * A utility type that converts a regular field to a regular migration field. + * A utility type that extends a regular field with their migration field + * equivalent. * * @typeParam TField - Regular field type to convert. */ -type RegularFieldToMigrationField = +type RegularFieldWithMigrationField< + TField extends AnyRegularField = AnyRegularField, +> = | (TField extends ImageField - ? ImageMigrationField - : TField extends LinkField - ? LinkMigrationField - : TField extends RichTextField - ? RichTextFieldToMigrationField - : never) + ? MigrationImage | undefined + : TField extends FilledLinkToMediaField + ? MigrationLinkToMedia | undefined + : TField extends FilledContentRelationshipField + ? + | MigrationContentRelationship + | UnresolvedMigrationContentRelationshipConfig + | undefined + : TField extends RichTextField + ? RichTextFieldWithMigrationField + : never) | TField /** - * A utility type that converts a group field to a group migration field. + * A utility type that extends a group's fields with their migration fields + * equivalent. * * @typeParam TField - Group field type to convert. */ -export type GroupFieldToMigrationField< - TField extends GroupField | Slice["items"] | SharedSlice["items"], -> = FieldsToMigrationFields[] - -type SliceToMigrationField = Omit< - TField, - "primary" | "items" -> & { - primary: FieldsToMigrationFields - items: GroupFieldToMigrationField +type GroupFieldWithMigrationField< + TField extends GroupField | Slice["items"] | SharedSlice["items"] = + | GroupField + | Slice["items"] + | SharedSlice["items"], +> = FieldsWithMigrationFields[] + +type SliceWithMigrationField< + TField extends Slice | SharedSlice = Slice | SharedSlice, +> = Omit & { + primary: FieldsWithMigrationFields + items: GroupFieldWithMigrationField } /** - * A utility type that converts a field to a migration field. + * A utility type that extends a SliceZone's slices fields with their migration + * fields equivalent. * * @typeParam TField - Field type to convert. */ -export type SliceZoneToMigrationField = - SliceToMigrationField[] +type SliceZoneWithMigrationField = + SliceWithMigrationField[] /** - * A utility type that converts a field to a migration field. + * A utility type that extends any field with their migration field equivalent. * * @typeParam TField - Field type to convert. */ -export type FieldToMigrationField< - TField extends AnyRegularField | GroupField | SliceZone, +export type FieldWithMigrationField< + TField extends AnyRegularField | GroupField | SliceZone = + | AnyRegularField + | GroupField + | SliceZone, > = TField extends AnyRegularField - ? RegularFieldToMigrationField + ? RegularFieldWithMigrationField : TField extends GroupField - ? GroupFieldToMigrationField + ? GroupFieldWithMigrationField : TField extends SliceZone - ? SliceZoneToMigrationField + ? SliceZoneWithMigrationField : never /** - * A utility type that converts a record of fields to a record of migration - * fields. + * A utility type that extends a record of fields with their migration fields + * equivalent. * * @typeParam TFields - Type of the record of Prismic fields. */ -export type FieldsToMigrationFields< - TFields extends Record, +export type FieldsWithMigrationFields< + TFields extends Record< + string, + AnyRegularField | GroupField | SliceZone + > = Record, > = { - [Key in keyof TFields]: FieldToMigrationField + [Key in keyof TFields]: FieldWithMigrationField } /** @@ -191,7 +226,7 @@ export type PrismicMigrationDocument< /** * Data contained in the document. */ - data: FieldsToMigrationFields + data: FieldsWithMigrationFields }> : never @@ -213,11 +248,52 @@ export type PrismicMigrationDocumentParams = { // it creates a circular reference to itself which makes TypeScript unhappy. // (but I think it's weird and it doesn't make sense :thinking:) masterLanguageDocument?: - | PrismicDocument - | PrismicMigrationDocument - | (() => - | Promise - | PrismicDocument - | PrismicMigrationDocument - | undefined) + | UnresolvedMigrationContentRelationshipConfig + | undefined +} + +export class MigrationDocument< + TDocument extends PrismicDocument = PrismicDocument, +> { + document: PrismicMigrationDocument + params: PrismicMigrationDocumentParams + dependencies: MigrationField[] + + constructor( + document: PrismicMigrationDocument, + params: PrismicMigrationDocumentParams, + dependencies: MigrationField[] = [], + ) { + this.document = document + this.params = params + this.dependencies = dependencies + } + + /** + * @internal + */ + async _prepare(args: { + assets: AssetMap + documents: DocumentMap + }): Promise { + for (const dependency of this.dependencies) { + await dependency._prepare(args) + } + } } + +/** + * A map of document IDs, documents, and migraiton documents to content + * relationship field used to resolve content relationships when patching + * migration Prismic documents. + * + * @typeParam TDocuments - Type of Prismic documents in the repository. + * + * @internal + */ +export type DocumentMap = + Map< + string | MigrationDocument | MigrationDocument, + | PrismicDocument + | (Omit, "id"> & { id: string }) + > diff --git a/src/types/migration/fields.ts b/src/types/migration/fields.ts deleted file mode 100644 index 6b83c53b..00000000 --- a/src/types/migration/fields.ts +++ /dev/null @@ -1,57 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { ContentRelationshipField } from "../value/contentRelationship" -import type { PrismicDocument } from "../value/document" -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { ImageField } from "../value/image" -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { LinkField } from "../value/link" -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { LinkToMediaField } from "../value/linkToMedia" - -import type { MigrationAsset } from "./asset" -import type { PrismicMigrationDocument } from "./document" - -export const MigrationFieldType = { - Image: "image", - LinkToMedia: "linkToMedia", -} as const - -/** - * An alternate version of the {@link ImageField} for use with the Migration API. - */ -export type ImageMigrationField = - | (MigrationAsset & { - migrationType: typeof MigrationFieldType.Image - }) - | undefined - -/** - * An alternate version of the {@link LinkToMediaField} for use with the - * Migration API. - */ -export type LinkToMediaMigrationField = - | (MigrationAsset & { - migrationType: typeof MigrationFieldType.LinkToMedia - }) - | undefined - -/** - * An alternate version of the {@link ContentRelationshipField} for use with the - * Migration API. - */ -export type ContentRelationshipMigrationField = - | PrismicDocument - | PrismicMigrationDocument - | (() => - | Promise - | PrismicDocument - | PrismicMigrationDocument - | undefined) - | undefined - -/** - * An alternate version of the {@link LinkField} for use with the Migration API. - */ -export type LinkMigrationField = - | LinkToMediaMigrationField - | ContentRelationshipMigrationField diff --git a/src/types/migration/richText.ts b/src/types/migration/richText.ts deleted file mode 100644 index 9196ee88..00000000 --- a/src/types/migration/richText.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { RTImageNode, RTLinkNode } from "../value/richText" - -import type { ImageMigrationField, LinkMigrationField } from "./fields" - -/** - * An alternate version of {@link RTLinkNode} that supports - * {@link LinkMigrationField} for use with the Migration API. - */ -export type RTLinkMigrationNode = Omit & { - data: LinkMigrationField -} - -/** - * An alternate version of {@link RTImageNode} that supports - * {@link ImageMigrationField} for use with the Migration API. - */ -export type RTImageMigrationNode = ImageMigrationField & { - linkTo?: RTImageNode["linkTo"] | LinkMigrationField -} diff --git a/test/__snapshots__/writeClient-migrate-patch-link.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap similarity index 54% rename from test/__snapshots__/writeClient-migrate-patch-link.test.ts.snap rename to test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap index 114003d9..739bcda2 100644 --- a/test/__snapshots__/writeClient-migrate-patch-link.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap @@ -3,19 +3,7 @@ exports[`patches link fields > brokenLink > group 1`] = ` { "group": [ - { - "field": { - "id": "adcdf90", - "isBroken": true, - "lang": "nulla", - "link_type": "Document", - "tags": [ - "Convallis Posuere", - ], - "type": "vitae", - "url": "https://ac.example", - }, - }, + {}, ], } `; @@ -26,42 +14,11 @@ exports[`patches link fields > brokenLink > shared slice 1`] = ` { "id": "2ff58c741ba", "items": [ - { - "field": { - "id": "9be9130", - "isBroken": true, - "lang": "faucibus", - "link_type": "Document", - "tags": [ - "Ultrices", - ], - "type": "egestas_integer", - "url": "https://scelerisque.example", - }, - }, + {}, ], "primary": { - "field": { - "id": "06ad9e3", - "isBroken": true, - "lang": "fringilla", - "link_type": "Document", - "tags": [], - "type": "amet", - "url": "https://nec.example", - }, "group": [ - { - "field": { - "id": "bd0255b", - "isBroken": true, - "lang": "neque", - "link_type": "Document", - "tags": [], - "type": "lacus", - "url": "https://cras.example", - }, - }, + {}, ], }, "slice_label": null, @@ -79,29 +36,9 @@ exports[`patches link fields > brokenLink > slice 1`] = ` { "id": "e93cc3e4f28", "items": [ - { - "field": { - "id": "5ec2f4d", - "isBroken": true, - "lang": "enim,", - "link_type": "Document", - "tags": [], - "type": "felis_donec", - "url": "https://sem.example", - }, - }, + {}, ], - "primary": { - "field": { - "id": "31a1bd2", - "isBroken": true, - "lang": "ac", - "link_type": "Document", - "tags": [], - "type": "enim_praesent", - "url": "https://sit.example", - }, - }, + "primary": {}, "slice_label": "Pellentesque", "slice_type": "nulla_posuere", }, @@ -109,19 +46,7 @@ exports[`patches link fields > brokenLink > slice 1`] = ` } `; -exports[`patches link fields > brokenLink > static zone 1`] = ` -{ - "field": { - "id": "2adafa9", - "isBroken": true, - "lang": "elementum", - "link_type": "Document", - "tags": [], - "type": "aenean", - "url": "https://volutpat.example", - }, -} -`; +exports[`patches link fields > brokenLink > static zone 1`] = `{}`; exports[`patches link fields > existing > group 1`] = ` { @@ -743,13 +668,13 @@ exports[`patches link fields > otherRepositoryContentRelationship > group 1`] = "field": { "id": "id-migration", "isBroken": false, - "lang": "nulla", + "lang": "a", "link_type": "Document", "tags": [ - "Convallis Posuere", + "Odio Eu", ], - "type": "vitae", - "url": "https://ac.example", + "type": "amet", + "uid": "Non sodales neque", }, }, ], @@ -766,13 +691,11 @@ exports[`patches link fields > otherRepositoryContentRelationship > shared slice "field": { "id": "id-migration", "isBroken": false, - "lang": "faucibus", + "lang": "gravida", "link_type": "Document", - "tags": [ - "Ultrices", - ], - "type": "egestas_integer", - "url": "https://scelerisque.example", + "tags": [], + "type": "leo", + "uid": "Blandit volutpat maecenas", }, }, ], @@ -780,22 +703,22 @@ exports[`patches link fields > otherRepositoryContentRelationship > shared slice "field": { "id": "id-migration", "isBroken": false, - "lang": "fringilla", + "lang": "gravida", "link_type": "Document", "tags": [], - "type": "amet", - "url": "https://nec.example", + "type": "leo", + "uid": "Blandit volutpat maecenas", }, "group": [ { "field": { "id": "id-migration", "isBroken": false, - "lang": "neque", + "lang": "gravida", "link_type": "Document", "tags": [], - "type": "lacus", - "url": "https://cras.example", + "type": "leo", + "uid": "Blandit volutpat maecenas", }, }, ], @@ -819,11 +742,13 @@ exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = "field": { "id": "id-migration", "isBroken": false, - "lang": "enim,", + "lang": "ultrices", "link_type": "Document", - "tags": [], - "type": "felis_donec", - "url": "https://sem.example", + "tags": [ + "Ipsum Consequat", + ], + "type": "eget", + "uid": "Amet dictum sit", }, }, ], @@ -831,11 +756,13 @@ exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = "field": { "id": "id-migration", "isBroken": false, - "lang": "ac", + "lang": "ultrices", "link_type": "Document", - "tags": [], - "type": "enim_praesent", - "url": "https://sit.example", + "tags": [ + "Ipsum Consequat", + ], + "type": "eget", + "uid": "Amet dictum sit", }, }, "slice_label": "Pellentesque", @@ -850,11 +777,11 @@ exports[`patches link fields > otherRepositoryContentRelationship > static zone "field": { "id": "id-migration", "isBroken": false, - "lang": "elementum", + "lang": "nisi,", "link_type": "Document", "tags": [], - "type": "aenean", - "url": "https://volutpat.example", + "type": "eros", + "uid": "Scelerisque mauris pellentesque", }, } `; @@ -1106,583 +1033,3 @@ exports[`patches link fields > richTextLinkNode > static zone 1`] = ` ], } `; - -exports[`patches link fields > richTextNewImageNodeLink > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "cdfdd322ca9", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "laoreet", - "link_type": "Document", - "tags": [], - "type": "orci", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", - }, - ], - }, - ], -} -`; - -exports[`patches link fields > richTextNewImageNodeLink > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Eu Mi", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - "group": [ - { - "field": [ - { - "spans": [], - "text": "Arcu", - "type": "heading1", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches link fields > richTextNewImageNodeLink > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "7963dc361cc", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "sapien", - "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "7963dc361cc", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "sapien", - "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches link fields > richTextNewImageNodeLink > static zone 1`] = ` -{ - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "0bbad670dad", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "eget", - "link_type": "Document", - "tags": [], - "type": "phasellus", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - }, - ], -} -`; - -exports[`patches link fields > richTextOtherReposiotryImageNodeLink > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", - }, - { - "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#7cfc26", - "x": 2977, - "y": -1163, - "zoom": 1.0472585898934068, - }, - "id": "e8d0985c099", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "laoreet", - "link_type": "Document", - "tags": [], - "type": "orci", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", - }, - ], - }, - ], -} -`; - -exports[`patches link fields > richTextOtherReposiotryImageNodeLink > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Facilisis", - "type": "heading3", - }, - { - "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#37ae3f", - "x": -1505, - "y": 902, - "zoom": 1.8328975606320652, - }, - "id": "3fc0dfa9fe9", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", - }, - { - "alt": "Proin libero nunc consequat interdum varius", - "copyright": null, - "dimensions": { - "height": 3036, - "width": 4554, - }, - "edit": { - "background": "#904f4f", - "x": 1462, - "y": 1324, - "zoom": 1.504938844941775, - }, - "id": "3fc0dfa9fe9", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - "group": [ - { - "field": [ - { - "spans": [], - "text": "Egestas Integer Eget Aliquet Nibh", - "type": "heading5", - }, - { - "alt": "Arcu cursus vitae congue mauris", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#5f7a9b", - "x": -119, - "y": -2667, - "zoom": 1.9681315715350518, - }, - "id": "3fc0dfa9fe9", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "tortor,", - "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches link fields > richTextOtherReposiotryImageNodeLink > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Amet", - "type": "heading5", - }, - { - "alt": "Auctor neque vitae tempus quam", - "copyright": null, - "dimensions": { - "height": 1440, - "width": 2560, - }, - "edit": { - "background": "#5bc5aa", - "x": -1072, - "y": -281, - "zoom": 1.3767766101231744, - }, - "id": "f70ca27104d", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "sapien", - "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", - }, - { - "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", - "copyright": null, - "dimensions": { - "height": 3036, - "width": 4554, - }, - "edit": { - "background": "#4860cb", - "x": 280, - "y": -379, - "zoom": 1.2389796902982004, - }, - "id": "f70ca27104d", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "sapien", - "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", - }, - ], - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches link fields > richTextOtherReposiotryImageNodeLink > static zone 1`] = ` -{ - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", - }, - { - "alt": "Interdum velit euismod in pellentesque", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#ab1d17", - "x": 3605, - "y": 860, - "zoom": 1.9465488211593005, - }, - "id": "04a95cc61c3", - "linkTo": { - "id": "id-existing", - "isBroken": false, - "lang": "eget", - "link_type": "Document", - "tags": [], - "type": "phasellus", - }, - "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", - }, - ], -} -`; diff --git a/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap index daa3e7f0..1a477d27 100644 --- a/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap @@ -15,7 +15,7 @@ exports[`patches image fields > existing > group 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "id-existing", "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", @@ -43,7 +43,7 @@ exports[`patches image fields > existing > shared slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "id-existing", "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", @@ -62,7 +62,7 @@ exports[`patches image fields > existing > shared slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "id-existing", "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", @@ -80,7 +80,7 @@ exports[`patches image fields > existing > shared slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "id-existing", "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", @@ -115,7 +115,7 @@ exports[`patches image fields > existing > slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "id-existing", "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", @@ -134,7 +134,7 @@ exports[`patches image fields > existing > slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "id-existing", "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", @@ -160,7 +160,7 @@ exports[`patches image fields > existing > static zone 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "id-existing", "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", @@ -183,7 +183,7 @@ exports[`patches image fields > new > group 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "ef95d5daa4d", "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", @@ -211,7 +211,7 @@ exports[`patches image fields > new > shared slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "f2c3f8bfc90", "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", @@ -230,7 +230,7 @@ exports[`patches image fields > new > shared slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "f2c3f8bfc90", "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", @@ -248,7 +248,7 @@ exports[`patches image fields > new > shared slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "f2c3f8bfc90", "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", @@ -283,7 +283,7 @@ exports[`patches image fields > new > slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "45306297c5e", "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", @@ -302,7 +302,7 @@ exports[`patches image fields > new > slice 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "45306297c5e", "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", @@ -328,7 +328,7 @@ exports[`patches image fields > new > static zone 1`] = ` "background": "transparent", "x": 0, "y": 0, - "zoom": 0, + "zoom": 1, }, "id": "c5c95f8d3ac", "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", @@ -344,8 +344,8 @@ exports[`patches image fields > otherRepository > group 1`] = ` "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", "copyright": null, "dimensions": { - "height": 1705, - "width": 2560, + "height": 1, + "width": 1, }, "edit": { "background": "#fdb338", @@ -372,8 +372,8 @@ exports[`patches image fields > otherRepository > shared slice 1`] = ` "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#61adcf", @@ -391,8 +391,8 @@ exports[`patches image fields > otherRepository > shared slice 1`] = ` "alt": "Ut consequat semper viverra nam libero justo laoreet", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#8efd5a", @@ -409,8 +409,8 @@ exports[`patches image fields > otherRepository > shared slice 1`] = ` "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { - "height": 4392, - "width": 7372, + "height": 1, + "width": 1, }, "edit": { "background": "#bc3178", @@ -444,8 +444,8 @@ exports[`patches image fields > otherRepository > slice 1`] = ` "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#c361cc", @@ -463,8 +463,8 @@ exports[`patches image fields > otherRepository > slice 1`] = ` "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#55e3be", @@ -489,8 +489,8 @@ exports[`patches image fields > otherRepository > static zone 1`] = ` "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#97acc6", @@ -508,7 +508,13 @@ exports[`patches image fields > otherRepositoryEmpty > group 1`] = ` { "group": [ { - "field": {}, + "field": { + "alt": null, + "copyright": null, + "dimensions": null, + "id": null, + "url": null, + }, }, ], } @@ -521,14 +527,32 @@ exports[`patches image fields > otherRepositoryEmpty > shared slice 1`] = ` "id": "2ff58c741ba", "items": [ { - "field": {}, + "field": { + "alt": null, + "copyright": null, + "dimensions": null, + "id": null, + "url": null, + }, }, ], "primary": { - "field": {}, + "field": { + "alt": null, + "copyright": null, + "dimensions": null, + "id": null, + "url": null, + }, "group": [ { - "field": {}, + "field": { + "alt": null, + "copyright": null, + "dimensions": null, + "id": null, + "url": null, + }, }, ], }, @@ -548,11 +572,23 @@ exports[`patches image fields > otherRepositoryEmpty > slice 1`] = ` "id": "e93cc3e4f28", "items": [ { - "field": {}, + "field": { + "alt": null, + "copyright": null, + "dimensions": null, + "id": null, + "url": null, + }, }, ], "primary": { - "field": {}, + "field": { + "alt": null, + "copyright": null, + "dimensions": null, + "id": null, + "url": null, + }, }, "slice_label": "Pellentesque", "slice_type": "nulla_posuere", @@ -563,7 +599,13 @@ exports[`patches image fields > otherRepositoryEmpty > slice 1`] = ` exports[`patches image fields > otherRepositoryEmpty > static zone 1`] = ` { - "field": {}, + "field": { + "alt": null, + "copyright": null, + "dimensions": null, + "id": null, + "url": null, + }, } `; @@ -575,8 +617,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > group 1`] = ` "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", "copyright": null, "dimensions": { - "height": 1705, - "width": 2560, + "height": 1, + "width": 1, }, "edit": { "background": "#fdb338", @@ -589,8 +631,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > group 1`] = ` "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", "copyright": null, "dimensions": { - "height": 1705, - "width": 2560, + "height": 1, + "width": 1, }, "edit": { "background": "#fdb338", @@ -619,8 +661,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#61adcf", @@ -633,8 +675,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#61adcf", @@ -654,8 +696,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] "alt": "Ut consequat semper viverra nam libero justo laoreet", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#8efd5a", @@ -668,8 +710,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] "alt": "Ut consequat semper viverra nam libero justo laoreet", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#8efd5a", @@ -688,8 +730,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { - "height": 4392, - "width": 7372, + "height": 1, + "width": 1, }, "edit": { "background": "#bc3178", @@ -702,8 +744,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { - "height": 4392, - "width": 7372, + "height": 1, + "width": 1, }, "edit": { "background": "#bc3178", @@ -739,8 +781,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#c361cc", @@ -753,8 +795,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#c361cc", @@ -774,8 +816,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#55e3be", @@ -788,8 +830,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#55e3be", @@ -816,8 +858,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > static zone 1`] "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#97acc6", @@ -830,8 +872,8 @@ exports[`patches image fields > otherRepositoryWithThumbnails > static zone 1`] "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#97acc6", @@ -855,8 +897,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = "alt": null, "copyright": null, "dimensions": { - "height": 1705, - "width": 2560, + "height": 1, + "width": 1, }, "edit": { "background": "#fdb338", @@ -869,8 +911,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = "alt": null, "copyright": null, "dimensions": { - "height": 1705, - "width": 2560, + "height": 1, + "width": 1, }, "edit": { "background": "#fdb338", @@ -899,8 +941,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "alt": null, "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#61adcf", @@ -913,8 +955,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "alt": null, "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#61adcf", @@ -934,8 +976,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "alt": null, "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#8efd5a", @@ -948,8 +990,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "alt": null, "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#8efd5a", @@ -968,8 +1010,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "alt": null, "copyright": null, "dimensions": { - "height": 4392, - "width": 7372, + "height": 1, + "width": 1, }, "edit": { "background": "#bc3178", @@ -982,8 +1024,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "alt": null, "copyright": null, "dimensions": { - "height": 4392, - "width": 7372, + "height": 1, + "width": 1, }, "edit": { "background": "#bc3178", @@ -1019,8 +1061,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "alt": null, "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#c361cc", @@ -1033,8 +1075,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "alt": null, "copyright": null, "dimensions": { - "height": 4000, - "width": 6000, + "height": 1, + "width": 1, }, "edit": { "background": "#c361cc", @@ -1054,8 +1096,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "alt": null, "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#55e3be", @@ -1068,8 +1110,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "alt": null, "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#55e3be", @@ -1096,8 +1138,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone "alt": null, "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#97acc6", @@ -1110,8 +1152,8 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone "alt": null, "copyright": null, "dimensions": { - "height": 2832, - "width": 4240, + "height": 1, + "width": 1, }, "edit": { "background": "#97acc6", @@ -1127,123 +1169,155 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone } `; -exports[`patches image fields > richTextExisting > group 1`] = ` +exports[`patches image fields > otherRepositoryWithTypeThumbnail > group 1`] = ` { "group": [ { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", + "field": { + "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, }, - { - "alt": null, + "edit": { + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, + }, + "id": "4dcf07cfc26", + "type": { + "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, + "background": "#fdb338", + "x": 349, + "y": 115, + "zoom": 1.5951464943576479, }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "id": "4dcf07cfc26", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", }, - ], + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", + }, }, ], } `; -exports[`patches image fields > richTextExisting > shared slice 1`] = ` +exports[`patches image fields > otherRepositoryWithTypeThumbnail > shared slice 1`] = ` { "slices": [ { "id": "2ff58c741ba", "items": [ { - "field": [ - { - "spans": [], - "text": "Eu Mi", - "type": "heading3", + "field": { + "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, }, - { - "alt": null, + "edit": { + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, + }, + "id": "f5dbf0d9ed2", + "type": { + "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, + "background": "#61adcf", + "x": -1586, + "y": -2819, + "zoom": 1.9393723758262054, }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", }, - ], + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, }, ], "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", + "field": { + "alt": "Ut consequat semper viverra nam libero justo laoreet", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, }, - { - "alt": null, + "edit": { + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, + }, + "id": "f5dbf0d9ed2", + "type": { + "alt": "Ut consequat semper viverra nam libero justo laoreet", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, + "background": "#8efd5a", + "x": 466, + "y": -571, + "zoom": 1.394663850868741, }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", }, - ], + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, "group": [ { - "field": [ - { - "spans": [], - "text": "Arcu", - "type": "heading1", + "field": { + "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, }, - { - "alt": null, + "edit": { + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, + }, + "id": "f5dbf0d9ed2", + "type": { + "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, + "background": "#bc3178", + "x": 1156, + "y": -2223, + "zoom": 1.602826201962965, }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "id": "f5dbf0d9ed2", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", }, - ], + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + }, }, ], }, @@ -1256,288 +1330,80 @@ exports[`patches image fields > richTextExisting > shared slice 1`] = ` } `; -exports[`patches image fields > richTextExisting > slice 1`] = ` +exports[`patches image fields > otherRepositoryWithTypeThumbnail > slice 1`] = ` { "slices": [ { "id": "e93cc3e4f28", "items": [ { - "field": [ - { - "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", + "field": { + "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, }, - { - "alt": null, + "edit": { + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, + }, + "id": "4f4a69ff72d", + "type": { + "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, + "background": "#c361cc", + "x": -751, + "y": -404, + "zoom": 1.6465649620272602, }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", }, - ], + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + }, }, ], "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", + "field": { + "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, }, - { - "alt": null, + "edit": { + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, + }, + "id": "4f4a69ff72d", + "type": { + "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, + "background": "#55e3be", + "x": -762, + "y": 991, + "zoom": 1.0207384987782544, }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", - }, - ], - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > richTextExisting > static zone 1`] = ` -{ - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "id-existing", - "type": "image", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", - }, - ], -} -`; - -exports[`patches image fields > richTextNew > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, + "id": "4f4a69ff72d", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", }, - "id": "cdfdd322ca9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", - }, - ], - }, - ], -} -`; - -exports[`patches image fields > richTextNew > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Eu Mi", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - "group": [ - { - "field": [ - { - "spans": [], - "text": "Arcu", - "type": "heading1", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "bc31787853a", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > richTextNew > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "7963dc361cc", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", - }, - { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "7963dc361cc", - "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - }, - ], }, "slice_label": "Pellentesque", "slice_type": "nulla_posuere", @@ -1546,255 +1412,39 @@ exports[`patches image fields > richTextNew > slice 1`] = ` } `; -exports[`patches image fields > richTextNew > static zone 1`] = ` +exports[`patches image fields > otherRepositoryWithTypeThumbnail > static zone 1`] = ` { - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", + "field": { + "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, }, - { - "alt": null, + "edit": { + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, + }, + "id": "9abffab1d17", + "type": { + "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 0, - }, - "id": "0bbad670dad", - "type": "image", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - }, - ], -} -`; - -exports[`patches image fields > richTextOtherRepository > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", - }, - { - "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", - "copyright": null, - "dimensions": { - "height": 4000, - "width": 6000, - }, - "edit": { - "background": "#7cfc26", - "x": 2977, - "y": -1163, - "zoom": 1.0472585898934068, - }, - "id": "e8d0985c099", - "type": "image", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", - }, - ], - }, - ], -} -`; - -exports[`patches image fields > richTextOtherRepository > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Facilisis", - "type": "heading3", - }, - { - "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#37ae3f", - "x": -1505, - "y": 902, - "zoom": 1.8328975606320652, - }, - "id": "3fc0dfa9fe9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", - }, - { - "alt": "Proin libero nunc consequat interdum varius", - "copyright": null, - "dimensions": { - "height": 3036, - "width": 4554, - }, - "edit": { - "background": "#904f4f", - "x": 1462, - "y": 1324, - "zoom": 1.504938844941775, - }, - "id": "3fc0dfa9fe9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - "group": [ - { - "field": [ - { - "spans": [], - "text": "Egestas Integer Eget Aliquet Nibh", - "type": "heading5", - }, - { - "alt": "Arcu cursus vitae congue mauris", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#5f7a9b", - "x": -119, - "y": -2667, - "zoom": 1.9681315715350518, - }, - "id": "3fc0dfa9fe9", - "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches image fields > richTextOtherRepository > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": [ - { - "spans": [], - "text": "Amet", - "type": "heading5", - }, - { - "alt": "Auctor neque vitae tempus quam", - "copyright": null, - "dimensions": { - "height": 1440, - "width": 2560, - }, - "edit": { - "background": "#5bc5aa", - "x": -1072, - "y": -281, - "zoom": 1.3767766101231744, - }, - "id": "f70ca27104d", - "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", - }, - { - "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", - "copyright": null, - "dimensions": { - "height": 3036, - "width": 4554, - }, - "edit": { - "background": "#4860cb", - "x": 280, - "y": -379, - "zoom": 1.2389796902982004, - }, - "id": "f70ca27104d", - "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", - }, - ], - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches image fields > richTextOtherRepository > static zone 1`] = ` -{ - "field": [ - { - "spans": [], - "text": "Elementum Integer", - "type": "heading6", - }, - { - "alt": "Interdum velit euismod in pellentesque", - "copyright": null, - "dimensions": { - "height": 4392, - "width": 7372, - }, - "edit": { - "background": "#ab1d17", - "x": 3605, - "y": 860, - "zoom": 1.9465488211593005, + "background": "#97acc6", + "x": 1922, + "y": -967, + "zoom": 1.9765406541937358, }, - "id": "04a95cc61c3", - "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + "id": "9abffab1d17", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", }, - ], + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + }, } `; diff --git a/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap index 7abdb040..2624fb3a 100644 --- a/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap @@ -369,14 +369,14 @@ exports[`patches link to media fields > otherRepository > group 1`] = ` "group": [ { "field": { - "height": "1715", + "height": "1", "id": "fdb33894dcf", - "kind": "purus", + "kind": "image", "link_type": "Media", - "name": "nulla.example", - "size": "2039", - "url": "https://db0f6f37ebeb6ea09489124345af2a45.example.com/foo.png", - "width": "2091", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "width": "1", }, }, ], @@ -391,39 +391,39 @@ exports[`patches link to media fields > otherRepository > shared slice 1`] = ` "items": [ { "field": { - "height": "2741", + "height": "1", "id": "ba97bfb16bf", - "kind": "quis", + "kind": "image", "link_type": "Media", - "name": "senectus.example", - "size": "844", - "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", - "width": "749", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "width": "1", }, }, ], "primary": { "field": { - "height": "1276", + "height": "1", "id": "ba97bfb16bf", - "kind": "ullamcorper", + "kind": "image", "link_type": "Media", - "name": "fringilla.example", - "size": "818", - "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", - "width": "2024", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "width": "1", }, "group": [ { "field": { - "height": "2964", + "height": "1", "id": "ba97bfb16bf", - "kind": "pellentesque", + "kind": "image", "link_type": "Media", - "name": "cras.example", - "size": "1564", - "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", - "width": "599", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "width": "1", }, }, ], @@ -445,27 +445,27 @@ exports[`patches link to media fields > otherRepository > slice 1`] = ` "items": [ { "field": { - "height": "1500", + "height": "1", "id": "a57963dc361", - "kind": "quam", + "kind": "image", "link_type": "Media", - "name": "integer.example", - "size": "1043", - "url": "https://6d52012dca4fc77aa554f25430aef501.example.com/foo.png", - "width": "737", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "width": "1", }, }, ], "primary": { "field": { - "height": "761", + "height": "1", "id": "a57963dc361", - "kind": "nibh", + "kind": "image", "link_type": "Media", - "name": "ac.example", - "size": "2757", - "url": "https://6d52012dca4fc77aa554f25430aef501.example.com/foo.png", - "width": "1300", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "width": "1", }, }, "slice_label": "Pellentesque", @@ -478,91 +478,14 @@ exports[`patches link to media fields > otherRepository > slice 1`] = ` exports[`patches link to media fields > otherRepository > static zone 1`] = ` { "field": { - "height": "740", + "height": "1", "id": "97acc6e9abf", - "kind": "suspendisse", + "kind": "image", "link_type": "Media", - "name": "elementum.example", - "size": "556", - "url": "https://7713b5e0c356963c79ccf86c6ff1710c.example.com/foo.png", - "width": "2883", - }, -} -`; - -exports[`patches link to media fields > otherRepositoryNotFoundID > group 1`] = ` -{ - "group": [ - { - "field": { - "link_type": "Any", - }, - }, - ], -} -`; - -exports[`patches link to media fields > otherRepositoryNotFoundID > shared slice 1`] = ` -{ - "slices": [ - { - "id": "2ff58c741ba", - "items": [ - { - "field": { - "link_type": "Any", - }, - }, - ], - "primary": { - "field": { - "link_type": "Any", - }, - "group": [ - { - "field": { - "link_type": "Any", - }, - }, - ], - }, - "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", - }, - ], -} -`; - -exports[`patches link to media fields > otherRepositoryNotFoundID > slice 1`] = ` -{ - "slices": [ - { - "id": "e93cc3e4f28", - "items": [ - { - "field": { - "link_type": "Any", - }, - }, - ], - "primary": { - "field": { - "link_type": "Any", - }, - }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", - }, - ], -} -`; - -exports[`patches link to media fields > otherRepositoryNotFoundID > static zone 1`] = ` -{ - "field": { - "link_type": "Any", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "width": "1", }, } `; @@ -1085,14 +1008,14 @@ exports[`patches link to media fields > richTextOtherRepository > group 1`] = ` }, { "data": { - "height": "1715", + "height": "1", "id": "fdb33894dcf", - "kind": "purus", + "kind": "image", "link_type": "Media", - "name": "nulla.example", - "size": "2039", - "url": "https://db0f6f37ebeb6ea09489124345af2a45.example.com/foo.png", - "width": "2091", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "width": "1", }, "end": 5, "start": 0, @@ -1125,14 +1048,14 @@ exports[`patches link to media fields > richTextOtherRepository > shared slice 1 }, { "data": { - "height": "2741", + "height": "1", "id": "ba97bfb16bf", - "kind": "quis", + "kind": "image", "link_type": "Media", - "name": "senectus.example", - "size": "844", - "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", - "width": "749", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "width": "1", }, "end": 5, "start": 0, @@ -1156,14 +1079,14 @@ exports[`patches link to media fields > richTextOtherRepository > shared slice 1 }, { "data": { - "height": "1276", + "height": "1", "id": "ba97bfb16bf", - "kind": "ullamcorper", + "kind": "image", "link_type": "Media", - "name": "fringilla.example", - "size": "818", - "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", - "width": "2024", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "width": "1", }, "end": 5, "start": 0, @@ -1186,14 +1109,14 @@ exports[`patches link to media fields > richTextOtherRepository > shared slice 1 }, { "data": { - "height": "2964", + "height": "1", "id": "ba97bfb16bf", - "kind": "pellentesque", + "kind": "image", "link_type": "Media", - "name": "cras.example", - "size": "1564", - "url": "https://515f7ca1126821ccd3eb8b013a857528.example.com/foo.png", - "width": "599", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "width": "1", }, "end": 5, "start": 0, @@ -1233,14 +1156,14 @@ exports[`patches link to media fields > richTextOtherRepository > slice 1`] = ` }, { "data": { - "height": "1500", + "height": "1", "id": "a57963dc361", - "kind": "quam", + "kind": "image", "link_type": "Media", - "name": "integer.example", - "size": "1043", - "url": "https://6d52012dca4fc77aa554f25430aef501.example.com/foo.png", - "width": "737", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "width": "1", }, "end": 5, "start": 0, @@ -1264,14 +1187,14 @@ exports[`patches link to media fields > richTextOtherRepository > slice 1`] = ` }, { "data": { - "height": "761", + "height": "1", "id": "a57963dc361", - "kind": "nibh", + "kind": "image", "link_type": "Media", - "name": "ac.example", - "size": "2757", - "url": "https://6d52012dca4fc77aa554f25430aef501.example.com/foo.png", - "width": "1300", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "width": "1", }, "end": 5, "start": 0, @@ -1302,14 +1225,14 @@ exports[`patches link to media fields > richTextOtherRepository > static zone 1` }, { "data": { - "height": "740", + "height": "1", "id": "97acc6e9abf", - "kind": "suspendisse", + "kind": "image", "link_type": "Media", - "name": "elementum.example", - "size": "556", - "url": "https://7713b5e0c356963c79ccf86c6ff1710c.example.com/foo.png", - "width": "2883", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "width": "1", }, "end": 5, "start": 0, diff --git a/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap new file mode 100644 index 00000000..28421765 --- /dev/null +++ b/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap @@ -0,0 +1,1253 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`patches rich text image nodes > existing > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches rich text image nodes > existing > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches rich text image nodes > existing > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches rich text image nodes > existing > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "id-existing", + "type": "image", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + }, + ], +} +`; + +exports[`patches rich text image nodes > new > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "cdfdd322ca9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches rich text image nodes > new > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "bc31787853a", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches rich text image nodes > new > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "7963dc361cc", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "7963dc361cc", + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches rich text image nodes > new > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "0bbad670dad", + "type": "image", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + }, + ], +} +`; + +exports[`patches rich text image nodes > newLinkTo > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "cdfdd322ca9", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "laoreet", + "link_type": "Document", + "tags": [], + "type": "orci", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", + }, + ], + }, + ], +} +`; + +exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Eu Mi", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "bc31787853a", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "bc31787853a", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Arcu", + "type": "heading1", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "bc31787853a", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Habitasse platea dictumst quisque sagittis purus sit", + "type": "o-list-item", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "7963dc361cc", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "7963dc361cc", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches rich text image nodes > newLinkTo > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "0bbad670dad", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "eget", + "link_type": "Document", + "tags": [], + "type": "phasellus", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + }, + ], +} +`; + +exports[`patches rich text image nodes > otherRepository > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#7cfc26", + "x": 2977, + "y": -1163, + "zoom": 1.0472585898934068, + }, + "id": "e8d0985c099", + "type": "image", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + }, + ], + }, + ], +} +`; + +exports[`patches rich text image nodes > otherRepository > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Facilisis", + "type": "heading3", + }, + { + "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#37ae3f", + "x": -1505, + "y": 902, + "zoom": 1.8328975606320652, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": "Proin libero nunc consequat interdum varius", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#904f4f", + "x": 1462, + "y": 1324, + "zoom": 1.504938844941775, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Egestas Integer Eget Aliquet Nibh", + "type": "heading5", + }, + { + "alt": "Arcu cursus vitae congue mauris", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#5f7a9b", + "x": -119, + "y": -2667, + "zoom": 1.9681315715350518, + }, + "id": "3fc0dfa9fe9", + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches rich text image nodes > otherRepository > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Amet", + "type": "heading5", + }, + { + "alt": "Auctor neque vitae tempus quam", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#5bc5aa", + "x": -1072, + "y": -281, + "zoom": 1.3767766101231744, + }, + "id": "f70ca27104d", + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#4860cb", + "x": 280, + "y": -379, + "zoom": 1.2389796902982004, + }, + "id": "f70ca27104d", + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches rich text image nodes > otherRepository > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": "Interdum velit euismod in pellentesque", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#ab1d17", + "x": 3605, + "y": 860, + "zoom": 1.9465488211593005, + }, + "id": "04a95cc61c3", + "type": "image", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + }, + ], +} +`; + +exports[`patches rich text image nodes > otherRepositoryLinkTo > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [], + "text": "Nulla Aliquet Porttitor Lacus Luctus", + "type": "heading2", + }, + { + "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#7cfc26", + "x": 2977, + "y": -1163, + "zoom": 1.0472585898934068, + }, + "id": "e8d0985c099", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "laoreet", + "link_type": "Document", + "tags": [], + "type": "orci", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + }, + ], + }, + ], +} +`; + +exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1`] = ` +{ + "slices": [ + { + "id": "2ff58c741ba", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Facilisis", + "type": "heading3", + }, + { + "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#37ae3f", + "x": -1505, + "y": 902, + "zoom": 1.8328975606320652, + }, + "id": "3fc0dfa9fe9", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", + "type": "heading3", + }, + { + "alt": "Proin libero nunc consequat interdum varius", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#904f4f", + "x": 1462, + "y": 1324, + "zoom": 1.504938844941775, + }, + "id": "3fc0dfa9fe9", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + "group": [ + { + "field": [ + { + "spans": [], + "text": "Egestas Integer Eget Aliquet Nibh", + "type": "heading5", + }, + { + "alt": "Arcu cursus vitae congue mauris", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#5f7a9b", + "x": -119, + "y": -2667, + "zoom": 1.9681315715350518, + }, + "id": "3fc0dfa9fe9", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "tortor,", + "link_type": "Document", + "tags": [ + "Risus In", + ], + "type": "dui", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "at", + "variation": "tortor", + "version": "a79b9dd", + }, + ], +} +`; + +exports[`patches rich text image nodes > otherRepositoryLinkTo > slice 1`] = ` +{ + "slices": [ + { + "id": "e93cc3e4f28", + "items": [ + { + "field": [ + { + "spans": [], + "text": "Amet", + "type": "heading5", + }, + { + "alt": "Auctor neque vitae tempus quam", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#5bc5aa", + "x": -1072, + "y": -281, + "zoom": 1.3767766101231744, + }, + "id": "f70ca27104d", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [], + "text": "Ac Orci Phasellus Egestas Tellus", + "type": "heading5", + }, + { + "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#4860cb", + "x": 280, + "y": -379, + "zoom": 1.2389796902982004, + }, + "id": "f70ca27104d", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "sapien", + "link_type": "Document", + "tags": [ + "Aenean", + ], + "type": "amet", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + }, + ], + }, + "slice_label": "Pellentesque", + "slice_type": "nulla_posuere", + }, + ], +} +`; + +exports[`patches rich text image nodes > otherRepositoryLinkTo > static zone 1`] = ` +{ + "field": [ + { + "spans": [], + "text": "Elementum Integer", + "type": "heading6", + }, + { + "alt": "Interdum velit euismod in pellentesque", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#ab1d17", + "x": 3605, + "y": 860, + "zoom": 1.9465488211593005, + }, + "id": "04a95cc61c3", + "linkTo": { + "id": "id-existing", + "isBroken": false, + "lang": "eget", + "link_type": "Document", + "tags": [], + "type": "phasellus", + }, + "type": "image", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + }, + ], +} +`; diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index 02187d2b..ab9ef6b1 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -108,7 +108,7 @@ export const mockPrismicAssetAPI = ( validateHeaders(req) - const index = Number.parseInt(req.url.searchParams.get("cursor") ?? "0") + const index = Number.parseInt(req.url.searchParams.get("cursor") || "0") const items: Asset[] = assetsDatabase[index] || [] let missing_ids: string[] | undefined @@ -143,7 +143,7 @@ export const mockPrismicAssetAPI = ( validateHeaders(req) const response: PostAssetResult = - args.newAssets?.shift() ?? mockAsset(args.ctx) + args.newAssets?.shift() || mockAsset(args.ctx) // Save the asset in DB assetsDatabase.push([response]) diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index be1a42a7..4a4c1a32 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -22,9 +22,7 @@ type GetDataArgs = { migration: prismic.Migration existingAssets: Asset[] existingDocuments: prismic.PrismicDocument[] - migrationDocuments: (Omit & { - uid: string - })[] + migrationDocuments: prismic.MigrationDocument[] mockedDomain: string } @@ -82,16 +80,20 @@ const internalTestMigrationFieldPatching = ( const migration = prismic.createMigration() + const migrationOtherDocument = migration.createDocument( + otherDocument, + "other", + ) + newDocument.data = args.getData({ ctx, migration, existingAssets: assetsDatabase.flat(), existingDocuments: queryResponse[0].results, - migrationDocuments: [otherDocument], + migrationDocuments: [migrationOtherDocument], mockedDomain, }) - migration.createDocument(otherDocument, "other") migration.createDocument(newDocument, "new") // We speed up the internal rate limiter to make these tests run faster (from 4500ms to nearly instant) diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index df7b98dc..960d2c0a 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -3,7 +3,8 @@ import { describe, expect, it } from "vitest" import type { MockFactory } from "@prismicio/mock" import * as prismic from "../src" -import type { MigrationAsset } from "../src/types/migration/asset" +import type { MigrationAssetConfig } from "../src/types/migration/Asset" +import { MigrationDocument } from "../src/types/migration/Document" it("creates a document", () => { const migration = prismic.createMigration() @@ -18,10 +19,9 @@ it("creates a document", () => { migration.createDocument(document, documentTitle) - expect(migration._documents[0]).toStrictEqual({ - document, - params: { documentTitle }, - }) + expect(migration._documents[0]).toStrictEqual( + new MigrationDocument(document, { documentTitle }), + ) }) it("creates a document from an existing Prismic document", (ctx) => { @@ -32,10 +32,9 @@ it("creates a document from an existing Prismic document", (ctx) => { migration.createDocument(document, documentTitle) - expect(migration._documents[0]).toStrictEqual({ - document, - params: { documentTitle }, - }) + expect(migration._documents[0]).toStrictEqual( + new MigrationDocument(document, { documentTitle }), + ) }) describe.each<{ @@ -46,7 +45,7 @@ describe.each<{ | prismic.FilledImageFieldImage | prismic.FilledLinkToMediaField | prismic.RichTextField<"filled"> - expected: MigrationAsset + expected: MigrationAssetConfig } }>([ { @@ -58,8 +57,8 @@ describe.each<{ id: image.id, field: image, expected: { - alt: image.alt ?? undefined, - credits: image.copyright ?? undefined, + alt: image.alt || undefined, + credits: image.copyright || undefined, file: image.url.split("?")[0], filename: image.url.split("/").pop()!.split("?")[0], id: image.id, @@ -104,8 +103,8 @@ describe.each<{ id: image.id, field: richText, expected: { - alt: image.alt ?? undefined, - credits: image.copyright ?? undefined, + alt: image.alt || undefined, + credits: image.copyright || undefined, file: image.url.split("?")[0], filename: image.url.split("/").pop()!.split("?")[0], id: image.id, diff --git a/test/migration-getByUID.test.ts b/test/migration-getByUID.test.ts index 0ac4d3ad..496e7b22 100644 --- a/test/migration-getByUID.test.ts +++ b/test/migration-getByUID.test.ts @@ -13,10 +13,10 @@ it("returns a document with a matching UID", () => { } const documentName = "documentName" - migration.createDocument(document, documentName) + const migrationDocument = migration.createDocument(document, documentName) expect(migration.getByUID(document.type, document.uid)).toStrictEqual( - document, + migrationDocument, ) }) diff --git a/test/migration-getSingle.test.ts b/test/migration-getSingle.test.ts index d2ff7359..4dbcc129 100644 --- a/test/migration-getSingle.test.ts +++ b/test/migration-getSingle.test.ts @@ -12,9 +12,9 @@ it("returns a document of a given singleton type", () => { } const documentName = "documentName" - migration.createDocument(document, documentName) + const migrationDocument = migration.createDocument(document, documentName) - expect(migration.getSingle(document.type)).toStrictEqual(document) + expect(migration.getSingle(document.type)).toStrictEqual(migrationDocument) }) it("returns `undefined` if a document is not found", () => { diff --git a/test/types/migration-document.types.ts b/test/types/migration-document.types.ts index 0d48cd56..14237c1a 100644 --- a/test/types/migration-document.types.ts +++ b/test/types/migration-document.types.ts @@ -100,8 +100,6 @@ expectType({ type Fields = { image: prismic.ImageField migrationImage: prismic.ImageField - link: prismic.LinkField - migrationLink: prismic.LinkField linkToMedia: prismic.LinkToMediaField migrationLinkToMedia: prismic.LinkToMediaField contentRelationship: prismic.ContentRelationshipField @@ -139,13 +137,11 @@ expectType({ lang: "", data: { image: {} as prismic.ImageField, - migrationImage: {} as prismic.ImageMigrationField, - link: {} as prismic.LinkField, - migrationLink: {} as prismic.LinkMigrationField, + migrationImage: {} as prismic.MigrationImage, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMedia, contentRelationship: {} as prismic.ContentRelationshipField, - migrationContentRelationship: {} as prismic.ContentRelationshipField, + migrationContentRelationship: {} as prismic.MigrationContentRelationship, }, }) @@ -158,13 +154,12 @@ expectType({ group: [ { image: {} as prismic.ImageField, - migrationImage: {} as prismic.ImageMigrationField, - link: {} as prismic.LinkField, - migrationLink: {} as prismic.LinkMigrationField, + migrationImage: {} as prismic.MigrationImage, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMedia, contentRelationship: {} as prismic.ContentRelationshipField, - migrationContentRelationship: {} as prismic.ContentRelationshipField, + migrationContentRelationship: + {} as prismic.MigrationContentRelationship, }, ], }, @@ -185,38 +180,33 @@ expectType({ version: "", primary: { image: {} as prismic.ImageField, - migrationImage: {} as prismic.ImageMigrationField, - link: {} as prismic.LinkField, - migrationLink: {} as prismic.LinkMigrationField, + migrationImage: {} as prismic.MigrationImage, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMedia, contentRelationship: {} as prismic.ContentRelationshipField, - migrationContentRelationship: {} as prismic.ContentRelationshipField, + migrationContentRelationship: + {} as prismic.MigrationContentRelationship, group: [ { image: {} as prismic.ImageField, - migrationImage: {} as prismic.ImageMigrationField, - link: {} as prismic.LinkField, - migrationLink: {} as prismic.LinkMigrationField, + migrationImage: {} as prismic.MigrationImage, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMedia, contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: - {} as prismic.ContentRelationshipField, + {} as prismic.MigrationContentRelationship, }, ], }, items: [ { image: {} as prismic.ImageField, - migrationImage: {} as prismic.ImageMigrationField, - link: {} as prismic.LinkField, - migrationLink: {} as prismic.LinkMigrationField, + migrationImage: {} as prismic.MigrationImage, linkToMedia: {} as prismic.LinkToMediaField, - migrationLinkToMedia: {} as prismic.LinkToMediaMigrationField, + migrationLinkToMedia: {} as prismic.MigrationLinkToMedia, contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: - {} as prismic.ContentRelationshipField, + {} as prismic.MigrationContentRelationship, }, ], }, diff --git a/test/types/migration.types.ts b/test/types/migration.types.ts index 91542ac2..f3c3bd2d 100644 --- a/test/types/migration.types.ts +++ b/test/types/migration.types.ts @@ -41,36 +41,85 @@ expectType>(false) // Default const defaultCreateAsset = defaultMigration.createAsset("url", "name") -expectType>(true) +expectType>(true) + +expectType< + TypeEqual< + ReturnType, + prismic.MigrationImage + > +>(true) expectType< - TypeEqual + TypeOf> >(true) + expectType< TypeEqual< - typeof defaultCreateAsset.linkToMedia, - prismic.LinkToMediaMigrationField + ReturnType, + prismic.MigrationLinkToMedia > >(true) expectType< - TypeOf + TypeOf< + prismic.MigrationLinkToMedia, + ReturnType + > +>(true) + +expectType< + TypeEqual< + ReturnType, + prismic.MigrationRTImageNode + > +>(true) +expectType< + TypeOf< + prismic.MigrationRTImageNode, + ReturnType + > >(true) // Documents const documentsCreateAsset = defaultMigration.createAsset("url", "name") -expectType>( - true, -) +expectType>(true) + expectType< - TypeEqual + TypeEqual< + ReturnType, + prismic.MigrationImage + > +>(true) +expectType< + TypeOf< + prismic.MigrationImage, + ReturnType + > +>(true) + +expectType< + TypeEqual< + ReturnType, + prismic.MigrationLinkToMedia + > >(true) +expectType< + TypeOf< + prismic.MigrationLinkToMedia, + ReturnType + > +>(true) + expectType< TypeEqual< - typeof documentsCreateAsset.linkToMedia, - prismic.LinkToMediaMigrationField + ReturnType, + prismic.MigrationRTImageNode > >(true) expectType< - TypeOf + TypeOf< + prismic.MigrationRTImageNode, + ReturnType + > >(true) /** @@ -87,9 +136,9 @@ const defaultCreateDocument = defaultMigration.createDocument( }, "", ) -expectType< - TypeEqual ->(true) +expectType>( + true, +) // Documents const documentsCreateDocument = documentsMigration.createDocument( @@ -104,6 +153,6 @@ const documentsCreateDocument = documentsMigration.createDocument( expectType< TypeEqual< typeof documentsCreateDocument, - prismic.PrismicMigrationDocument + prismic.MigrationDocument > >(true) diff --git a/test/writeClient-migrate-documents.test.ts b/test/writeClient-migrate-documents.test.ts index 28ce988d..fcb75ae7 100644 --- a/test/writeClient-migrate-documents.test.ts +++ b/test/writeClient-migrate-documents.test.ts @@ -8,7 +8,7 @@ import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" import * as prismic from "../src" -import type { DocumentMap } from "../src/lib/patchMigrationDocumentData" +import type { DocumentMap } from "../src/types/migration/Document" // Skip test on Node 16 and 18 (File and FormData support) const isNode16 = process.version.startsWith("v16") @@ -137,7 +137,7 @@ it.concurrent("creates new documents", async (ctx) => { const migration = prismic.createMigration() - migration.createDocument(document, "foo") + const migrationDocument = migration.createDocument(document, "foo") let documents: DocumentMap | undefined const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( @@ -162,28 +162,36 @@ it.concurrent("creates new documents", async (ctx) => { }, }, }) - expect(documents?.get(document)?.id).toBe(newDocument.id) + expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) }) it.concurrent( - "creates new non-master locale document with direct reference", + "creates new non-master locale document with direct reference (existing document)", async (ctx) => { const client = createTestWriteClient({ ctx }) - const masterLanguageDocument = ctx.mock.value.document() + const queryResponse = createPagedQueryResponses({ + ctx, + pages: 1, + pageSize: 1, + }) + + const masterLanguageDocument = queryResponse[0].results[0] const document = ctx.mock.value.document() const newDocument = { id: "foo", masterLanguageDocumentID: masterLanguageDocument.id, } - mockPrismicRestAPIV2({ ctx }) + mockPrismicRestAPIV2({ ctx, queryResponse }) mockPrismicAssetAPI({ ctx, client }) mockPrismicMigrationAPI({ ctx, client, newDocuments: [newDocument] }) const migration = prismic.createMigration() - migration.createDocument(document, "foo", { masterLanguageDocument }) + const migrationDocument = migration.createDocument(document, "foo", { + masterLanguageDocument, + }) let documents: DocumentMap | undefined const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( @@ -209,7 +217,62 @@ it.concurrent( }, }, }) - ctx.expect(documents?.get(document)?.id).toBe(newDocument.id) + ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) + ctx.expect.assertions(3) + }, +) + +it.concurrent( + "creates new non-master locale document with direct reference (new document)", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const masterLanguageDocument = + ctx.mock.value.document() as prismic.PrismicMigrationDocument + const document = + ctx.mock.value.document() as prismic.PrismicMigrationDocument + const newDocuments = [ + { + id: masterLanguageDocument.id!, + }, + { + id: document.id!, + masterLanguageDocumentID: masterLanguageDocument.id!, + }, + ] + + delete masterLanguageDocument.id + delete document.id + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, newDocuments: [...newDocuments] }) + + const migration = prismic.createMigration() + + const masterLanguageMigrationDocument = migration.createDocument( + masterLanguageDocument, + "foo", + ) + const migrationDocument = migration.createDocument(document, "bar", { + masterLanguageDocument: masterLanguageMigrationDocument, + }) + + let documents: DocumentMap | undefined + const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( + (event) => { + if (event.type === "documents:created") { + documents = event.data.documents + } + }, + ) + + await client.migrate(migration, { reporter }) + + ctx + .expect(documents?.get(masterLanguageMigrationDocument)?.id) + .toBe(newDocuments[0].id) + ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocuments[1].id) ctx.expect.assertions(3) }, ) @@ -238,7 +301,7 @@ it.concurrent( const migration = prismic.createMigration() - migration.createDocument(document, "foo", { + const migrationDocument = migration.createDocument(document, "foo", { masterLanguageDocument: () => masterLanguageDocument, }) @@ -266,7 +329,7 @@ it.concurrent( }, }, }) - ctx.expect(documents?.get(document)?.id).toBe(newDocument.id) + ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) ctx.expect.assertions(3) }, ) @@ -299,9 +362,12 @@ it.concurrent( const migration = prismic.createMigration() - migration.createDocument(masterLanguageDocument, "foo") - migration.createDocument(document, "bar", { - masterLanguageDocument: () => masterLanguageDocument, + const masterLanguageMigrationDocument = migration.createDocument( + masterLanguageDocument, + "foo", + ) + const migrationDocument = migration.createDocument(document, "bar", { + masterLanguageDocument: () => masterLanguageMigrationDocument, }) let documents: DocumentMap | undefined @@ -316,9 +382,9 @@ it.concurrent( await client.migrate(migration, { reporter }) ctx - .expect(documents?.get(masterLanguageDocument)?.id) + .expect(documents?.get(masterLanguageMigrationDocument)?.id) .toBe(newDocuments[0].id) - ctx.expect(documents?.get(document)?.id).toBe(newDocuments[1].id) + ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocuments[1].id) ctx.expect.assertions(3) }, ) @@ -351,7 +417,7 @@ it.concurrent( const migration = prismic.createMigration() - migration.createDocument(document, "foo") + const migrationDocument = migration.createDocument(document, "foo") let documents: DocumentMap | undefined const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( @@ -376,7 +442,7 @@ it.concurrent( }, }, }) - ctx.expect(documents?.get(document)?.id).toBe(newDocument.id) + ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) ctx.expect.assertions(3) }, ) @@ -404,8 +470,11 @@ it.concurrent("creates master locale documents first", async (ctx) => { const migration = prismic.createMigration() - migration.createDocument(document, "bar") - migration.createDocument(masterLanguageDocument, "foo") + const migrationDocument = migration.createDocument(document, "bar") + const masterLanguageMigrationDocument = migration.createDocument( + masterLanguageDocument, + "foo", + ) let documents: DocumentMap | undefined const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( @@ -419,7 +488,7 @@ it.concurrent("creates master locale documents first", async (ctx) => { await client.migrate(migration, { reporter }) ctx - .expect(documents?.get(masterLanguageDocument)?.id) + .expect(documents?.get(masterLanguageMigrationDocument)?.id) .toBe(newDocuments[0].id) - ctx.expect(documents?.get(document)?.id).toBe(newDocuments[1].id) + ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocuments[1].id) }) diff --git a/test/writeClient-migrate-patch-link.test.ts b/test/writeClient-migrate-patch-contentRelationship.test.ts similarity index 57% rename from test/writeClient-migrate-patch-link.test.ts rename to test/writeClient-migrate-patch-contentRelationship.test.ts index 51cb64ef..5cd030b8 100644 --- a/test/writeClient-migrate-patch-link.test.ts +++ b/test/writeClient-migrate-patch-contentRelationship.test.ts @@ -5,7 +5,7 @@ import { RichTextNodeType } from "../src" testMigrationFieldPatching("patches link fields", { existing: ({ existingDocuments }) => existingDocuments[0], migration: ({ migrationDocuments }) => { - delete migrationDocuments[0].id + delete migrationDocuments[0].document.id return migrationDocuments[0] }, @@ -13,21 +13,27 @@ testMigrationFieldPatching("patches link fields", { return () => existingDocuments[0] }, lazyMigration: ({ migration, migrationDocuments }) => { - delete migrationDocuments[0].id + delete migrationDocuments[0].document.id return () => - migration.getByUID(migrationDocuments[0].type, migrationDocuments[0].uid) + migration.getByUID( + migrationDocuments[0].document.type, + migrationDocuments[0].document.uid!, + ) }, migrationNoTags: ({ migration, migrationDocuments }) => { - migrationDocuments[0].tags = undefined + migrationDocuments[0].document.tags = undefined return () => - migration.getByUID(migrationDocuments[0].type, migrationDocuments[0].uid) + migration.getByUID( + migrationDocuments[0].document.type, + migrationDocuments[0].document.uid!, + ) }, otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { const contentRelationship = ctx.mock.value.link({ type: "Document" }) // `migrationDocuments` contains documents from "another repository" - contentRelationship.id = migrationDocuments[0].id! + contentRelationship.id = migrationDocuments[0].document.id! return contentRelationship }, @@ -47,25 +53,4 @@ testMigrationFieldPatching("patches link fields", { ], }, ], - richTextNewImageNodeLink: ({ ctx, migration, existingDocuments }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - { - ...migration.createAsset("foo", "foo.png"), - linkTo: existingDocuments[0], - }, - ], - richTextOtherReposiotryImageNodeLink: ({ - ctx, - mockedDomain, - existingDocuments, - }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - { - type: RichTextNodeType.image, - ...ctx.mock.value.image({ state: "filled" }), - id: "foo-id", - url: `${mockedDomain}/foo.png`, - linkTo: existingDocuments[0], - }, - ], }) diff --git a/test/writeClient-migrate-patch-image.test.ts b/test/writeClient-migrate-patch-image.test.ts index 39efa933..f0897927 100644 --- a/test/writeClient-migrate-patch-image.test.ts +++ b/test/writeClient-migrate-patch-image.test.ts @@ -1,7 +1,5 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" -import { RichTextNodeType } from "../src" - testMigrationFieldPatching("patches image fields", { new: ({ migration }) => migration.createAsset("foo", "foo.png"), existing: ({ migration, existingAssets }) => @@ -44,22 +42,20 @@ testMigrationFieldPatching("patches image fields", { }, } }, + otherRepositoryWithTypeThumbnail: ({ ctx, mockedDomain }) => { + const image = ctx.mock.value.image({ state: "filled" }) + const id = "foo-id" + + return { + ...image, + id, + url: `${mockedDomain}/foo.png?some=query`, + type: { + ...image, + id, + url: `${mockedDomain}/foo.png?some=other&query=params`, + }, + } + }, otherRepositoryEmpty: ({ ctx }) => ctx.mock.value.image({ state: "empty" }), - richTextNew: ({ ctx, migration }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - migration.createAsset("foo", "foo.png"), - ], - richTextExisting: ({ ctx, migration, existingAssets }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - migration.createAsset(existingAssets[0]), - ], - richTextOtherRepository: ({ ctx, mockedDomain }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - { - type: RichTextNodeType.image, - ...ctx.mock.value.image({ state: "filled" }), - id: "foo-id", - url: `${mockedDomain}/foo.png`, - }, - ], }) diff --git a/test/writeClient-migrate-patch-linkToMedia.test.ts b/test/writeClient-migrate-patch-linkToMedia.test.ts index abe49fa0..8a19025c 100644 --- a/test/writeClient-migrate-patch-linkToMedia.test.ts +++ b/test/writeClient-migrate-patch-linkToMedia.test.ts @@ -4,9 +4,10 @@ import { RichTextNodeType } from "../src" import { AssetType } from "../src/types/api/asset/asset" testMigrationFieldPatching("patches link to media fields", { - new: ({ migration }) => migration.createAsset("foo", "foo.png").linkToMedia, + new: ({ migration }) => + migration.createAsset("foo", "foo.png").asLinkToMedia(), existing: ({ migration, existingAssets }) => - migration.createAsset(existingAssets[0]).linkToMedia, + migration.createAsset(existingAssets[0]).asLinkToMedia(), existingNonImage: ({ migration, existingAssets }) => { existingAssets[0].filename = "foo.pdf" existingAssets[0].extension = "pdf" @@ -14,7 +15,7 @@ testMigrationFieldPatching("patches link to media fields", { existingAssets[0].width = undefined existingAssets[0].height = undefined - return migration.createAsset(existingAssets[0]).linkToMedia + return migration.createAsset(existingAssets[0]).asLinkToMedia() }, otherRepository: ({ ctx, mockedDomain }) => { return { @@ -23,12 +24,6 @@ testMigrationFieldPatching("patches link to media fields", { url: `${mockedDomain}/foo.png`, } }, - otherRepositoryNotFoundID: ({ ctx }) => { - return { - ...ctx.mock.value.linkToMedia({ state: "empty" }), - id: null, - } - }, richTextNew: ({ migration }) => [ { type: RichTextNodeType.paragraph, @@ -39,7 +34,7 @@ testMigrationFieldPatching("patches link to media fields", { type: RichTextNodeType.hyperlink, start: 0, end: 5, - data: migration.createAsset("foo", "foo.png").linkToMedia, + data: migration.createAsset("foo", "foo.png").asLinkToMedia(), }, ], }, @@ -54,7 +49,7 @@ testMigrationFieldPatching("patches link to media fields", { type: RichTextNodeType.hyperlink, start: 0, end: 5, - data: migration.createAsset(existingAssets[0]).linkToMedia, + data: migration.createAsset(existingAssets[0]).asLinkToMedia(), }, ], }, diff --git a/test/writeClient-migrate-patch-rtImageNode.test.ts b/test/writeClient-migrate-patch-rtImageNode.test.ts new file mode 100644 index 00000000..4d69bc1e --- /dev/null +++ b/test/writeClient-migrate-patch-rtImageNode.test.ts @@ -0,0 +1,43 @@ +import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" + +import { RichTextNodeType } from "../src" +import { MigrationContentRelationship } from "../src/types/migration/ContentRelationship" + +testMigrationFieldPatching("patches rich text image nodes", { + new: ({ ctx, migration }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + migration.createAsset("foo", "foo.png").asRTImageNode(), + ], + existing: ({ ctx, migration, existingAssets }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + migration.createAsset(existingAssets[0]).asRTImageNode(), + ], + otherRepository: ({ ctx, mockedDomain }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + ...ctx.mock.value.image({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + }, + ], + newLinkTo: ({ ctx, migration, existingDocuments }) => { + const rtImageNode = migration.createAsset("foo", "foo.png").asRTImageNode() + rtImageNode.linkTo = new MigrationContentRelationship(existingDocuments[0]) + + return [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + rtImageNode, + ] + }, + otherRepositoryLinkTo: ({ ctx, mockedDomain, existingDocuments }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + ...ctx.mock.value.image({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + linkTo: existingDocuments[0], + }, + ], +}) diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index db2a5137..3f6bb056 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -9,10 +9,8 @@ import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" import * as prismic from "../src" -import type { - AssetMap, - DocumentMap, -} from "../src/lib/patchMigrationDocumentData" +import type { AssetMap } from "../src/types/migration/Asset" +import type { DocumentMap } from "../src/types/migration/Document" // Skip test on Node 16 and 18 (File and FormData support) const isNode16 = process.version.startsWith("v16") From c7655f8bb2b76fda071424d85edf75e7cbb73144 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 11 Sep 2024 22:58:54 +0200 Subject: [PATCH 46/61] chore: rename file casing --- src/types/migration/{asset.ts => Asset.ts} | 0 src/types/migration/{document.ts => Document.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/types/migration/{asset.ts => Asset.ts} (100%) rename src/types/migration/{document.ts => Document.ts} (100%) diff --git a/src/types/migration/asset.ts b/src/types/migration/Asset.ts similarity index 100% rename from src/types/migration/asset.ts rename to src/types/migration/Asset.ts diff --git a/src/types/migration/document.ts b/src/types/migration/Document.ts similarity index 100% rename from src/types/migration/document.ts rename to src/types/migration/Document.ts From a6788f111a65db9e980b340b2d7475ce75da4499 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 11 Sep 2024 23:05:11 +0200 Subject: [PATCH 47/61] refactor: rename `_prepare` to `_resolve` --- src/WriteClient.ts | 4 ++-- src/types/migration/Asset.ts | 10 +++++----- src/types/migration/ContentRelationship.ts | 2 +- src/types/migration/Document.ts | 4 ++-- src/types/migration/Field.ts | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 2dc6ac16..badf149f 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -579,7 +579,7 @@ export class WriteClient< migrationDocument.params.masterLanguageDocument, ) - await link._prepare({ documents }) + await link._resolve({ documents }) masterLanguageDocumentID = link._field?.id } else if (migrationDocument.document.alternate_languages) { masterLanguageDocumentID = @@ -655,7 +655,7 @@ export class WriteClient< }) const { id, uid } = documents.get(migrationDocument)! - await migrationDocument._prepare({ assets, documents }) + await migrationDocument._resolve({ assets, documents }) await this.updateDocument( id, diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index 1bf3d934..98b7bd17 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -144,7 +144,7 @@ export class MigrationImage extends MigrationAsset { this.#thumbnails[name] = thumbnail } - async _prepare({ + async _resolve({ assets, documents, }: { @@ -157,7 +157,7 @@ export class MigrationImage extends MigrationAsset { this._field = assetToImage(asset, this._initialField) for (const name in this.#thumbnails) { - await this.#thumbnails[name]._prepare({ assets, documents }) + await this.#thumbnails[name]._resolve({ assets, documents }) const thumbnail = this.#thumbnails[name]._field if (thumbnail) { @@ -171,7 +171,7 @@ export class MigrationImage extends MigrationAsset { export class MigrationLinkToMedia extends MigrationAsset< LinkToMediaField<"filled"> > { - _prepare({ assets }: { assets: AssetMap }): void { + _resolve({ assets }: { assets: AssetMap }): void { const asset = assets.get(this.config.id) if (asset) { @@ -197,7 +197,7 @@ export class MigrationRTImageNode extends MigrationAsset { | FilledLinkToWebField | undefined - async _prepare({ + async _resolve({ assets, documents, }: { @@ -207,7 +207,7 @@ export class MigrationRTImageNode extends MigrationAsset { const asset = assets.get(this.config.id) if (this.linkTo instanceof MigrationField) { - await this.linkTo._prepare({ assets, documents }) + await this.linkTo._resolve({ assets, documents }) } if (asset) { diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts index f9c0eff3..6ada005a 100644 --- a/src/types/migration/ContentRelationship.ts +++ b/src/types/migration/ContentRelationship.ts @@ -29,7 +29,7 @@ export class MigrationContentRelationship extends MigrationField { + async _resolve({ documents }: { documents: DocumentMap }): Promise { const config = typeof this.unresolvedConfig === "function" ? await this.unresolvedConfig() diff --git a/src/types/migration/Document.ts b/src/types/migration/Document.ts index aa381c77..e18704cd 100644 --- a/src/types/migration/Document.ts +++ b/src/types/migration/Document.ts @@ -272,12 +272,12 @@ export class MigrationDocument< /** * @internal */ - async _prepare(args: { + async _resolve(args: { assets: AssetMap documents: DocumentMap }): Promise { for (const dependency of this.dependencies) { - await dependency._prepare(args) + await dependency._resolve(args) } } } diff --git a/src/types/migration/Field.ts b/src/types/migration/Field.ts index b70926a8..edf4618c 100644 --- a/src/types/migration/Field.ts +++ b/src/types/migration/Field.ts @@ -9,7 +9,7 @@ interface Preparable { /** * @internal */ - _prepare(args: { + _resolve(args: { assets: AssetMap documents: DocumentMap }): Promise | void @@ -41,7 +41,7 @@ export abstract class MigrationField< return this._field } - abstract _prepare(args: { + abstract _resolve(args: { assets: AssetMap documents: DocumentMap }): Promise | void From 895c86cc69dc57fb2611760825cfef97db5bd098 Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 12 Sep 2024 12:19:27 +0200 Subject: [PATCH 48/61] refactor: abstract thunks under `Migration#createContentRelationship` --- src/Migration.ts | 21 ++- src/WriteClient.ts | 64 ++++------ src/index.ts | 3 +- src/lib/isValue.ts | 41 ------ src/lib/prepareMigrationRecord.ts | 22 ++-- src/types/migration/Asset.ts | 120 ++++++++++-------- src/types/migration/ContentRelationship.ts | 29 +++-- src/types/migration/Document.ts | 47 +++---- src/types/migration/Field.ts | 19 ++- .../testMigrationFieldPatching.ts | 4 +- test/migration-createDocument.test.ts | 10 +- test/types/migration-document.types.ts | 14 +- test/writeClient-migrate-documents.test.ts | 55 ++++---- ...-migrate-patch-contentRelationship.test.ts | 41 +++--- ...teClient-migrate-patch-rtImageNode.test.ts | 24 ++-- test/writeClient-migrate.test.ts | 3 +- 16 files changed, 237 insertions(+), 280 deletions(-) diff --git a/src/Migration.ts b/src/Migration.ts index 10faaf3e..c0e09732 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -4,10 +4,12 @@ import { validateAssetMetadata } from "./lib/validateAssetMetadata" import type { Asset } from "./types/api/asset/asset" import type { MigrationAssetConfig } from "./types/migration/Asset" import { MigrationImage } from "./types/migration/Asset" +import type { UnresolvedMigrationContentRelationshipConfig } from "./types/migration/ContentRelationship" +import { MigrationContentRelationship } from "./types/migration/ContentRelationship" import { MigrationDocument } from "./types/migration/Document" import type { - PrismicMigrationDocument, - PrismicMigrationDocumentParams, + MigrationDocumentParams, + MigrationDocumentValue, } from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" import type { FilledImageFieldImage } from "./types/value/image" @@ -22,7 +24,7 @@ import type { FilledLinkToMediaField } from "./types/value/linkToMedia" * @typeParam TDocumentType - Type(s) to match `TDocuments` against. */ type ExtractDocumentType< - TDocuments extends PrismicDocument | PrismicMigrationDocument, + TDocuments extends PrismicDocument | MigrationDocumentValue, TDocumentType extends TDocuments["type"], > = Extract extends never @@ -43,7 +45,7 @@ const SINGLE_INDEX = "__SINGLE__" export class Migration< TDocuments extends PrismicDocument = PrismicDocument, TMigrationDocuments extends - PrismicMigrationDocument = PrismicMigrationDocument, + MigrationDocumentValue = MigrationDocumentValue, > { /** * @internal @@ -167,8 +169,8 @@ export class Migration< createDocument( document: ExtractDocumentType, - documentTitle: PrismicMigrationDocumentParams["documentTitle"], - params: Omit = {}, + documentTitle: string, + params: Omit = {}, ): MigrationDocument> { const { record: data, dependencies } = prepareMigrationRecord( document.data, @@ -196,6 +198,13 @@ export class Migration< return migrationDocument } + createContentRelationship( + config: UnresolvedMigrationContentRelationshipConfig, + text?: string, + ): MigrationContentRelationship { + return new MigrationContentRelationship(config, undefined, text) + } + getByUID( documentType: TType, uid: string, diff --git a/src/WriteClient.ts b/src/WriteClient.ts index badf149f..03b1a50c 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -23,12 +23,10 @@ import { type PutDocumentParams, } from "./types/api/migration/document" import type { AssetMap, MigrationAssetConfig } from "./types/migration/Asset" -import { MigrationContentRelationship } from "./types/migration/ContentRelationship" import type { DocumentMap, MigrationDocument, - PrismicMigrationDocument, - PrismicMigrationDocumentParams, + MigrationDocumentValue, } from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" @@ -117,15 +115,13 @@ type MigrateReporterEventMap = { current: number remaining: number total: number - document: PrismicMigrationDocument - documentParams: PrismicMigrationDocumentParams + document: MigrationDocument } "documents:creating": { current: number remaining: number total: number - document: PrismicMigrationDocument - documentParams: PrismicMigrationDocumentParams + document: MigrationDocument } "documents:created": { created: number @@ -135,8 +131,7 @@ type MigrateReporterEventMap = { current: number remaining: number total: number - document: PrismicMigrationDocument - documentParams: PrismicMigrationDocumentParams + document: MigrationDocument } "documents:updated": { updated: number @@ -527,7 +522,7 @@ export class WriteClient< // We create an array with non-master locale documents last because // we need their master locale document to be created first. for (const migrationDocument of migration._documents) { - if (migrationDocument.document.lang === masterLocale) { + if (migrationDocument.value.lang === masterLocale) { sortedMigrationDocuments.unshift(migrationDocument) } else { sortedMigrationDocuments.push(migrationDocument) @@ -538,8 +533,8 @@ export class WriteClient< let created = 0 for (const migrationDocument of sortedMigrationDocuments) { if ( - migrationDocument.document.id && - documents.has(migrationDocument.document.id) + migrationDocument.value.id && + documents.has(migrationDocument.value.id) ) { reporter?.({ type: "documents:skipping", @@ -548,15 +543,14 @@ export class WriteClient< current: ++i, remaining: sortedMigrationDocuments.length - i, total: sortedMigrationDocuments.length, - document: migrationDocument.document, - documentParams: migrationDocument.params, + document: migrationDocument, }, }) // Index the migration document documents.set( migrationDocument, - documents.get(migrationDocument.document.id)!, + documents.get(migrationDocument.value.id)!, ) } else { created++ @@ -566,24 +560,21 @@ export class WriteClient< current: ++i, remaining: sortedMigrationDocuments.length - i, total: sortedMigrationDocuments.length, - document: migrationDocument.document, - documentParams: migrationDocument.params, + document: migrationDocument, }, }) // Resolve master language document ID for non-master locale documents let masterLanguageDocumentID: string | undefined - if (migrationDocument.document.lang !== masterLocale) { + if (migrationDocument.value.lang !== masterLocale) { if (migrationDocument.params.masterLanguageDocument) { - const link = new MigrationContentRelationship( - migrationDocument.params.masterLanguageDocument, - ) + const link = migrationDocument.params.masterLanguageDocument - await link._resolve({ documents }) + await link._resolve({ documents, assets: new Map() }) masterLanguageDocumentID = link._field?.id - } else if (migrationDocument.document.alternate_languages) { + } else if (migrationDocument.value.alternate_languages) { masterLanguageDocumentID = - migrationDocument.document.alternate_languages.find( + migrationDocument.value.alternate_languages.find( ({ lang }) => lang === masterLocale, )?.id } @@ -591,7 +582,7 @@ export class WriteClient< const { id } = await this.createDocument( // We'll upload docuements data later on. - { ...migrationDocument.document, data: {} }, + { ...migrationDocument.value, data: {} }, migrationDocument.params.documentTitle, { masterLanguageDocumentID, @@ -600,13 +591,13 @@ export class WriteClient< ) // Index old ID for Prismic to Prismic migration - if (migrationDocument.document.id) { - documents.set(migrationDocument.document.id, { - ...migrationDocument.document, + if (migrationDocument.value.id) { + documents.set(migrationDocument.value.id, { + ...migrationDocument.value, id, }) } - documents.set(migrationDocument, { ...migrationDocument.document, id }) + documents.set(migrationDocument, { ...migrationDocument.value, id }) } } @@ -649,8 +640,7 @@ export class WriteClient< current: ++i, remaining: migration._documents.length - i, total: migration._documents.length, - document: migrationDocument.document, - documentParams: migrationDocument.params, + document: migrationDocument, }, }) @@ -664,8 +654,8 @@ export class WriteClient< { documentTitle: migrationDocument.params.documentTitle, uid, - tags: migrationDocument.document.tags, - data: migrationDocument.document.data, + tags: migrationDocument.value.tags, + data: migrationDocument.value.data, }, fetchParams, ) @@ -1046,8 +1036,8 @@ export class WriteClient< * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ private async createDocument( - document: PrismicMigrationDocument>, - documentTitle: PrismicMigrationDocumentParams["documentTitle"], + document: MigrationDocumentValue>, + documentTitle: string, { masterLanguageDocumentID, ...params @@ -1089,10 +1079,10 @@ export class WriteClient< private async updateDocument( id: string, document: Pick< - PrismicMigrationDocument>, + MigrationDocumentValue>, "uid" | "tags" | "data" > & { - documentTitle?: PrismicMigrationDocumentParams["documentTitle"] + documentTitle?: string }, params?: FetchParams, ): Promise { diff --git a/src/index.ts b/src/index.ts index 8c1dfaf3..68187114 100644 --- a/src/index.ts +++ b/src/index.ts @@ -328,7 +328,7 @@ export type { // Migrations - Types representing Prismic Migration API content values. export type { MigrationDocument, - PrismicMigrationDocument, + MigrationDocumentValue, RichTextFieldWithMigrationField, } from "./types/migration/Document" @@ -337,7 +337,6 @@ export type { MigrationLinkToMedia, MigrationRTImageNode, } from "./types/migration/Asset" - export type { MigrationContentRelationship } from "./types/migration/ContentRelationship" // API - Types representing Prismic Rest API V2 responses. diff --git a/src/lib/isValue.ts b/src/lib/isValue.ts index 71f563f0..805ac593 100644 --- a/src/lib/isValue.ts +++ b/src/lib/isValue.ts @@ -172,44 +172,3 @@ export const contentRelationship = ( return false } - -/** - * Checks if a value is a Prismic document. - * - * @param value - Value to check. - * - * @returns `true` if `value` is a content relationship, `false` otherwise. - * - * @internal - * This is not an official helper function and it's only designed to work with internal processes. - */ -export const document = ( - value: - | PrismicDocument - | FieldWithMigrationField - | RichTextBlockNodeWithMigrationField - | unknown, -): value is PrismicDocument => { - if (value && typeof value === "object" && !("version" in value)) { - if ( - "id" in value && - "uid" in value && - "url" in value && - "type" in value && - typeof value.type === "string" && - "href" in value && - "tags" in value && - "first_publication_date" in value && - "last_publication_date" in value && - "slugs" in value && - "linked_documents" in value && - "lang" in value && - "alternate_languages" in value && - "data" in value - ) { - return true - } - } - - return false -} diff --git a/src/lib/prepareMigrationRecord.ts b/src/lib/prepareMigrationRecord.ts index 26ea459e..618d4c7f 100644 --- a/src/lib/prepareMigrationRecord.ts +++ b/src/lib/prepareMigrationRecord.ts @@ -2,9 +2,7 @@ import type { MigrationImage, MigrationLinkToMedia, } from "../types/migration/Asset" -import type { UnresolvedMigrationContentRelationshipConfig } from "../types/migration/ContentRelationship" import { MigrationContentRelationship } from "../types/migration/ContentRelationship" -import { MigrationDocument } from "../types/migration/Document" import { MigrationField } from "../types/migration/Field" import type { FilledImageFieldImage } from "../types/value/image" import type { FilledLinkToWebField } from "../types/value/link" @@ -37,14 +35,17 @@ export const prepareMigrationRecord = >( const field: unknown = record[key] if (field instanceof MigrationField) { + // Existing migration fields dependencies.push(field) result[key] = field } else if (is.linkToMedia(field)) { + // Link to media const linkToMedia = onAsset(field).asLinkToMedia() dependencies.push(linkToMedia) result[key] = linkToMedia } else if (is.rtImageNode(field)) { + // Rich text image nodes const rtImageNode = onAsset(field).asRTImageNode() if (field.linkTo) { @@ -61,6 +62,7 @@ export const prepareMigrationRecord = >( dependencies.push(rtImageNode) result[key] = rtImageNode } else if (is.image(field)) { + // Image fields const image = onAsset(field).asImage() const { @@ -75,25 +77,21 @@ export const prepareMigrationRecord = >( for (const name in thumbnails) { if (is.image(thumbnails[name])) { + // Node thumbnails dependencies are tracked internally image.addThumbnail(name, onAsset(thumbnails[name]).asImage()) } } dependencies.push(image) result[key] = image - } else if ( - is.contentRelationship(field) || - is.document(field) || - field instanceof MigrationDocument || - typeof field === "function" - ) { - const contentRelationship = new MigrationContentRelationship( - field as UnresolvedMigrationContentRelationshipConfig, - ) + } else if (is.contentRelationship(field)) { + // Content relationships + const contentRelationship = new MigrationContentRelationship(field) dependencies.push(contentRelationship) result[key] = contentRelationship } else if (Array.isArray(field)) { + // Traverse arrays const array = [] for (const item of field) { @@ -106,12 +104,14 @@ export const prepareMigrationRecord = >( result[key] = array } else if (field && typeof field === "object") { + // Traverse objects const { record, dependencies: fieldDependencies } = prepareMigrationRecord({ ...field }, onAsset) dependencies.push(...fieldDependencies) result[key] = record } else { + // Primitives result[key] = field } } diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index 98b7bd17..a784ffb5 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -5,9 +5,17 @@ import type { LinkToMediaField } from "../value/linkToMedia" import { type RTImageNode, RichTextNodeType } from "../value/richText" import type { MigrationContentRelationship } from "./ContentRelationship" -import type { DocumentMap } from "./Document" +import type { ResolveArgs } from "./Field" import { MigrationField } from "./Field" +/** + * Any type of image field handled by {@link MigrationAsset} + */ +type ImageLike = + | FilledImageFieldImage + | LinkToMediaField<"filled"> + | RTImageNode + /** * Converts an asset to an image field. * @@ -19,10 +27,7 @@ import { MigrationField } from "./Field" */ const assetToImage = ( asset: Asset, - maybeInitialField?: - | FilledImageFieldImage - | LinkToMediaField<"filled"> - | RTImageNode, + maybeInitialField?: ImageLike, ): FilledImageFieldImage => { const parameters = (maybeInitialField?.url || asset.url).split("?")[1] const url = `${asset.url.split("?")[0]}${parameters ? `?${parameters}` : ""}` @@ -99,26 +104,11 @@ export type MigrationAssetConfig = { } export abstract class MigrationAsset< - TField extends - | FilledImageFieldImage - | LinkToMediaField<"filled"> - | RTImageNode = - | FilledImageFieldImage - | LinkToMediaField<"filled"> - | RTImageNode, -> extends MigrationField< - TField, - FilledImageFieldImage | LinkToMediaField<"filled"> | RTImageNode -> { + TField extends ImageLike = ImageLike, +> extends MigrationField { config: MigrationAssetConfig - constructor( - config: MigrationAssetConfig, - initialField?: - | FilledImageFieldImage - | LinkToMediaField<"filled"> - | RTImageNode, - ) { + constructor(config: MigrationAssetConfig, initialField?: ImageLike) { super(initialField) this.config = config @@ -128,29 +118,38 @@ export abstract class MigrationAsset< return new MigrationImage(this.config, this._initialField) } - asLinkToMedia(): MigrationLinkToMedia { - return new MigrationLinkToMedia(this.config, this._initialField) + asLinkToMedia(text?: string): MigrationLinkToMedia { + return new MigrationLinkToMedia(this.config, this._initialField, text) } - asRTImageNode(): MigrationRTImageNode { - return new MigrationRTImageNode(this.config, this._initialField) + asRTImageNode( + linkTo?: + | MigrationLinkToMedia + | MigrationContentRelationship + | FilledLinkToWebField, + ): MigrationRTImageNode { + return new MigrationRTImageNode(this.config, this._initialField, linkTo) } } +/** + * A map of asset IDs to asset used to resolve assets when patching migration + * Prismic documents. + * + * @internal + */ +export type AssetMap = Map + export class MigrationImage extends MigrationAsset { #thumbnails: Record = {} - addThumbnail(name: string, thumbnail: MigrationImage): void { + addThumbnail(name: string, thumbnail: MigrationImage): this { this.#thumbnails[name] = thumbnail + + return this } - async _resolve({ - assets, - documents, - }: { - assets: AssetMap - documents: DocumentMap - }): Promise { + async _resolve({ assets, documents }: ResolveArgs): Promise { const asset = assets.get(this.config.id) if (asset) { @@ -171,7 +170,19 @@ export class MigrationImage extends MigrationAsset { export class MigrationLinkToMedia extends MigrationAsset< LinkToMediaField<"filled"> > { - _resolve({ assets }: { assets: AssetMap }): void { + text?: string + + constructor( + config: MigrationAssetConfig, + initialField?: ImageLike, + text?: string, + ) { + super(config, initialField) + + this.text = text + } + + _resolve({ assets }: ResolveArgs): void { const asset = assets.get(this.config.id) if (asset) { @@ -185,25 +196,34 @@ export class MigrationLinkToMedia extends MigrationAsset< height: typeof asset.height === "number" ? `${asset.height}` : undefined, width: typeof asset.width === "number" ? `${asset.width}` : undefined, + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + text: this.text, } } } } export class MigrationRTImageNode extends MigrationAsset { - linkTo: + linkTo?: | MigrationLinkToMedia | MigrationContentRelationship | FilledLinkToWebField - | undefined - - async _resolve({ - assets, - documents, - }: { - assets: AssetMap - documents: DocumentMap - }): Promise { + + constructor( + config: MigrationAssetConfig, + initialField?: ImageLike, + linkTo?: + | MigrationLinkToMedia + | MigrationContentRelationship + | FilledLinkToWebField, + ) { + super(config, initialField) + + this.linkTo = linkTo + } + + async _resolve({ assets, documents }: ResolveArgs): Promise { const asset = assets.get(this.config.id) if (this.linkTo instanceof MigrationField) { @@ -222,11 +242,3 @@ export class MigrationRTImageNode extends MigrationAsset { } } } - -/** - * A map of asset IDs to asset used to resolve assets when patching migration - * Prismic documents. - * - * @internal - */ -export type AssetMap = Map diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts index 6ada005a..cc5fb6ad 100644 --- a/src/types/migration/ContentRelationship.ts +++ b/src/types/migration/ContentRelationship.ts @@ -2,34 +2,42 @@ import type { FilledContentRelationshipField } from "../value/contentRelationshi import type { PrismicDocument } from "../value/document" import { LinkType } from "../value/link" -import type { DocumentMap, MigrationDocument } from "./Document" +import type { MigrationDocument } from "./Document" +import type { ResolveArgs } from "./Field" import { MigrationField } from "./Field" -type MigrationContentRelationshipConfig = - | PrismicDocument - | MigrationDocument +type MigrationContentRelationshipConfig< + TDocuments extends PrismicDocument = PrismicDocument, +> = + | TDocuments + | MigrationDocument | FilledContentRelationshipField | undefined -export type UnresolvedMigrationContentRelationshipConfig = - | MigrationContentRelationshipConfig +export type UnresolvedMigrationContentRelationshipConfig< + TDocuments extends PrismicDocument = PrismicDocument, +> = + | MigrationContentRelationshipConfig | (() => - | Promise - | MigrationContentRelationshipConfig) + | Promise> + | MigrationContentRelationshipConfig) export class MigrationContentRelationship extends MigrationField { unresolvedConfig: UnresolvedMigrationContentRelationshipConfig + text?: string constructor( unresolvedConfig: UnresolvedMigrationContentRelationshipConfig, initialField?: FilledContentRelationshipField, + text?: string, ) { super(initialField) this.unresolvedConfig = unresolvedConfig + this.text = text } - async _resolve({ documents }: { documents: DocumentMap }): Promise { + async _resolve({ documents }: ResolveArgs): Promise { const config = typeof this.unresolvedConfig === "function" ? await this.unresolvedConfig() @@ -48,6 +56,9 @@ export class MigrationContentRelationship extends MigrationField & { - data: - | MigrationLinkToMedia - | MigrationContentRelationship - | UnresolvedMigrationContentRelationshipConfig + data: MigrationLinkToMedia | MigrationContentRelationship }) )[] } @@ -89,10 +82,7 @@ type RegularFieldWithMigrationField< : TField extends FilledLinkToMediaField ? MigrationLinkToMedia | undefined : TField extends FilledContentRelationshipField - ? - | MigrationContentRelationship - | UnresolvedMigrationContentRelationshipConfig - | undefined + ? MigrationContentRelationship | undefined : TField extends RichTextField ? RichTextFieldWithMigrationField : never) @@ -161,8 +151,8 @@ export type FieldsWithMigrationFields< } /** - * Makes the UID of `PrismicMigrationDocument` optional on custom types without - * UID. TypeScript fails to infer correct types if done with a type + * Makes the UID of {@link MigrationDocumentValue} optional on custom types + * without UID. TypeScript fails to infer correct types if done with a type * intersection. * * @internal @@ -177,7 +167,7 @@ type MakeUIDOptional = * * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} */ -export type PrismicMigrationDocument< +export type MigrationDocumentValue< TDocument extends PrismicDocument = PrismicDocument, > = TDocument extends PrismicDocument @@ -235,7 +225,7 @@ export type PrismicMigrationDocument< * * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} */ -export type PrismicMigrationDocumentParams = { +export type MigrationDocumentParams = { /** * Name of the document displayed in the editor. */ @@ -247,24 +237,22 @@ export type PrismicMigrationDocumentParams = { // We're forced to inline `ContentRelationshipMigrationField` here, otherwise // it creates a circular reference to itself which makes TypeScript unhappy. // (but I think it's weird and it doesn't make sense :thinking:) - masterLanguageDocument?: - | UnresolvedMigrationContentRelationshipConfig - | undefined + masterLanguageDocument?: MigrationContentRelationship } export class MigrationDocument< TDocument extends PrismicDocument = PrismicDocument, > { - document: PrismicMigrationDocument - params: PrismicMigrationDocumentParams + value: MigrationDocumentValue + params: MigrationDocumentParams dependencies: MigrationField[] constructor( - document: PrismicMigrationDocument, - params: PrismicMigrationDocumentParams, + value: MigrationDocumentValue, + params: MigrationDocumentParams, dependencies: MigrationField[] = [], ) { - this.document = document + this.value = value this.params = params this.dependencies = dependencies } @@ -272,10 +260,7 @@ export class MigrationDocument< /** * @internal */ - async _resolve(args: { - assets: AssetMap - documents: DocumentMap - }): Promise { + async _resolve(args: ResolveArgs): Promise { for (const dependency of this.dependencies) { await dependency._resolve(args) } @@ -295,5 +280,5 @@ export type DocumentMap = Map< string | MigrationDocument | MigrationDocument, | PrismicDocument - | (Omit, "id"> & { id: string }) + | (Omit, "id"> & { id: string }) > diff --git a/src/types/migration/Field.ts b/src/types/migration/Field.ts index edf4618c..df11e3fd 100644 --- a/src/types/migration/Field.ts +++ b/src/types/migration/Field.ts @@ -5,14 +5,16 @@ import type { RTBlockNode, RTInlineNode } from "../value/richText" import type { AssetMap } from "./Asset" import type { DocumentMap } from "./Document" +export type ResolveArgs = { + assets: AssetMap + documents: DocumentMap +} + interface Preparable { /** * @internal */ - _resolve(args: { - assets: AssetMap - documents: DocumentMap - }): Promise | void + _resolve(args: ResolveArgs): Promise | void } export abstract class MigrationField< @@ -26,12 +28,12 @@ export abstract class MigrationField< /** * @internal */ - _field: TField | undefined + _field?: TField /** * @internal */ - _initialField: TInitialField | undefined + _initialField?: TInitialField constructor(initialField?: TInitialField) { this._initialField = initialField @@ -41,8 +43,5 @@ export abstract class MigrationField< return this._field } - abstract _resolve(args: { - assets: AssetMap - documents: DocumentMap - }): Promise | void + abstract _resolve(args: ResolveArgs): Promise | void } diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index 4a4c1a32..e9650ea5 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -27,7 +27,7 @@ type GetDataArgs = { } type InternalTestMigrationFieldPatchingArgs = { - getData: (args: GetDataArgs) => prismic.PrismicMigrationDocument["data"] + getData: (args: GetDataArgs) => prismic.MigrationDocumentValue["data"] expectStrictEqual?: boolean } @@ -115,7 +115,7 @@ const internalTestMigrationFieldPatching = ( type TestMigrationFieldPatchingFactoryCases = Record< string, - (args: GetDataArgs) => prismic.PrismicMigrationDocument["data"][string] + (args: GetDataArgs) => prismic.MigrationDocumentValue["data"][string] > export const testMigrationFieldPatching = ( diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index 960d2c0a..0877330b 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -9,7 +9,7 @@ import { MigrationDocument } from "../src/types/migration/Document" it("creates a document", () => { const migration = prismic.createMigration() - const document: prismic.PrismicMigrationDocument = { + const document: prismic.MigrationDocumentValue = { type: "type", uid: "uid", lang: "lang", @@ -184,7 +184,7 @@ describe.each<{ const { id, field, expected } = getField(mock) - const document: prismic.PrismicMigrationDocument = { + const document: prismic.MigrationDocumentValue = { type: "type", uid: "uid", lang: "lang", @@ -204,7 +204,7 @@ describe.each<{ const { id, field, expected } = getField(mock) - const document: prismic.PrismicMigrationDocument = { + const document: prismic.MigrationDocumentValue = { type: "type", uid: "uid", lang: "lang", @@ -234,7 +234,7 @@ describe.each<{ }, ] - const document: prismic.PrismicMigrationDocument = { + const document: prismic.MigrationDocumentValue = { type: "type", uid: "uid", lang: "lang", @@ -264,7 +264,7 @@ describe.each<{ }, ] - const document: prismic.PrismicMigrationDocument = { + const document: prismic.MigrationDocumentValue = { type: "type", uid: "uid", lang: "lang", diff --git a/test/types/migration-document.types.ts b/test/types/migration-document.types.ts index 14237c1a..e43626a3 100644 --- a/test/types/migration-document.types.ts +++ b/test/types/migration-document.types.ts @@ -3,7 +3,7 @@ import { expectNever, expectType } from "ts-expect" import type * as prismic from "../../src" -;(value: prismic.PrismicMigrationDocument): true => { +;(value: prismic.MigrationDocumentValue): true => { switch (typeof value) { case "object": { if (value === null) { @@ -19,7 +19,7 @@ import type * as prismic from "../../src" } } -expectType({ +expectType({ uid: "", type: "", lang: "", @@ -29,7 +29,7 @@ expectType({ /** * Supports any field when generic. */ -expectType({ +expectType({ uid: "", type: "", lang: "", @@ -39,12 +39,12 @@ expectType({ }) /** - * `PrismicDocument` is assignable to `PrismicMigrationDocument` with added + * `PrismicDocument` is assignable to `MigrationDocumentValue` with added * `title`. */ expectType< TypeOf< - prismic.PrismicMigrationDocument, + prismic.MigrationDocumentValue, prismic.PrismicDocument & { title: string } > >(true) @@ -55,7 +55,7 @@ type BarDocument = prismic.PrismicDocument<{ bar: prismic.KeyTextField }, "bar"> type BazDocument = prismic.PrismicDocument, "baz"> type Documents = FooDocument | BarDocument | BazDocument -type MigrationDocuments = prismic.PrismicMigrationDocument +type MigrationDocuments = prismic.MigrationDocumentValue /** * Infers data type from document type. @@ -128,7 +128,7 @@ type SliceDocument = prismic.PrismicDocument< > type AdvancedDocuments = StaticDocument | GroupDocument | SliceDocument type MigrationAdvancedDocuments = - prismic.PrismicMigrationDocument + prismic.MigrationDocumentValue // Static expectType({ diff --git a/test/writeClient-migrate-documents.test.ts b/test/writeClient-migrate-documents.test.ts index fcb75ae7..a1197304 100644 --- a/test/writeClient-migrate-documents.test.ts +++ b/test/writeClient-migrate-documents.test.ts @@ -93,7 +93,7 @@ it.concurrent("skips creating existing documents", async (ctx) => { const migration = prismic.createMigration() - migration.createDocument(document, "foo") + const migrationDocument = migration.createDocument(document, "foo") const reporter = vi.fn() @@ -106,10 +106,7 @@ it.concurrent("skips creating existing documents", async (ctx) => { current: 1, remaining: 0, total: 1, - document, - documentParams: { - documentTitle: "foo", - }, + document: migrationDocument, }, }) expect(reporter).toHaveBeenCalledWith({ @@ -156,10 +153,7 @@ it.concurrent("creates new documents", async (ctx) => { current: 1, remaining: 0, total: 1, - document, - documentParams: { - documentTitle: "foo", - }, + document: migrationDocument, }, }) expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) @@ -190,7 +184,9 @@ it.concurrent( const migration = prismic.createMigration() const migrationDocument = migration.createDocument(document, "foo", { - masterLanguageDocument, + masterLanguageDocument: migration.createContentRelationship( + masterLanguageDocument, + ), }) let documents: DocumentMap | undefined @@ -210,11 +206,7 @@ it.concurrent( current: 1, remaining: 0, total: 1, - document, - documentParams: { - documentTitle: "foo", - masterLanguageDocument, - }, + document: migrationDocument, }, }) ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) @@ -228,9 +220,8 @@ it.concurrent( const client = createTestWriteClient({ ctx }) const masterLanguageDocument = - ctx.mock.value.document() as prismic.PrismicMigrationDocument - const document = - ctx.mock.value.document() as prismic.PrismicMigrationDocument + ctx.mock.value.document() as prismic.MigrationDocumentValue + const document = ctx.mock.value.document() as prismic.MigrationDocumentValue const newDocuments = [ { id: masterLanguageDocument.id!, @@ -255,7 +246,9 @@ it.concurrent( "foo", ) const migrationDocument = migration.createDocument(document, "bar", { - masterLanguageDocument: masterLanguageMigrationDocument, + masterLanguageDocument: migration.createContentRelationship( + masterLanguageMigrationDocument, + ), }) let documents: DocumentMap | undefined @@ -302,7 +295,9 @@ it.concurrent( const migration = prismic.createMigration() const migrationDocument = migration.createDocument(document, "foo", { - masterLanguageDocument: () => masterLanguageDocument, + masterLanguageDocument: migration.createContentRelationship( + () => masterLanguageDocument, + ), }) let documents: DocumentMap | undefined @@ -322,11 +317,7 @@ it.concurrent( current: 1, remaining: 0, total: 1, - document, - documentParams: { - documentTitle: "foo", - masterLanguageDocument: expect.any(Function), - }, + document: migrationDocument, }, }) ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) @@ -340,9 +331,8 @@ it.concurrent( const client = createTestWriteClient({ ctx }) const masterLanguageDocument = - ctx.mock.value.document() as prismic.PrismicMigrationDocument - const document = - ctx.mock.value.document() as prismic.PrismicMigrationDocument + ctx.mock.value.document() as prismic.MigrationDocumentValue + const document = ctx.mock.value.document() as prismic.MigrationDocumentValue const newDocuments = [ { id: masterLanguageDocument.id!, @@ -367,7 +357,9 @@ it.concurrent( "foo", ) const migrationDocument = migration.createDocument(document, "bar", { - masterLanguageDocument: () => masterLanguageMigrationDocument, + masterLanguageDocument: migration.createContentRelationship( + () => masterLanguageMigrationDocument, + ), }) let documents: DocumentMap | undefined @@ -436,10 +428,7 @@ it.concurrent( current: 1, remaining: 0, total: 1, - document, - documentParams: { - documentTitle: "foo", - }, + document: migrationDocument, }, }) ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) diff --git a/test/writeClient-migrate-patch-contentRelationship.test.ts b/test/writeClient-migrate-patch-contentRelationship.test.ts index 5cd030b8..98713761 100644 --- a/test/writeClient-migrate-patch-contentRelationship.test.ts +++ b/test/writeClient-migrate-patch-contentRelationship.test.ts @@ -3,42 +3,45 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPa import { RichTextNodeType } from "../src" testMigrationFieldPatching("patches link fields", { - existing: ({ existingDocuments }) => existingDocuments[0], - migration: ({ migrationDocuments }) => { - delete migrationDocuments[0].document.id + existing: ({ migration, existingDocuments }) => + migration.createContentRelationship(existingDocuments[0]), + migration: ({ migration, migrationDocuments }) => { + delete migrationDocuments[0].value.id - return migrationDocuments[0] + return migration.createContentRelationship(migrationDocuments[0]) }, - lazyExisting: ({ existingDocuments }) => { - return () => existingDocuments[0] + lazyExisting: ({ migration, existingDocuments }) => { + return migration.createContentRelationship(() => existingDocuments[0]) }, lazyMigration: ({ migration, migrationDocuments }) => { - delete migrationDocuments[0].document.id + delete migrationDocuments[0].value.id - return () => + return migration.createContentRelationship(() => migration.getByUID( - migrationDocuments[0].document.type, - migrationDocuments[0].document.uid!, - ) + migrationDocuments[0].value.type, + migrationDocuments[0].value.uid!, + ), + ) }, migrationNoTags: ({ migration, migrationDocuments }) => { - migrationDocuments[0].document.tags = undefined + migrationDocuments[0].value.tags = undefined - return () => + return migration.createContentRelationship(() => migration.getByUID( - migrationDocuments[0].document.type, - migrationDocuments[0].document.uid!, - ) + migrationDocuments[0].value.type, + migrationDocuments[0].value.uid!, + ), + ) }, otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { const contentRelationship = ctx.mock.value.link({ type: "Document" }) // `migrationDocuments` contains documents from "another repository" - contentRelationship.id = migrationDocuments[0].document.id! + contentRelationship.id = migrationDocuments[0].value.id! return contentRelationship }, brokenLink: ({ ctx }) => ctx.mock.value.link({ type: "Document" }), - richTextLinkNode: ({ existingDocuments }) => [ + richTextLinkNode: ({ migration, existingDocuments }) => [ { type: RichTextNodeType.paragraph, text: "lorem", @@ -48,7 +51,7 @@ testMigrationFieldPatching("patches link fields", { type: RichTextNodeType.hyperlink, start: 0, end: 5, - data: existingDocuments[0], + data: migration.createContentRelationship(existingDocuments[0]), }, ], }, diff --git a/test/writeClient-migrate-patch-rtImageNode.test.ts b/test/writeClient-migrate-patch-rtImageNode.test.ts index 4d69bc1e..47d9ace3 100644 --- a/test/writeClient-migrate-patch-rtImageNode.test.ts +++ b/test/writeClient-migrate-patch-rtImageNode.test.ts @@ -21,23 +21,25 @@ testMigrationFieldPatching("patches rich text image nodes", { url: `${mockedDomain}/foo.png`, }, ], - newLinkTo: ({ ctx, migration, existingDocuments }) => { - const rtImageNode = migration.createAsset("foo", "foo.png").asRTImageNode() - rtImageNode.linkTo = new MigrationContentRelationship(existingDocuments[0]) - - return [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - rtImageNode, - ] - }, - otherRepositoryLinkTo: ({ ctx, mockedDomain, existingDocuments }) => [ + newLinkTo: ({ ctx, migration, existingDocuments }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + migration + .createAsset("foo", "foo.png") + .asRTImageNode(new MigrationContentRelationship(existingDocuments[0])), + ], + otherRepositoryLinkTo: ({ + ctx, + migration, + mockedDomain, + existingDocuments, + }) => [ ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), { type: RichTextNodeType.image, ...ctx.mock.value.image({ state: "filled" }), id: "foo-id", url: `${mockedDomain}/foo.png`, - linkTo: existingDocuments[0], + linkTo: migration.createContentRelationship(existingDocuments[0]), }, ], }) diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index 3f6bb056..4d70c967 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -37,8 +37,7 @@ it.concurrent("performs migration", async (ctx) => { const migration = prismic.createMigration() - const documentFoo: prismic.PrismicMigrationDocument = - ctx.mock.value.document() + const documentFoo: prismic.MigrationDocumentValue = ctx.mock.value.document() documentFoo.data = { image: migration.createAsset("foo", "foo.png"), link: () => migration.getByUID("bar", "bar"), From 9a072778e19dbb175a178e6c593d3cddc28056b4 Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 12 Sep 2024 16:05:08 +0200 Subject: [PATCH 49/61] docs: document new types --- src/Migration.ts | 218 ++++++++++++++++++--- src/WriteClient.ts | 62 +++--- src/lib/pLimit.ts | 10 +- src/lib/prepareMigrationRecord.ts | 13 +- src/types/migration/Asset.ts | 121 ++++++++++-- src/types/migration/ContentRelationship.ts | 42 +++- src/types/migration/Document.ts | 63 +++++- src/types/migration/Field.ts | 49 ++++- test/migration-updateDocument.test.ts | 17 ++ test/writeClient-migrate-documents.test.ts | 43 +++- 10 files changed, 538 insertions(+), 100 deletions(-) create mode 100644 test/migration-updateDocument.test.ts diff --git a/src/Migration.ts b/src/Migration.ts index c0e09732..bbd78244 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -48,22 +48,77 @@ export class Migration< MigrationDocumentValue = MigrationDocumentValue, > { /** + * Assets registered in the migration. + * * @internal */ _assets: Map = new Map() /** + * Documents registered in the migration. + * * @internal */ _documents: MigrationDocument[] = [] + + /** + * A map indexing documents by their type and UID used for quick lookups by + * the {@link getByUID} and {@link getSingle} methods. + */ #indexedDocuments: Record< string, Record> > = {} + /** + * 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 configured to be serialized as an + * image field by default. + */ + createAsset(asset: Asset): MigrationImage + + /** + * 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 configured to be serialized as an + * image field by default. + */ createAsset( - asset: Asset | FilledImageFieldImage | FilledLinkToMediaField, + imageOrLinkToMediaField: FilledImageFieldImage | FilledLinkToMediaField, ): MigrationImage + + /** + * 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 configured to be serialized as an + * image field by default. + */ createAsset( file: MigrationAssetConfig["file"], filename: MigrationAssetConfig["filename"], @@ -74,8 +129,21 @@ export class Migration< tags?: string[] }, ): MigrationImage + + /** + * 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 configured to be serialized as an + * image field by default. + */ createAsset( - fileOrAsset: + fileOrAssetOrField: | MigrationAssetConfig["file"] | Asset | FilledImageFieldImage @@ -95,26 +163,31 @@ export class Migration< ): MigrationImage { let asset: MigrationAssetConfig let maybeInitialField: FilledImageFieldImage | undefined - if (typeof fileOrAsset === "object" && "url" in fileOrAsset) { - if ("dimensions" in fileOrAsset || "link_type" in fileOrAsset) { - const url = fileOrAsset.url.split("?")[0] + 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 fileOrAsset - ? fileOrAsset.name + "name" in fileOrAssetOrField + ? fileOrAssetOrField.name : url.split("/").pop()!.split("_").pop()! const credits = - "copyright" in fileOrAsset && fileOrAsset.copyright - ? fileOrAsset.copyright + "copyright" in fileOrAssetOrField && fileOrAssetOrField.copyright + ? fileOrAssetOrField.copyright : undefined const alt = - "alt" in fileOrAsset && fileOrAsset.alt ? fileOrAsset.alt : undefined + "alt" in fileOrAssetOrField && fileOrAssetOrField.alt + ? fileOrAssetOrField.alt + : undefined - if ("dimensions" in fileOrAsset) { - maybeInitialField = fileOrAsset + if ("dimensions" in fileOrAssetOrField) { + maybeInitialField = fileOrAssetOrField } asset = { - id: fileOrAsset.id, + id: fileOrAssetOrField.id, file: url, filename, notes: undefined, @@ -124,19 +197,19 @@ export class Migration< } } else { asset = { - id: fileOrAsset.id, - file: fileOrAsset.url, - filename: fileOrAsset.filename, - notes: fileOrAsset.notes, - credits: fileOrAsset.credits, - alt: fileOrAsset.alt, - tags: fileOrAsset.tags?.map(({ name }) => name), + 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 { asset = { - id: fileOrAsset, - file: fileOrAsset, + id: fileOrAssetOrField, + file: fileOrAssetOrField, filename: filename!, notes, credits, @@ -167,6 +240,23 @@ export class Migration< return new MigrationImage(this._assets.get(asset.id)!, maybeInitialField) } + /** + * 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 Prismic documents to create. + * + * @param document - The document to create. + * @param documentTitle - 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, documentTitle: string, @@ -198,13 +288,76 @@ export class Migration< return migrationDocument } + /** + * 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 documentTitle - The title of the document to update which will be + * displayed in the editor. + * + * @returns A migration document instance. + */ + updateDocument( + document: Omit, "id"> & { + id: string + }, + documentTitle: string, + ): MigrationDocument> { + const migrationDocument = this.createDocument( + document as ExtractDocumentType, + documentTitle, + ) + + migrationDocument._mode = "update" + + return migrationDocument + } + + /** + * Creates a content relationship fields that can be used in documents to link + * to other documents. + * + * @param config - A Prismic document, a migration document instance, an + * existing content relationship field's content, or a function that returns + * one of the previous. + * @param text - Link text associated with the content relationship field. + * + * @returns A migration content relationship field instance. + */ createContentRelationship( config: UnresolvedMigrationContentRelationshipConfig, text?: string, ): MigrationContentRelationship { - return new MigrationContentRelationship(config, undefined, text) + return new MigrationContentRelationship(config, text, undefined) } + /** + * 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 documentType - 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( documentType: TType, uid: string, @@ -214,6 +367,25 @@ export class Migration< | undefined } + /** + * 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 documentType - The API ID of the singleton custom type. + * + * @returns The migration document instance for the custom type, if a matching + * document is found. + */ getSingle( documentType: TType, ): MigrationDocument> | undefined { diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 03b1a50c..451b25f5 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -531,10 +531,10 @@ export class WriteClient< let i = 0 let created = 0 - for (const migrationDocument of sortedMigrationDocuments) { + for (const document of sortedMigrationDocuments) { if ( - migrationDocument.value.id && - documents.has(migrationDocument.value.id) + document.value.id && + (document._mode === "update" || documents.has(document.value.id)) ) { reporter?.({ type: "documents:skipping", @@ -543,15 +543,12 @@ export class WriteClient< current: ++i, remaining: sortedMigrationDocuments.length - i, total: sortedMigrationDocuments.length, - document: migrationDocument, + document: document, }, }) // Index the migration document - documents.set( - migrationDocument, - documents.get(migrationDocument.value.id)!, - ) + documents.set(document, documents.get(document.value.id)!) } else { created++ reporter?.({ @@ -560,30 +557,29 @@ export class WriteClient< current: ++i, remaining: sortedMigrationDocuments.length - i, total: sortedMigrationDocuments.length, - document: migrationDocument, + document: document, }, }) // Resolve master language document ID for non-master locale documents let masterLanguageDocumentID: string | undefined - if (migrationDocument.value.lang !== masterLocale) { - if (migrationDocument.params.masterLanguageDocument) { - const link = migrationDocument.params.masterLanguageDocument + if (document.value.lang !== masterLocale) { + if (document.params.masterLanguageDocument) { + const link = document.params.masterLanguageDocument await link._resolve({ documents, assets: new Map() }) masterLanguageDocumentID = link._field?.id - } else if (migrationDocument.value.alternate_languages) { - masterLanguageDocumentID = - migrationDocument.value.alternate_languages.find( - ({ lang }) => lang === masterLocale, - )?.id + } else if (document.value.alternate_languages) { + masterLanguageDocumentID = document.value.alternate_languages.find( + ({ lang }) => lang === masterLocale, + )?.id } } const { id } = await this.createDocument( // We'll upload docuements data later on. - { ...migrationDocument.value, data: {} }, - migrationDocument.params.documentTitle, + { ...document.value, data: {} }, + document.params.documentTitle, { masterLanguageDocumentID, ...fetchParams, @@ -591,13 +587,13 @@ export class WriteClient< ) // Index old ID for Prismic to Prismic migration - if (migrationDocument.value.id) { - documents.set(migrationDocument.value.id, { - ...migrationDocument.value, + if (document.value.id) { + documents.set(document.value.id, { + ...document.value, id, }) } - documents.set(migrationDocument, { ...migrationDocument.value, id }) + documents.set(document, { ...document.value, id }) } } @@ -633,29 +629,29 @@ export class WriteClient< }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise { let i = 0 - for (const migrationDocument of migration._documents) { + for (const document of migration._documents) { reporter?.({ type: "documents:updating", data: { current: ++i, remaining: migration._documents.length - i, total: migration._documents.length, - document: migrationDocument, + document: document, }, }) - const { id, uid } = documents.get(migrationDocument)! - await migrationDocument._resolve({ assets, documents }) + const { id, uid } = documents.get(document)! + await document._resolve({ assets, documents }) await this.updateDocument( id, // We need to forward again document name and tags to update them // in case the document already existed during the previous step. { - documentTitle: migrationDocument.params.documentTitle, + documentTitle: document.params.documentTitle, uid, - tags: migrationDocument.value.tags, - data: migrationDocument.value.data, + tags: document.value.tags, + data: document.value.data, }, fetchParams, ) @@ -1025,8 +1021,8 @@ export class WriteClient< * * @typeParam TType - Type of Prismic documents to create. * - * @param document - The document data to create. - * @param documentTitle - The name of the document to create which will be + * @param document - The document to create. + * @param documentTitle - The title of the document to create which will be * displayed in the editor. * @param params - Document master language document ID and additional fetch * parameters. @@ -1071,7 +1067,7 @@ export class WriteClient< * @typeParam TType - Type of Prismic documents to update. * * @param id - The ID of the document to update. - * @param document - The document data to update. + * @param document - The document content to update. * @param params - Additional fetch parameters. * * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} diff --git a/src/lib/pLimit.ts b/src/lib/pLimit.ts index 40aba216..13eab709 100644 --- a/src/lib/pLimit.ts +++ b/src/lib/pLimit.ts @@ -37,18 +37,18 @@ export const pLimit = ({ interval, }: { interval?: number } = {}): LimitFunction => { const queue: AnyFunction[] = [] - let activeCount = 0 + let busy = false let lastCompletion = 0 const resumeNext = () => { - if (activeCount === 0 && queue.length > 0) { + if (!busy && queue.length > 0) { queue.shift()?.() - activeCount++ + busy = true } } const next = () => { - activeCount-- + busy = false resumeNext() } @@ -93,7 +93,7 @@ export const pLimit = ({ // needs to happen asynchronously as well to get an up-to-date value for `activeCount`. await Promise.resolve() - if (activeCount === 0) { + if (!busy) { resumeNext() } })() diff --git a/src/lib/prepareMigrationRecord.ts b/src/lib/prepareMigrationRecord.ts index 618d4c7f..64055503 100644 --- a/src/lib/prepareMigrationRecord.ts +++ b/src/lib/prepareMigrationRecord.ts @@ -11,7 +11,7 @@ import type { FilledLinkToMediaField } from "../types/value/linkToMedia" import * as is from "./isValue" /** - * Replaces existings assets and links in a record of Prismic fields get all + * Replaces existings assets and links in a record of Prismic fields and get all * dependencies to them. * * @typeParam TRecord - Record of values to work with. @@ -40,7 +40,9 @@ export const prepareMigrationRecord = >( result[key] = field } else if (is.linkToMedia(field)) { // Link to media - const linkToMedia = onAsset(field).asLinkToMedia() + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + const linkToMedia = onAsset(field).asLinkToMedia(field.text) dependencies.push(linkToMedia) result[key] = linkToMedia @@ -86,7 +88,12 @@ export const prepareMigrationRecord = >( result[key] = image } else if (is.contentRelationship(field)) { // Content relationships - const contentRelationship = new MigrationContentRelationship(field) + const contentRelationship = new MigrationContentRelationship( + field, + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + field.text, + ) dependencies.push(contentRelationship) result[key] = contentRelationship diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index a784ffb5..3ba70c93 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -103,46 +103,91 @@ export type MigrationAssetConfig = { tags?: string[] } +/** + * A migration asset used with the Prismic Migration API. + * + * @typeParam TImageLike - Type of the image-like value. + */ export abstract class MigrationAsset< - TField extends ImageLike = ImageLike, -> extends MigrationField { - config: MigrationAssetConfig + TImageLike extends ImageLike = ImageLike, +> extends MigrationField { + /** + * Configuration of the asset. + * + * @internal + */ + _config: MigrationAssetConfig + /** + * Creates a migration asset used with the Prismic Migration API. + * + * @param config - Configuration of the asset. + * @param initialField - The initial field value if any. + * + * @returns A migration asset instance. + */ constructor(config: MigrationAssetConfig, initialField?: ImageLike) { super(initialField) - this.config = config + this._config = config } + /** + * Marks the migration asset instance to be serialized as an image field. + * + * @returns A migration image instance. + */ asImage(): MigrationImage { - return new MigrationImage(this.config, this._initialField) + return new MigrationImage(this._config, this._initialField) } + /** + * Marks the migration asset instance to be serialized as a link to media + * field. + * + * @param text - Link text for the link to media field if any. + * + * @returns A migration link to media instance. + */ asLinkToMedia(text?: string): MigrationLinkToMedia { - return new MigrationLinkToMedia(this.config, this._initialField, text) + return new MigrationLinkToMedia(this._config, text, this._initialField) } + /** + * Marks the migration asset instance to be serialized as a rich text image + * node. + * + * @param linkTo - Image node's link if any. + * + * @returns A migration rich text image node instance. + */ asRTImageNode( linkTo?: | MigrationLinkToMedia | MigrationContentRelationship | FilledLinkToWebField, ): MigrationRTImageNode { - return new MigrationRTImageNode(this.config, this._initialField, linkTo) + return new MigrationRTImageNode(this._config, linkTo, this._initialField) } } /** - * A map of asset IDs to asset used to resolve assets when patching migration - * Prismic documents. - * - * @internal + * A migration image used with the Prismic Migration API. */ -export type AssetMap = Map - export class MigrationImage extends MigrationAsset { + /** + * Thumbnails of the image. + */ #thumbnails: Record = {} + /** + * Adds a thumbnail to the migration image instance. + * + * @param name - Name of the thumbnail. + * @param thumbnail - Thumbnail to add as a migration image instance. + * + * @returns The current migration image instance, useful for chaining. + */ addThumbnail(name: string, thumbnail: MigrationImage): this { this.#thumbnails[name] = thumbnail @@ -150,7 +195,7 @@ export class MigrationImage extends MigrationAsset { } async _resolve({ assets, documents }: ResolveArgs): Promise { - const asset = assets.get(this.config.id) + const asset = assets.get(this._config.id) if (asset) { this._field = assetToImage(asset, this._initialField) @@ -167,15 +212,31 @@ export class MigrationImage extends MigrationAsset { } } +/** + * A migration link to media used with the Prismic Migration API. + */ export class MigrationLinkToMedia extends MigrationAsset< LinkToMediaField<"filled"> > { + /** + * Link text for the link to media field if any. + */ text?: string + /** + * Creates a migration link to media instance used with the Prismic Migration + * API. + * + * @param config - Configuration of the asset. + * @param text - Link text for the link to media field if any. + * @param initialField - The initial field value if any. + * + * @returns A migration link to media instance. + */ constructor( config: MigrationAssetConfig, - initialField?: ImageLike, text?: string, + initialField?: ImageLike, ) { super(config, initialField) @@ -183,7 +244,7 @@ export class MigrationLinkToMedia extends MigrationAsset< } _resolve({ assets }: ResolveArgs): void { - const asset = assets.get(this.config.id) + const asset = assets.get(this._config.id) if (asset) { this._field = { @@ -204,19 +265,35 @@ export class MigrationLinkToMedia extends MigrationAsset< } } +/** + * A migration rich text image node used with the Prismic Migration API. + */ export class MigrationRTImageNode extends MigrationAsset { + /** + * Image node's link if any. + */ linkTo?: | MigrationLinkToMedia | MigrationContentRelationship | FilledLinkToWebField + /** + * Creates a migration rich text image node instance used with the Prismic + * Migration API. + * + * @param config - Configuration of the asset. + * @param linkTo - Image node's link if any. + * @param initialField - The initial field value if any. + * + * @returns A migration rich text image node instance. + */ constructor( config: MigrationAssetConfig, - initialField?: ImageLike, linkTo?: | MigrationLinkToMedia | MigrationContentRelationship | FilledLinkToWebField, + initialField?: ImageLike, ) { super(config, initialField) @@ -224,7 +301,7 @@ export class MigrationRTImageNode extends MigrationAsset { } async _resolve({ assets, documents }: ResolveArgs): Promise { - const asset = assets.get(this.config.id) + const asset = assets.get(this._config.id) if (this.linkTo instanceof MigrationField) { await this.linkTo._resolve({ assets, documents }) @@ -242,3 +319,11 @@ export class MigrationRTImageNode extends MigrationAsset { } } } + +/** + * A map of asset IDs to asset used to resolve assets when patching migration + * Prismic documents. + * + * @internal + */ +export type AssetMap = Map diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts index cc5fb6ad..cc56c1ae 100644 --- a/src/types/migration/ContentRelationship.ts +++ b/src/types/migration/ContentRelationship.ts @@ -6,6 +6,9 @@ import type { MigrationDocument } from "./Document" import type { ResolveArgs } from "./Field" import { MigrationField } from "./Field" +/** + * Configuration used to create a content relationship field in a migration. + */ type MigrationContentRelationshipConfig< TDocuments extends PrismicDocument = PrismicDocument, > = @@ -14,6 +17,10 @@ type MigrationContentRelationshipConfig< | FilledContentRelationshipField | undefined +/** + * Unresolved configuration used to create a content relationship field in a + * migration allowing for lazy resolution. + */ export type UnresolvedMigrationContentRelationshipConfig< TDocuments extends PrismicDocument = PrismicDocument, > = @@ -22,26 +29,49 @@ export type UnresolvedMigrationContentRelationshipConfig< | Promise> | MigrationContentRelationshipConfig) +/** + * A migration content relationship field used with the Prismic Migration API. + */ export class MigrationContentRelationship extends MigrationField { - unresolvedConfig: UnresolvedMigrationContentRelationshipConfig + /** + * Unresolved configuration used to resolve the content relationship field's + * value + */ + #unresolvedConfig: UnresolvedMigrationContentRelationshipConfig + + /** + * Link text for the content relationship if any. + */ text?: string + /** + * Creates a migration content relationship field used with the Prismic + * Migration API. + * + * @param unresolvedConfig - A Prismic document, a migration document + * instance, an existing content relationship field's content, or a function + * that returns one of the previous. + * @param text - Link text for the content relationship if any. + * @param initialField - The initial field value if any. + * + * @returns A migration content relationship field instance. + */ constructor( unresolvedConfig: UnresolvedMigrationContentRelationshipConfig, - initialField?: FilledContentRelationshipField, text?: string, + initialField?: FilledContentRelationshipField, ) { super(initialField) - this.unresolvedConfig = unresolvedConfig + this.#unresolvedConfig = unresolvedConfig this.text = text } async _resolve({ documents }: ResolveArgs): Promise { const config = - typeof this.unresolvedConfig === "function" - ? await this.unresolvedConfig() - : this.unresolvedConfig + typeof this.#unresolvedConfig === "function" + ? await this.#unresolvedConfig() + : this.#unresolvedConfig if (config) { const document = diff --git a/src/types/migration/Document.ts b/src/types/migration/Document.ts index 550a5e2f..c109bcbe 100644 --- a/src/types/migration/Document.ts +++ b/src/types/migration/Document.ts @@ -3,7 +3,7 @@ import type { AnyRegularField } from "../value/types" import type { FilledContentRelationshipField } from "../value/contentRelationship" import type { PrismicDocument } from "../value/document" import type { GroupField } from "../value/group" -import type { ImageField } from "../value/image" +import type { FilledImageFieldImage } from "../value/image" import type { FilledLinkToMediaField } from "../value/linkToMedia" import type { RTBlockNode, @@ -77,7 +77,7 @@ export type RichTextFieldWithMigrationField< type RegularFieldWithMigrationField< TField extends AnyRegularField = AnyRegularField, > = - | (TField extends ImageField + | (TField extends FilledImageFieldImage ? MigrationImage | undefined : TField extends FilledLinkToMediaField ? MigrationLinkToMedia | undefined @@ -163,9 +163,9 @@ type MakeUIDOptional = : Omit & Partial> /** - * A Prismic document compatible with the Migration API. + * A Prismic document value compatible with the Migration API. * - * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see More details on the Migration API: {@link https://prismic.io/docs/migration-api-technical-reference} */ export type MigrationDocumentValue< TDocument extends PrismicDocument = PrismicDocument, @@ -223,7 +223,7 @@ export type MigrationDocumentValue< /** * Parameters used when creating a Prismic document with the Migration API. * - * @see More details on the migraiton API: {@link https://prismic.io/docs/migration-api-technical-reference} + * @see More details on the Migration API: {@link https://prismic.io/docs/migration-api-technical-reference} */ export type MigrationDocumentParams = { /** @@ -240,13 +240,53 @@ export type MigrationDocumentParams = { masterLanguageDocument?: MigrationContentRelationship } +/** + * A Prismic migration document instance. + * + * @see More details on the Migration API: {@link https://prismic.io/docs/migration-api-technical-reference} + */ export class MigrationDocument< TDocument extends PrismicDocument = PrismicDocument, > { + /** + * The document value to be sent to the Migration API. + */ value: MigrationDocumentValue + + /** + * Parameters to create/update the document with on the Migration API. + */ params: MigrationDocumentParams - dependencies: MigrationField[] + /** + * Asset and content relationship fields that this document depends on. + */ + #dependencies: MigrationField[] + + /** + * The mode to use when creating or updating the document. + * + * - `auto`: Automatically determines if the document should be created or + * updated on the document's existence on the repository's Document API. + * - `update`: Forces the document to only be updated. This is useful when the + * document only exists within the migration release which cannot be + * queried. + * + * @internal + */ + _mode: "auto" | "update" = "auto" + + /** + * Creates a Prismic migration document instance. + * + * @param value - The document value to be sent to the Migration API. + * @param params - Parameters to create/update the document with on the + * Migration API. + * @param dependencies - Asset and content relationship fields that this + * document depends on. + * + * @returns A Prismic migration document instance. + */ constructor( value: MigrationDocumentValue, params: MigrationDocumentParams, @@ -254,21 +294,26 @@ export class MigrationDocument< ) { this.value = value this.params = params - this.dependencies = dependencies + this.#dependencies = dependencies } /** + * Resolves each dependencies of the document with the provided maps. + * + * @param args - A map of documents and a map of assets to resolve content + * with. + * * @internal */ async _resolve(args: ResolveArgs): Promise { - for (const dependency of this.dependencies) { + for (const dependency of this.#dependencies) { await dependency._resolve(args) } } } /** - * A map of document IDs, documents, and migraiton documents to content + * A map of document IDs, documents, and migration documents to content * relationship field used to resolve content relationships when patching * migration Prismic documents. * diff --git a/src/types/migration/Field.ts b/src/types/migration/Field.ts index df11e3fd..0f559a01 100644 --- a/src/types/migration/Field.ts +++ b/src/types/migration/Field.ts @@ -5,40 +5,85 @@ import type { RTBlockNode, RTInlineNode } from "../value/richText" import type { AssetMap } from "./Asset" import type { DocumentMap } from "./Document" +/** + * Arguments passed to the `_resolve` method of a migration field. + */ export type ResolveArgs = { + /** + * A map of asset IDs to asset used to resolve assets when patching migration + * Prismic documents. + */ assets: AssetMap + + /** + * A map of document IDs, documents, and migration documents to content + * relationship field used to resolve content relationships when patching + * migration Prismic documents. + */ documents: DocumentMap } -interface Preparable { +/** + * Interface for migration fields that can be resolved. + */ +interface Resolvable { /** + * Resolves the field's value with the provided maps. + * + * @param args - A map of documents and a map of assets to resolve content + * with. + * * @internal */ _resolve(args: ResolveArgs): Promise | void } +/** + * A migration field used with the Prismic Migration API. + * + * @typeParam TField - Type of the field value. + * @typeParam TInitialField - Type of the initial field value. + */ export abstract class MigrationField< TField extends AnyRegularField | RTBlockNode | RTInlineNode = | AnyRegularField | RTBlockNode | RTInlineNode, TInitialField = TField, -> implements Preparable +> implements Resolvable { /** + * The resolved field value. + * * @internal */ _field?: TField /** + * The initial field value this migration field was created with. + * * @internal */ _initialField?: TInitialField + /** + * Creates a migration field used with the Prismic Migration API. + * + * @param initialField - The initial field value if any. + * + * @returns A migration field instance. + */ constructor(initialField?: TInitialField) { this._initialField = initialField } + /** + * Prepares the field to be stringified with {@link JSON.stringify} + * + * @returns The value of the field to be stringified. + * + * @internal + */ toJSON(): TField | undefined { return this._field } diff --git a/test/migration-updateDocument.test.ts b/test/migration-updateDocument.test.ts new file mode 100644 index 00000000..d48aaf58 --- /dev/null +++ b/test/migration-updateDocument.test.ts @@ -0,0 +1,17 @@ +import { expect, it } from "vitest" + +import * as prismic from "../src" +import { MigrationDocument } from "../src/types/migration/Document" + +it("updates a document", (ctx) => { + const migration = prismic.createMigration() + + const document = ctx.mock.value.document() + const documentTitle = "documentTitle" + + migration.updateDocument(document, documentTitle) + + const expectedDocument = new MigrationDocument(document, { documentTitle }) + expectedDocument._mode = "update" + expect(migration._documents[0]).toStrictEqual(expectedDocument) +}) diff --git a/test/writeClient-migrate-documents.test.ts b/test/writeClient-migrate-documents.test.ts index a1197304..a16c6028 100644 --- a/test/writeClient-migrate-documents.test.ts +++ b/test/writeClient-migrate-documents.test.ts @@ -77,7 +77,48 @@ it.concurrent("discovers existing documents", async (ctx) => { }) }) -it.concurrent("skips creating existing documents", async (ctx) => { +it.concurrent("skips creating existing documents (update)", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const queryResponse = createPagedQueryResponses({ + ctx, + pages: 1, + pageSize: 1, + }) + const document = queryResponse[0].results[0] + + mockPrismicRestAPIV2({ ctx, queryResponse }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, existingDocuments: [document] }) + + const migration = prismic.createMigration() + + const migrationDocument = migration.updateDocument(document, "foo") + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + expect(reporter).toHaveBeenCalledWith({ + type: "documents:skipping", + data: { + reason: "already exists", + current: 1, + remaining: 0, + total: 1, + document: migrationDocument, + }, + }) + expect(reporter).toHaveBeenCalledWith({ + type: "documents:created", + data: { + created: 0, + documents: expect.anything(), + }, + }) +}) + +it.concurrent("skips creating existing documents (auto)", async (ctx) => { const client = createTestWriteClient({ ctx }) const queryResponse = createPagedQueryResponses({ From f244807c4c874bb4c740d9e20a7773cb12324c07 Mon Sep 17 00:00:00 2001 From: lihbr Date: Fri, 13 Sep 2024 19:11:17 +0200 Subject: [PATCH 50/61] refactor: simplify migration documents traversing --- src/Migration.ts | 211 +++-- src/WriteClient.ts | 82 +- src/index.ts | 7 +- src/lib/isValue.ts | 53 +- ...ord.ts => prepareMigrationDocumentData.ts} | 11 +- src/types/migration/ContentRelationship.ts | 4 +- src/types/migration/Document.ts | 323 ++----- ...ate-patch-contentRelationship.test.ts.snap | 230 ++--- ...iteClient-migrate-patch-image.test.ts.snap | 794 +++++++++--------- ...ent-migrate-patch-linkToMedia.test.ts.snap | 240 +++--- ...ent-migrate-patch-rtImageNode.test.ts.snap | 474 ++++++----- .../testMigrationFieldPatching.ts | 34 +- test/migration-createDocument.test.ts | 16 +- test/migration-updateDocument.test.ts | 8 +- test/types/migration-document.types.ts | 14 +- test/types/migration.types.ts | 418 ++++++++- test/writeClient-migrate-documents.test.ts | 95 +-- ...-migrate-patch-contentRelationship.test.ts | 19 +- ...teClient-migrate-patch-simpleField.test.ts | 6 +- test/writeClient-migrate.test.ts | 8 +- test/writeClient-updateDocument.test.ts | 2 + 21 files changed, 1680 insertions(+), 1369 deletions(-) rename src/lib/{prepareMigrationRecord.ts => prepareMigrationDocumentData.ts} (91%) diff --git a/src/Migration.ts b/src/Migration.ts index bbd78244..303ca967 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -1,4 +1,4 @@ -import { prepareMigrationRecord } from "./lib/prepareMigrationRecord" +import { prepareMigrationDocumentData } from "./lib/prepareMigrationDocumentData" import { validateAssetMetadata } from "./lib/validateAssetMetadata" import type { Asset } from "./types/api/asset/asset" @@ -6,10 +6,10 @@ import type { MigrationAssetConfig } from "./types/migration/Asset" import { MigrationImage } from "./types/migration/Asset" import type { UnresolvedMigrationContentRelationshipConfig } from "./types/migration/ContentRelationship" import { MigrationContentRelationship } from "./types/migration/ContentRelationship" -import { MigrationDocument } from "./types/migration/Document" +import { PrismicMigrationDocument } from "./types/migration/Document" import type { - MigrationDocumentParams, - MigrationDocumentValue, + ExistingPrismicDocument, + PendingPrismicDocument, } from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" import type { FilledImageFieldImage } from "./types/value/image" @@ -24,29 +24,20 @@ import type { FilledLinkToMediaField } from "./types/value/linkToMedia" * @typeParam TDocumentType - Type(s) to match `TDocuments` against. */ type ExtractDocumentType< - TDocuments extends PrismicDocument | MigrationDocumentValue, + TDocuments extends { type: string }, TDocumentType extends TDocuments["type"], > = Extract extends never ? TDocuments : Extract -/** - * The symbol used to index documents that are singletons. - */ -const SINGLE_INDEX = "__SINGLE__" - /** * 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< - TDocuments extends PrismicDocument = PrismicDocument, - TMigrationDocuments extends - MigrationDocumentValue = MigrationDocumentValue, -> { +export class Migration { /** * Assets registered in the migration. * @@ -59,16 +50,7 @@ export class Migration< * * @internal */ - _documents: MigrationDocument[] = [] - - /** - * A map indexing documents by their type and UID used for quick lookups by - * the {@link getByUID} and {@link getSingle} methods. - */ - #indexedDocuments: Record< - string, - Record> - > = {} + _documents: PrismicMigrationDocument[] = [] /** * Registers an asset to be created in the migration from an asset object. @@ -248,43 +230,33 @@ export class Migration< * registers it in your migration. The document will be created when the * migration is executed through the `writeClient.migrate()` method. * - * @typeParam TType - Type of Prismic documents to create. + * @typeParam TType - Type of the Prismic document to create. * * @param document - The document to create. - * @param documentTitle - The title of the document to create which will be - * displayed in the editor. - * @param params - Document master language document ID. + * @param title - The title of the document to create which will be displayed + * in the editor. + * @param options - Document master language document ID. * * @returns A migration document instance. */ - createDocument( - document: ExtractDocumentType, - documentTitle: string, - params: Omit = {}, - ): MigrationDocument> { - const { record: data, dependencies } = prepareMigrationRecord( - document.data, + createDocument( + document: ExtractDocumentType, TType>, + title: string, + options?: { + masterLanguageDocument?: MigrationContentRelationship + }, + ): PrismicMigrationDocument> { + const { record: data, dependencies } = prepareMigrationDocumentData( + document.data!, this.createAsset.bind(this), ) - const migrationDocument = new MigrationDocument( - { ...document, data }, - { - documentTitle, - ...params, - }, - dependencies, - ) + const migrationDocument = new PrismicMigrationDocument< + ExtractDocumentType + >({ ...document, data }, title, { ...options, dependencies }) this._documents.push(migrationDocument) - // Index document - if (!(document.type in this.#indexedDocuments)) { - this.#indexedDocuments[document.type] = {} - } - this.#indexedDocuments[document.type][document.uid || SINGLE_INDEX] = - migrationDocument - return migrationDocument } @@ -304,18 +276,63 @@ export class Migration< * * @returns A migration document instance. */ - updateDocument( - document: Omit, "id"> & { - id: string - }, - documentTitle: string, - ): MigrationDocument> { - const migrationDocument = this.createDocument( - document as ExtractDocumentType, - documentTitle, + updateDocument( + document: ExtractDocumentType, TType>, + title: string, + ): PrismicMigrationDocument> { + const { record: data, dependencies } = prepareMigrationDocumentData( + document.data, + this.createAsset.bind(this), ) - migrationDocument._mode = "update" + const migrationDocument = new PrismicMigrationDocument< + ExtractDocumentType + >({ ...document, data }, title, { dependencies }) + + this._documents.push(migrationDocument) + + return migrationDocument + } + + /** + * 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. + * + * @param document - The document to create. + * @param title - The title of the document to create which will be displayed + * in the editor. + * @param options - Document master language document ID. + * + * @returns A migration document instance. + */ + createDocumentFromPrismic( + document: ExtractDocumentType, TType>, + title: string, + ): PrismicMigrationDocument> { + const { record: data, dependencies } = prepareMigrationDocumentData( + document.data, + this.createAsset.bind(this), + ) + + const migrationDocument = new PrismicMigrationDocument( + { + type: document.type, + lang: document.lang, + uid: document.uid, + tags: document.tags, + data: data, + } as unknown as PendingPrismicDocument< + ExtractDocumentType + >, + title, + { originalPrismicDocument: document, dependencies }, + ) + + this._documents.push(migrationDocument) return migrationDocument } @@ -352,19 +369,25 @@ export class Migration< * * @typeParam TType - Type of the Prismic document returned. * - * @param documentType - The API ID of the document's custom type. + * @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( - documentType: TType, + getByUID( + type: TType, uid: string, - ): MigrationDocument> | undefined { - return this.#indexedDocuments[documentType]?.[uid] as - | MigrationDocument - | undefined + ): + | PrismicMigrationDocument> + | undefined { + return this._documents.find( + ( + doc, + ): doc is PrismicMigrationDocument< + ExtractDocumentType + > => doc.document.type === type && doc.document.uid === uid, + ) } /** @@ -381,16 +404,54 @@ export class Migration< * * @typeParam TType - Type of the Prismic document returned. * - * @param documentType - The API ID of the singleton custom type. + * @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( - documentType: TType, - ): MigrationDocument> | undefined { - return this.#indexedDocuments[documentType]?.[SINGLE_INDEX] as - | MigrationDocument - | undefined + getSingle( + type: TType, + ): + | PrismicMigrationDocument> + | undefined { + return this._documents.find( + ( + doc, + ): doc is PrismicMigrationDocument< + ExtractDocumentType + > => doc.document.type === type, + ) } + + /** + * 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. + */ + // getByOriginalID( + // id: string, + // ): + // | PrismicMigrationDocument> + // | undefined { + // return this._documents.find( + // ( + // doc, + // ): doc is PrismicMigrationDocument< + // ExtractDocumentType + // > => doc.originalPrismicDocument?.id === id, + // ) + // } } diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 451b25f5..c46c6fd0 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -26,7 +26,8 @@ import type { AssetMap, MigrationAssetConfig } from "./types/migration/Asset" import type { DocumentMap, MigrationDocument, - MigrationDocumentValue, + PendingPrismicDocument, + PrismicMigrationDocument, } from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" @@ -115,13 +116,13 @@ type MigrateReporterEventMap = { current: number remaining: number total: number - document: MigrationDocument + document: PrismicMigrationDocument } "documents:creating": { current: number remaining: number total: number - document: MigrationDocument + document: PrismicMigrationDocument } "documents:created": { created: number @@ -131,7 +132,7 @@ type MigrateReporterEventMap = { current: number remaining: number total: number - document: MigrationDocument + document: PrismicMigrationDocument } "documents:updated": { updated: number @@ -517,12 +518,12 @@ export class WriteClient< documents.set(document.id, document) } - const sortedMigrationDocuments: MigrationDocument[] = [] + const sortedMigrationDocuments: PrismicMigrationDocument[] = [] // We create an array with non-master locale documents last because // we need their master locale document to be created first. for (const migrationDocument of migration._documents) { - if (migrationDocument.value.lang === masterLocale) { + if (migrationDocument.document.lang === masterLocale) { sortedMigrationDocuments.unshift(migrationDocument) } else { sortedMigrationDocuments.push(migrationDocument) @@ -531,24 +532,24 @@ export class WriteClient< let i = 0 let created = 0 - for (const document of sortedMigrationDocuments) { - if ( - document.value.id && - (document._mode === "update" || documents.has(document.value.id)) - ) { + for (const migrationDocument of sortedMigrationDocuments) { + if (migrationDocument.document.id) { reporter?.({ type: "documents:skipping", data: { - reason: "already exists", + reason: "exists", current: ++i, remaining: sortedMigrationDocuments.length - i, total: sortedMigrationDocuments.length, - document: document, + document: migrationDocument, }, }) // Index the migration document - documents.set(document, documents.get(document.value.id)!) + documents.set( + migrationDocument, + documents.get(migrationDocument.document.id)!, + ) } else { created++ reporter?.({ @@ -557,29 +558,28 @@ export class WriteClient< current: ++i, remaining: sortedMigrationDocuments.length - i, total: sortedMigrationDocuments.length, - document: document, + document: migrationDocument, }, }) // Resolve master language document ID for non-master locale documents let masterLanguageDocumentID: string | undefined - if (document.value.lang !== masterLocale) { - if (document.params.masterLanguageDocument) { - const link = document.params.masterLanguageDocument - - await link._resolve({ documents, assets: new Map() }) - masterLanguageDocumentID = link._field?.id - } else if (document.value.alternate_languages) { - masterLanguageDocumentID = document.value.alternate_languages.find( + if (migrationDocument.masterLanguageDocument) { + const link = migrationDocument.masterLanguageDocument + + await link._resolve({ documents, assets: new Map() }) + masterLanguageDocumentID = link._field?.id + } else if (migrationDocument.originalPrismicDocument) { + masterLanguageDocumentID = + migrationDocument.originalPrismicDocument.alternate_languages.find( ({ lang }) => lang === masterLocale, )?.id - } } const { id } = await this.createDocument( // We'll upload docuements data later on. - { ...document.value, data: {} }, - document.params.documentTitle, + { ...migrationDocument.document, data: {} }, + migrationDocument.title, { masterLanguageDocumentID, ...fetchParams, @@ -587,13 +587,13 @@ export class WriteClient< ) // Index old ID for Prismic to Prismic migration - if (document.value.id) { - documents.set(document.value.id, { - ...document.value, + if (migrationDocument.originalPrismicDocument) { + documents.set(migrationDocument.originalPrismicDocument.id, { + ...migrationDocument.document, id, }) } - documents.set(document, { ...document.value, id }) + documents.set(migrationDocument, { ...migrationDocument.document, id }) } } @@ -629,30 +629,25 @@ export class WriteClient< }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise { let i = 0 - for (const document of migration._documents) { + for (const migrationDocument of migration._documents) { reporter?.({ type: "documents:updating", data: { current: ++i, remaining: migration._documents.length - i, total: migration._documents.length, - document: document, + document: migrationDocument, }, }) - const { id, uid } = documents.get(document)! - await document._resolve({ assets, documents }) + const { id } = documents.get(migrationDocument)! + await migrationDocument._resolve({ assets, documents }) await this.updateDocument( id, // We need to forward again document name and tags to update them // in case the document already existed during the previous step. - { - documentTitle: document.params.documentTitle, - uid, - tags: document.value.tags, - data: document.value.data, - }, + migrationDocument.document, fetchParams, ) } @@ -1032,7 +1027,7 @@ export class WriteClient< * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ private async createDocument( - document: MigrationDocumentValue>, + document: PendingPrismicDocument>, documentTitle: string, { masterLanguageDocumentID, @@ -1074,10 +1069,7 @@ export class WriteClient< */ private async updateDocument( id: string, - document: Pick< - MigrationDocumentValue>, - "uid" | "tags" | "data" - > & { + document: MigrationDocument> & { documentTitle?: string }, params?: FetchParams, diff --git a/src/index.ts b/src/index.ts index 68187114..6750f34a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -327,9 +327,10 @@ export type { // Migrations - Types representing Prismic Migration API content values. export type { - MigrationDocument, - MigrationDocumentValue, - RichTextFieldWithMigrationField, + PrismicMigrationDocument, + PendingPrismicDocument, + ExistingPrismicDocument, + InjectMigrationSpecificTypes, } from "./types/migration/Document" export type { diff --git a/src/lib/isValue.ts b/src/lib/isValue.ts index 805ac593..90b72cbc 100644 --- a/src/lib/isValue.ts +++ b/src/lib/isValue.ts @@ -1,13 +1,24 @@ -import type { - FieldWithMigrationField, - RichTextBlockNodeWithMigrationField, -} from "../types/migration/Document" +import type { InjectMigrationSpecificTypes } from "../types/migration/Document" import type { FilledContentRelationshipField } from "../types/value/contentRelationship" import type { PrismicDocument } from "../types/value/document" +import type { GroupField } from "../types/value/group" import type { ImageField } from "../types/value/image" import { LinkType } from "../types/value/link" import type { FilledLinkToMediaField } from "../types/value/linkToMedia" import { type RTImageNode, RichTextNodeType } from "../types/value/richText" +import type { SliceZone } from "../types/value/sliceZone" +import type { AnyRegularField } from "../types/value/types" + +/** + * Unknown value to check if it's a specific field type. + * + * @remarks + * Explicit types are added to help ensure narrowing is done effectively. + */ +type UnknownValue = + | PrismicDocument + | InjectMigrationSpecificTypes + | unknown /** * Checks if a value is a link to media field. @@ -20,11 +31,7 @@ import { type RTImageNode, RichTextNodeType } from "../types/value/richText" * This is not an official helper function and it's only designed to work with internal processes. */ export const linkToMedia = ( - value: - | PrismicDocument - | FieldWithMigrationField - | RichTextBlockNodeWithMigrationField - | unknown, + value: UnknownValue, ): value is FilledLinkToMediaField => { if (value && typeof value === "object" && !("version" in value)) { if ( @@ -36,6 +43,8 @@ export const linkToMedia = ( "url" in value && "size" in value ) { + value + return true } } @@ -54,11 +63,7 @@ export const linkToMedia = ( * This is not an official helper function and it's only designed to work with internal processes. */ const imageLike = ( - value: - | PrismicDocument - | FieldWithMigrationField - | RichTextBlockNodeWithMigrationField - | unknown, + value: UnknownValue, ): value is ImageField | RTImageNode => { if ( value && @@ -92,11 +97,7 @@ const imageLike = ( * This is not an official helper function and it's only designed to work with internal processes. */ export const image = ( - value: - | PrismicDocument - | FieldWithMigrationField - | RichTextBlockNodeWithMigrationField - | unknown, + value: UnknownValue, ): value is ImageField => { if ( imageLike(value) && @@ -120,13 +121,7 @@ export const image = ( * @internal * This is not an official helper function and it's only designed to work with internal processes. */ -export const rtImageNode = ( - value: - | PrismicDocument - | FieldWithMigrationField - | RichTextBlockNodeWithMigrationField - | unknown, -): value is RTImageNode => { +export const rtImageNode = (value: UnknownValue): value is RTImageNode => { if ( imageLike(value) && "type" in value && @@ -151,11 +146,7 @@ export const rtImageNode = ( * This is not an official helper function and it's only designed to work with internal processes. */ export const contentRelationship = ( - value: - | PrismicDocument - | FieldWithMigrationField - | RichTextBlockNodeWithMigrationField - | unknown, + value: UnknownValue, ): value is FilledContentRelationshipField => { if (value && typeof value === "object" && !("version" in value)) { if ( diff --git a/src/lib/prepareMigrationRecord.ts b/src/lib/prepareMigrationDocumentData.ts similarity index 91% rename from src/lib/prepareMigrationRecord.ts rename to src/lib/prepareMigrationDocumentData.ts index 64055503..4bfd01de 100644 --- a/src/lib/prepareMigrationRecord.ts +++ b/src/lib/prepareMigrationDocumentData.ts @@ -22,7 +22,10 @@ import * as is from "./isValue" * @returns An object containing the record with replaced assets and links and a * list of dependencies found and/or created. */ -export const prepareMigrationRecord = >( +export const prepareMigrationDocumentData = < + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TRecord extends Record, +>( record: TRecord, onAsset: ( asset: FilledImageFieldImage | FilledLinkToMediaField, @@ -52,7 +55,7 @@ export const prepareMigrationRecord = >( if (field.linkTo) { // Node `linkTo` dependency is tracked internally - rtImageNode.linkTo = prepareMigrationRecord( + rtImageNode.linkTo = prepareMigrationDocumentData( { linkTo: field.linkTo }, onAsset, ).record.linkTo as @@ -103,7 +106,7 @@ export const prepareMigrationRecord = >( for (const item of field) { const { record, dependencies: itemDependencies } = - prepareMigrationRecord({ item }, onAsset) + prepareMigrationDocumentData({ item }, onAsset) array.push(record.item) dependencies.push(...itemDependencies) @@ -113,7 +116,7 @@ export const prepareMigrationRecord = >( } else if (field && typeof field === "object") { // Traverse objects const { record, dependencies: fieldDependencies } = - prepareMigrationRecord({ ...field }, onAsset) + prepareMigrationDocumentData({ ...field }, onAsset) dependencies.push(...fieldDependencies) result[key] = record diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts index cc56c1ae..33abf26a 100644 --- a/src/types/migration/ContentRelationship.ts +++ b/src/types/migration/ContentRelationship.ts @@ -2,7 +2,7 @@ import type { FilledContentRelationshipField } from "../value/contentRelationshi import type { PrismicDocument } from "../value/document" import { LinkType } from "../value/link" -import type { MigrationDocument } from "./Document" +import type { PrismicMigrationDocument } from "./Document" import type { ResolveArgs } from "./Field" import { MigrationField } from "./Field" @@ -13,7 +13,7 @@ type MigrationContentRelationshipConfig< TDocuments extends PrismicDocument = PrismicDocument, > = | TDocuments - | MigrationDocument + | PrismicMigrationDocument | FilledContentRelationshipField | undefined diff --git a/src/types/migration/Document.ts b/src/types/migration/Document.ts index c109bcbe..54df4497 100644 --- a/src/types/migration/Document.ts +++ b/src/types/migration/Document.ts @@ -1,21 +1,8 @@ -import type { AnyRegularField } from "../value/types" - import type { FilledContentRelationshipField } from "../value/contentRelationship" -import type { PrismicDocument } from "../value/document" -import type { GroupField } from "../value/group" +import type { PrismicDocument, PrismicDocumentWithUID } from "../value/document" import type { FilledImageFieldImage } from "../value/image" import type { FilledLinkToMediaField } from "../value/linkToMedia" -import type { - RTBlockNode, - RTImageNode, - RTInlineNode, - RTLinkNode, - RTTextNode, - RichTextField, -} from "../value/richText" -import type { SharedSlice } from "../value/sharedSlice" -import type { Slice } from "../value/slice" -import type { SliceZone } from "../value/sliceZone" +import type { RTImageNode } from "../value/richText" import type { MigrationImage, @@ -26,261 +13,122 @@ import type { MigrationContentRelationship } from "./ContentRelationship" import type { MigrationField, ResolveArgs } from "./Field" /** - * A utility type that extends Rich text field node's spans with their migration - * node equivalent. - * - * @typeParam TRTNode - Rich text text node type to convert. - */ -type RichTextTextNodeWithMigrationField< - TRTNode extends RTTextNode = RTTextNode, -> = Omit & { - spans: ( - | RTInlineNode - | (Omit & { - data: MigrationLinkToMedia | MigrationContentRelationship - }) - )[] -} - -/** - * A utility type that extends a Rich text field node with their migration node - * equivalent. - * - * @typeParam TRTNode - Rich text block node type to convert. - */ -export type RichTextBlockNodeWithMigrationField< - TRTNode extends RTBlockNode = RTBlockNode, -> = TRTNode extends RTImageNode - ? RTImageNode | MigrationRTImageNode - : TRTNode extends RTTextNode - ? RichTextTextNodeWithMigrationField - : TRTNode - -/** - * A utility type that extends a Rich text field's nodes with their migration - * node equivalent. - * - * @typeParam TField - Rich text field type to convert. - */ -export type RichTextFieldWithMigrationField< - TField extends RichTextField = RichTextField, -> = { - [Index in keyof TField]: RichTextBlockNodeWithMigrationField -} - -/** - * A utility type that extends a regular field with their migration field - * equivalent. - * - * @typeParam TField - Regular field type to convert. - */ -type RegularFieldWithMigrationField< - TField extends AnyRegularField = AnyRegularField, -> = - | (TField extends FilledImageFieldImage - ? MigrationImage | undefined - : TField extends FilledLinkToMediaField - ? MigrationLinkToMedia | undefined - : TField extends FilledContentRelationshipField - ? MigrationContentRelationship | undefined - : TField extends RichTextField - ? RichTextFieldWithMigrationField - : never) - | TField - -/** - * A utility type that extends a group's fields with their migration fields - * equivalent. - * - * @typeParam TField - Group field type to convert. - */ -type GroupFieldWithMigrationField< - TField extends GroupField | Slice["items"] | SharedSlice["items"] = - | GroupField - | Slice["items"] - | SharedSlice["items"], -> = FieldsWithMigrationFields[] - -type SliceWithMigrationField< - TField extends Slice | SharedSlice = Slice | SharedSlice, -> = Omit & { - primary: FieldsWithMigrationFields - items: GroupFieldWithMigrationField -} - -/** - * A utility type that extends a SliceZone's slices fields with their migration + * A utility type that extends any fields in a record with their migration * fields equivalent. * - * @typeParam TField - Field type to convert. + * @typeParam T - Type of the record to extend. */ -type SliceZoneWithMigrationField = - SliceWithMigrationField[] +export type InjectMigrationSpecificTypes = T extends RTImageNode + ? T | MigrationRTImageNode | undefined + : T extends FilledImageFieldImage + ? T | MigrationImage | undefined + : T extends FilledLinkToMediaField + ? T | MigrationLinkToMedia | undefined + : T extends FilledContentRelationshipField + ? T | MigrationContentRelationship | undefined + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Record + ? { [P in keyof T]: InjectMigrationSpecificTypes } + : T extends Array + ? Array> + : T /** - * A utility type that extends any field with their migration field equivalent. - * - * @typeParam TField - Field type to convert. + * A utility type that ties the type and data of a Prismic document, creating a + * strict union. */ -export type FieldWithMigrationField< - TField extends AnyRegularField | GroupField | SliceZone = - | AnyRegularField - | GroupField - | SliceZone, -> = TField extends AnyRegularField - ? RegularFieldWithMigrationField - : TField extends GroupField - ? GroupFieldWithMigrationField - : TField extends SliceZone - ? SliceZoneWithMigrationField - : never +type TiedDocumentTypeAndData = + TDocument extends PrismicDocument + ? { + /** + * Type of the document. + */ + type: TType + + /** + * Data contained in the document. + */ + data: InjectMigrationSpecificTypes + } & (TDocument extends PrismicDocumentWithUID + ? Pick + : Partial>) + : never /** - * A utility type that extends a record of fields with their migration fields - * equivalent. + * A pending Prismic document to be created with the Migration API. * - * @typeParam TFields - Type of the record of Prismic fields. + * @typeParam TDocument - Type of the Prismic document. */ -export type FieldsWithMigrationFields< - TFields extends Record< - string, - AnyRegularField | GroupField | SliceZone - > = Record, -> = { - [Key in keyof TFields]: FieldWithMigrationField -} +export type PendingPrismicDocument< + TDocument extends PrismicDocument = PrismicDocument, +> = Pick & + Partial> & + TiedDocumentTypeAndData /** - * Makes the UID of {@link MigrationDocumentValue} optional on custom types - * without UID. TypeScript fails to infer correct types if done with a type - * intersection. + * An existing Prismic document to be updated with the Migration API. * - * @internal + * @typeParam TDocument - Type of the Prismic document. */ -type MakeUIDOptional = - TMigrationDocument["uid"] extends string - ? TMigrationDocument - : Omit & Partial> +export type ExistingPrismicDocument< + TDocument extends PrismicDocument = PrismicDocument, +> = Omit & + TiedDocumentTypeAndData /** - * A Prismic document value compatible with the Migration API. + * A Prismic document to be sent to the Migration API. * - * @see More details on the Migration API: {@link https://prismic.io/docs/migration-api-technical-reference} + * @typeParam TDocument - Type of the Prismic document. */ -export type MigrationDocumentValue< +export type MigrationDocument< TDocument extends PrismicDocument = PrismicDocument, -> = - TDocument extends PrismicDocument - ? MakeUIDOptional<{ - /** - * Type of the document. - */ - type: TType - - /** - * The unique identifier for the document. Guaranteed to be unique among - * all Prismic documents of the same type. - */ - uid: TDocument["uid"] - - /** - * Language of document. - */ - lang: TLang - - /** - * The identifier for the document. Used for compatibily with the - * content API. - * - * @internal - */ - // Made optional compared to the original type. - id?: TDocument["id"] - - /** - * Alternate language documents from Prismic content API. Used as a - * substitute to the `masterLanguageDocument` options when the latter is - * not available. - * - * @internal - */ - // Made optional compared to the original type. - alternate_languages?: TDocument["alternate_languages"] - - /** - * Tags associated with document. - */ - // Made optional compared to the original type. - tags?: TDocument["tags"] - - /** - * Data contained in the document. - */ - data: FieldsWithMigrationFields - }> - : never +> = PendingPrismicDocument | ExistingPrismicDocument /** - * Parameters used when creating a Prismic document with the Migration API. + * A Prismic migration document instance. * - * @see More details on the Migration API: {@link https://prismic.io/docs/migration-api-technical-reference} + * @typeParam TDocument - Type of the Prismic document. */ -export type MigrationDocumentParams = { +export class PrismicMigrationDocument< + TDocument extends PrismicDocument = PrismicDocument, +> { /** - * Name of the document displayed in the editor. + * The document to be sent to the Migration API. */ - documentTitle: string + document: MigrationDocument & Partial> /** - * A link to the master language document. + * The name of the document displayed in the editor. */ + title: string + // We're forced to inline `ContentRelationshipMigrationField` here, otherwise // it creates a circular reference to itself which makes TypeScript unhappy. // (but I think it's weird and it doesn't make sense :thinking:) masterLanguageDocument?: MigrationContentRelationship -} -/** - * A Prismic migration document instance. - * - * @see More details on the Migration API: {@link https://prismic.io/docs/migration-api-technical-reference} - */ -export class MigrationDocument< - TDocument extends PrismicDocument = PrismicDocument, -> { /** - * The document value to be sent to the Migration API. - */ - value: MigrationDocumentValue - - /** - * Parameters to create/update the document with on the Migration API. + * Original Prismic document when the migration document came from another + * Prismic repository. + * + * @remarks + * When migrating a document from another repository, one might want to alter + * it with migration specific types, hence accepting an + * `ExistingPrismicDocument` instead of a regular `PrismicDocument`. */ - params: MigrationDocumentParams + originalPrismicDocument?: ExistingPrismicDocument /** * Asset and content relationship fields that this document depends on. */ #dependencies: MigrationField[] - /** - * The mode to use when creating or updating the document. - * - * - `auto`: Automatically determines if the document should be created or - * updated on the document's existence on the repository's Document API. - * - `update`: Forces the document to only be updated. This is useful when the - * document only exists within the migration release which cannot be - * queried. - * - * @internal - */ - _mode: "auto" | "update" = "auto" - /** * Creates a Prismic migration document instance. * - * @param value - The document value to be sent to the Migration API. - * @param params - Parameters to create/update the document with on the + * @param document - The document to be sent to the Migration API. + * @param title - The name of the document displayed in the editor. + * @param options - Parameters to create/update the document with on the * Migration API. * @param dependencies - Asset and content relationship fields that this * document depends on. @@ -288,13 +136,19 @@ export class MigrationDocument< * @returns A Prismic migration document instance. */ constructor( - value: MigrationDocumentValue, - params: MigrationDocumentParams, - dependencies: MigrationField[] = [], + document: MigrationDocument, + title: string, + options: { + masterLanguageDocument?: MigrationContentRelationship + originalPrismicDocument?: ExistingPrismicDocument + dependencies: MigrationField[] + }, ) { - this.value = value - this.params = params - this.#dependencies = dependencies + this.document = document + this.title = title + this.masterLanguageDocument = options.masterLanguageDocument + this.originalPrismicDocument = options.originalPrismicDocument + this.#dependencies = options.dependencies } /** @@ -323,7 +177,6 @@ export class MigrationDocument< */ export type DocumentMap = Map< - string | MigrationDocument | MigrationDocument, - | PrismicDocument - | (Omit, "id"> & { id: string }) + string | PrismicMigrationDocument, + PrismicDocument | (MigrationDocument & Pick) > diff --git a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap index 739bcda2..c15c15c2 100644 --- a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap @@ -12,7 +12,7 @@ exports[`patches link fields > brokenLink > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ {}, ], @@ -22,9 +22,9 @@ exports[`patches link fields > brokenLink > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -34,13 +34,13 @@ exports[`patches link fields > brokenLink > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ {}, ], "primary": {}, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -69,7 +69,7 @@ exports[`patches link fields > existing > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { @@ -111,9 +111,9 @@ exports[`patches link fields > existing > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -123,7 +123,7 @@ exports[`patches link fields > existing > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { @@ -150,8 +150,8 @@ exports[`patches link fields > existing > slice 1`] = ` "type": "amet", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -191,7 +191,7 @@ exports[`patches link fields > lazyExisting > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { @@ -233,9 +233,9 @@ exports[`patches link fields > lazyExisting > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -245,7 +245,7 @@ exports[`patches link fields > lazyExisting > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { @@ -272,8 +272,8 @@ exports[`patches link fields > lazyExisting > slice 1`] = ` "type": "amet", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -297,7 +297,7 @@ exports[`patches link fields > lazyMigration > group 1`] = ` "group": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "a", "link_type": "Document", @@ -316,11 +316,11 @@ exports[`patches link fields > lazyMigration > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -332,7 +332,7 @@ exports[`patches link fields > lazyMigration > shared slice 1`] = ` ], "primary": { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -343,7 +343,7 @@ exports[`patches link fields > lazyMigration > shared slice 1`] = ` "group": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -355,9 +355,9 @@ exports[`patches link fields > lazyMigration > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -367,11 +367,11 @@ exports[`patches link fields > lazyMigration > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "ultrices", "link_type": "Document", @@ -385,7 +385,7 @@ exports[`patches link fields > lazyMigration > slice 1`] = ` ], "primary": { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "ultrices", "link_type": "Document", @@ -396,8 +396,8 @@ exports[`patches link fields > lazyMigration > slice 1`] = ` "uid": "Amet dictum sit", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -406,7 +406,7 @@ exports[`patches link fields > lazyMigration > slice 1`] = ` exports[`patches link fields > lazyMigration > static zone 1`] = ` { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "nisi,", "link_type": "Document", @@ -422,7 +422,7 @@ exports[`patches link fields > migration > group 1`] = ` "group": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "a", "link_type": "Document", @@ -441,11 +441,11 @@ exports[`patches link fields > migration > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -457,7 +457,7 @@ exports[`patches link fields > migration > shared slice 1`] = ` ], "primary": { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -468,7 +468,7 @@ exports[`patches link fields > migration > shared slice 1`] = ` "group": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -480,9 +480,9 @@ exports[`patches link fields > migration > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -492,11 +492,11 @@ exports[`patches link fields > migration > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "ultrices", "link_type": "Document", @@ -510,7 +510,7 @@ exports[`patches link fields > migration > slice 1`] = ` ], "primary": { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "ultrices", "link_type": "Document", @@ -521,8 +521,8 @@ exports[`patches link fields > migration > slice 1`] = ` "uid": "Amet dictum sit", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -531,7 +531,7 @@ exports[`patches link fields > migration > slice 1`] = ` exports[`patches link fields > migration > static zone 1`] = ` { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "nisi,", "link_type": "Document", @@ -547,7 +547,7 @@ exports[`patches link fields > migrationNoTags > group 1`] = ` "group": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "a", "link_type": "Document", @@ -564,11 +564,11 @@ exports[`patches link fields > migrationNoTags > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -580,7 +580,7 @@ exports[`patches link fields > migrationNoTags > shared slice 1`] = ` ], "primary": { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -591,7 +591,7 @@ exports[`patches link fields > migrationNoTags > shared slice 1`] = ` "group": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "gravida", "link_type": "Document", @@ -603,9 +603,9 @@ exports[`patches link fields > migrationNoTags > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -615,11 +615,11 @@ exports[`patches link fields > migrationNoTags > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "ultrices", "link_type": "Document", @@ -631,7 +631,7 @@ exports[`patches link fields > migrationNoTags > slice 1`] = ` ], "primary": { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "ultrices", "link_type": "Document", @@ -640,8 +640,8 @@ exports[`patches link fields > migrationNoTags > slice 1`] = ` "uid": "Amet dictum sit", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -650,7 +650,7 @@ exports[`patches link fields > migrationNoTags > slice 1`] = ` exports[`patches link fields > migrationNoTags > static zone 1`] = ` { "field": { - "id": "id-migration", + "id": "id-other", "isBroken": false, "lang": "nisi,", "link_type": "Document", @@ -666,15 +666,13 @@ exports[`patches link fields > otherRepositoryContentRelationship > group 1`] = "group": [ { "field": { - "id": "id-migration", + "id": "id-other-repository", "isBroken": false, - "lang": "a", + "lang": "faucibus", "link_type": "Document", - "tags": [ - "Odio Eu", - ], - "type": "amet", - "uid": "Non sodales neque", + "tags": [], + "type": "lorem", + "uid": "Pharetra et ultrices", }, }, ], @@ -685,48 +683,54 @@ exports[`patches link fields > otherRepositoryContentRelationship > shared slice { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { - "id": "id-migration", + "id": "id-other-repository", "isBroken": false, - "lang": "gravida", + "lang": "eu", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", + "tags": [ + "Pharetra", + ], + "type": "pellentesque", + "uid": "Volutpat ac tincidunt", }, }, ], "primary": { "field": { - "id": "id-migration", + "id": "id-other-repository", "isBroken": false, - "lang": "gravida", + "lang": "eu", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", + "tags": [ + "Pharetra", + ], + "type": "pellentesque", + "uid": "Volutpat ac tincidunt", }, "group": [ { "field": { - "id": "id-migration", + "id": "id-other-repository", "isBroken": false, - "lang": "gravida", + "lang": "eu", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", + "tags": [ + "Pharetra", + ], + "type": "pellentesque", + "uid": "Volutpat ac tincidunt", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -736,37 +740,37 @@ exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { - "id": "id-migration", + "id": "id-other-repository", "isBroken": false, - "lang": "ultrices", + "lang": "dignissim", "link_type": "Document", "tags": [ - "Ipsum Consequat", + "Arcu Vitae", ], - "type": "eget", - "uid": "Amet dictum sit", + "type": "a", + "uid": "Tristique senectus et", }, }, ], "primary": { "field": { - "id": "id-migration", + "id": "id-other-repository", "isBroken": false, - "lang": "ultrices", + "lang": "dignissim", "link_type": "Document", "tags": [ - "Ipsum Consequat", + "Arcu Vitae", ], - "type": "eget", - "uid": "Amet dictum sit", + "type": "a", + "uid": "Tristique senectus et", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -775,13 +779,13 @@ exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = exports[`patches link fields > otherRepositoryContentRelationship > static zone 1`] = ` { "field": { - "id": "id-migration", + "id": "id-other-repository", "isBroken": false, - "lang": "nisi,", + "lang": "quam", "link_type": "Document", "tags": [], - "type": "eros", - "uid": "Scelerisque mauris pellentesque", + "type": "semper", + "uid": "Sed viverra ipsum", }, } `; @@ -825,7 +829,7 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ @@ -921,9 +925,9 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -933,7 +937,7 @@ exports[`patches link fields > richTextLinkNode > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ @@ -996,8 +1000,8 @@ exports[`patches link fields > richTextLinkNode > slice 1`] = ` }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } diff --git a/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap index 1a477d27..98f4ebb3 100644 --- a/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap @@ -18,7 +18,7 @@ exports[`patches image fields > existing > group 1`] = ` "zoom": 1, }, "id": "id-existing", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", }, }, ], @@ -29,7 +29,7 @@ exports[`patches image fields > existing > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { @@ -46,7 +46,7 @@ exports[`patches image fields > existing > shared slice 1`] = ` "zoom": 1, }, "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], @@ -65,7 +65,7 @@ exports[`patches image fields > existing > shared slice 1`] = ` "zoom": 1, }, "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, "group": [ { @@ -83,15 +83,15 @@ exports[`patches image fields > existing > shared slice 1`] = ` "zoom": 1, }, "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -101,7 +101,7 @@ exports[`patches image fields > existing > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { @@ -140,8 +140,8 @@ exports[`patches image fields > existing > slice 1`] = ` "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -163,7 +163,7 @@ exports[`patches image fields > existing > static zone 1`] = ` "zoom": 1, }, "id": "id-existing", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, } `; @@ -185,8 +185,8 @@ exports[`patches image fields > new > group 1`] = ` "y": 0, "zoom": 1, }, - "id": "ef95d5daa4d", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", + "id": "dfdd322ca9d", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", }, }, ], @@ -197,7 +197,7 @@ exports[`patches image fields > new > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { @@ -213,8 +213,8 @@ exports[`patches image fields > new > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "id": "0eddbd0255b", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", }, }, ], @@ -232,8 +232,8 @@ exports[`patches image fields > new > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "id": "0eddbd0255b", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", }, "group": [ { @@ -250,16 +250,16 @@ exports[`patches image fields > new > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "f2c3f8bfc90", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "id": "0eddbd0255b", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -269,7 +269,7 @@ exports[`patches image fields > new > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { @@ -285,8 +285,8 @@ exports[`patches image fields > new > slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "45306297c5e", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "id": "2beb55ec2f4", + "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", }, }, ], @@ -304,12 +304,12 @@ exports[`patches image fields > new > slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "45306297c5e", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "id": "2beb55ec2f4", + "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -330,8 +330,8 @@ exports[`patches image fields > new > static zone 1`] = ` "y": 0, "zoom": 1, }, - "id": "c5c95f8d3ac", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "id": "bbad670dad7", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, } `; @@ -341,20 +341,20 @@ exports[`patches image fields > otherRepository > group 1`] = ` "group": [ { "field": { - "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, + "background": "#cfc26f", + "x": -2885, + "y": -2419, + "zoom": 1.7509753524211642, }, - "id": "4dcf07cfc26", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + "id": "8d0985c0990", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f", }, }, ], @@ -365,69 +365,69 @@ exports[`patches image fields > otherRepository > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { - "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, + "background": "#9ed252", + "x": -1720, + "y": -191, + "zoom": 1.6165685319845957, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", }, }, ], "primary": { "field": { - "alt": "Ut consequat semper viverra nam libero justo laoreet", + "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, + "background": "#4f4f59", + "x": 3094, + "y": -2976, + "zoom": 1.0855120641844143, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", }, "group": [ { "field": { - "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "alt": "Id aliquet risus feugiat in", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, + "background": "#42f5cf", + "x": 427, + "y": 731, + "zoom": 1.6417661415510265, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -437,47 +437,47 @@ exports[`patches image fields > otherRepository > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { - "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "alt": "Pellentesque nec nam aliquam sem", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, + "background": "#f72dec", + "x": 2027, + "y": 7, + "zoom": 1.9216652845315787, }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c", + "id": "7a35bc5aa2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", }, }, ], "primary": { "field": { - "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, + "background": "#60cbac", + "x": -484, + "y": -2038, + "zoom": 1.8400009569805118, }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c", + "id": "7a35bc5aa2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -486,20 +486,20 @@ exports[`patches image fields > otherRepository > slice 1`] = ` exports[`patches image fields > otherRepository > static zone 1`] = ` { "field": { - "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "alt": "Gravida rutrum quisque non tellus orci", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, + "background": "#b1d17b", + "x": 3291, + "y": -730, + "zoom": 1.061566393836766, }, - "id": "9abffab1d17", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f", + "id": "4a95cc61c37", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d", }, } `; @@ -524,7 +524,7 @@ exports[`patches image fields > otherRepositoryEmpty > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { @@ -557,9 +557,9 @@ exports[`patches image fields > otherRepositoryEmpty > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -569,7 +569,7 @@ exports[`patches image fields > otherRepositoryEmpty > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { @@ -590,8 +590,8 @@ exports[`patches image fields > otherRepositoryEmpty > slice 1`] = ` "url": null, }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -614,36 +614,36 @@ exports[`patches image fields > otherRepositoryWithThumbnails > group 1`] = ` "group": [ { "field": { - "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, + "background": "#cfc26f", + "x": -2885, + "y": -2419, + "zoom": 1.7509753524211642, }, - "id": "4dcf07cfc26", + "id": "8d0985c0990", "square": { - "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, + "background": "#cfc26f", + "x": -2885, + "y": -2419, + "zoom": 1.7509753524211642, }, - "id": "4dcf07cfc26", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", + "id": "8d0985c0990", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", }, }, ], @@ -654,117 +654,117 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { - "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, + "background": "#9ed252", + "x": -1720, + "y": -191, + "zoom": 1.6165685319845957, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "square": { - "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, + "background": "#9ed252", + "x": -1720, + "y": -191, + "zoom": 1.6165685319845957, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], "primary": { "field": { - "alt": "Ut consequat semper viverra nam libero justo laoreet", + "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, + "background": "#4f4f59", + "x": 3094, + "y": -2976, + "zoom": 1.0855120641844143, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "square": { - "alt": "Ut consequat semper viverra nam libero justo laoreet", + "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, + "background": "#4f4f59", + "x": 3094, + "y": -2976, + "zoom": 1.0855120641844143, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, "group": [ { "field": { - "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "alt": "Id aliquet risus feugiat in", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, + "background": "#42f5cf", + "x": 427, + "y": 731, + "zoom": 1.6417661415510265, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "square": { - "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "alt": "Id aliquet risus feugiat in", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, + "background": "#42f5cf", + "x": 427, + "y": 731, + "zoom": 1.6417661415510265, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -774,79 +774,79 @@ exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { - "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "alt": "Pellentesque nec nam aliquam sem", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, + "background": "#f72dec", + "x": 2027, + "y": 7, + "zoom": 1.9216652845315787, }, - "id": "4f4a69ff72d", + "id": "7a35bc5aa2f", "square": { - "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "alt": "Pellentesque nec nam aliquam sem", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, + "background": "#f72dec", + "x": 2027, + "y": 7, + "zoom": 1.9216652845315787, }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + "id": "7a35bc5aa2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", }, }, ], "primary": { "field": { - "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, + "background": "#60cbac", + "x": -484, + "y": -2038, + "zoom": 1.8400009569805118, }, - "id": "4f4a69ff72d", + "id": "7a35bc5aa2f", "square": { - "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, + "background": "#60cbac", + "x": -484, + "y": -2038, + "zoom": 1.8400009569805118, }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + "id": "7a35bc5aa2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -855,36 +855,36 @@ exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` exports[`patches image fields > otherRepositoryWithThumbnails > static zone 1`] = ` { "field": { - "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "alt": "Gravida rutrum quisque non tellus orci", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, + "background": "#b1d17b", + "x": 3291, + "y": -730, + "zoom": 1.061566393836766, }, - "id": "9abffab1d17", + "id": "4a95cc61c37", "square": { - "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "alt": "Gravida rutrum quisque non tellus orci", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, + "background": "#b1d17b", + "x": 3291, + "y": -730, + "zoom": 1.061566393836766, }, - "id": "9abffab1d17", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + "id": "4a95cc61c37", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=query", }, } `; @@ -901,12 +901,12 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = "width": 1, }, "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, + "background": "#cfc26f", + "x": -2885, + "y": -2419, + "zoom": 1.7509753524211642, }, - "id": "4dcf07cfc26", + "id": "8d0985c0990", "square": { "alt": null, "copyright": null, @@ -915,15 +915,15 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = "width": 1, }, "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, + "background": "#cfc26f", + "x": -2885, + "y": -2419, + "zoom": 1.7509753524211642, }, - "id": "4dcf07cfc26", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", + "id": "8d0985c0990", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", }, }, ], @@ -934,7 +934,7 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { @@ -945,12 +945,12 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, + "background": "#9ed252", + "x": -1720, + "y": -191, + "zoom": 1.6165685319845957, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "square": { "alt": null, "copyright": null, @@ -959,15 +959,15 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, + "background": "#9ed252", + "x": -1720, + "y": -191, + "zoom": 1.6165685319845957, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], @@ -980,12 +980,12 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, + "background": "#4f4f59", + "x": 3094, + "y": -2976, + "zoom": 1.0855120641844143, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "square": { "alt": null, "copyright": null, @@ -994,15 +994,15 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, + "background": "#4f4f59", + "x": 3094, + "y": -2976, + "zoom": 1.0855120641844143, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, "group": [ { @@ -1014,12 +1014,12 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, + "background": "#42f5cf", + "x": 427, + "y": 731, + "zoom": 1.6417661415510265, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "square": { "alt": null, "copyright": null, @@ -1028,23 +1028,23 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, + "background": "#42f5cf", + "x": 427, + "y": 731, + "zoom": 1.6417661415510265, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -1054,7 +1054,7 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { @@ -1065,12 +1065,12 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "width": 1, }, "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, + "background": "#f72dec", + "x": 2027, + "y": 7, + "zoom": 1.9216652845315787, }, - "id": "4f4a69ff72d", + "id": "7a35bc5aa2f", "square": { "alt": null, "copyright": null, @@ -1079,15 +1079,15 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "width": 1, }, "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, + "background": "#f72dec", + "x": 2027, + "y": 7, + "zoom": 1.9216652845315787, }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + "id": "7a35bc5aa2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", }, }, ], @@ -1100,12 +1100,12 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "width": 1, }, "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, + "background": "#60cbac", + "x": -484, + "y": -2038, + "zoom": 1.8400009569805118, }, - "id": "4f4a69ff72d", + "id": "7a35bc5aa2f", "square": { "alt": null, "copyright": null, @@ -1114,19 +1114,19 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "width": 1, }, "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, + "background": "#60cbac", + "x": -484, + "y": -2038, + "zoom": 1.8400009569805118, }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + "id": "7a35bc5aa2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -1142,12 +1142,12 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone "width": 1, }, "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, + "background": "#b1d17b", + "x": 3291, + "y": -730, + "zoom": 1.061566393836766, }, - "id": "9abffab1d17", + "id": "4a95cc61c37", "square": { "alt": null, "copyright": null, @@ -1156,15 +1156,15 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone "width": 1, }, "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, + "background": "#b1d17b", + "x": 3291, + "y": -730, + "zoom": 1.061566393836766, }, - "id": "9abffab1d17", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + "id": "4a95cc61c37", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=query", }, } `; @@ -1174,36 +1174,36 @@ exports[`patches image fields > otherRepositoryWithTypeThumbnail > group 1`] = ` "group": [ { "field": { - "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, + "background": "#cfc26f", + "x": -2885, + "y": -2419, + "zoom": 1.7509753524211642, }, - "id": "4dcf07cfc26", + "id": "8d0985c0990", "type": { - "alt": "Odio ut enim blandit volutpat maecenas volutpat blandit", + "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#fdb338", - "x": 349, - "y": 115, - "zoom": 1.5951464943576479, + "background": "#cfc26f", + "x": -2885, + "y": -2419, + "zoom": 1.7509753524211642, }, - "id": "4dcf07cfc26", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=other&query=params", + "id": "8d0985c0990", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?some=query", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", }, }, ], @@ -1214,117 +1214,117 @@ exports[`patches image fields > otherRepositoryWithTypeThumbnail > shared slice { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { - "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, + "background": "#9ed252", + "x": -1720, + "y": -191, + "zoom": 1.6165685319845957, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "type": { - "alt": "Vitae et leo duis ut diam quam nulla porttitor massa id neque aliquam vestibulum", + "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#61adcf", - "x": -1586, - "y": -2819, - "zoom": 1.9393723758262054, + "background": "#9ed252", + "x": -1720, + "y": -191, + "zoom": 1.6165685319845957, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], "primary": { "field": { - "alt": "Ut consequat semper viverra nam libero justo laoreet", + "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, + "background": "#4f4f59", + "x": 3094, + "y": -2976, + "zoom": 1.0855120641844143, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "type": { - "alt": "Ut consequat semper viverra nam libero justo laoreet", + "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#8efd5a", - "x": 466, - "y": -571, - "zoom": 1.394663850868741, + "background": "#4f4f59", + "x": 3094, + "y": -2976, + "zoom": 1.0855120641844143, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, "group": [ { "field": { - "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "alt": "Id aliquet risus feugiat in", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, + "background": "#42f5cf", + "x": 427, + "y": 731, + "zoom": 1.6417661415510265, }, - "id": "f5dbf0d9ed2", + "id": "35eaa8ebee4", "type": { - "alt": "Porttitor massa id neque aliquam vestibulum morbi blandit", + "alt": "Id aliquet risus feugiat in", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#bc3178", - "x": 1156, - "y": -2223, - "zoom": 1.602826201962965, + "background": "#42f5cf", + "x": 427, + "y": 731, + "zoom": 1.6417661415510265, }, - "id": "f5dbf0d9ed2", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=other&query=params", + "id": "35eaa8ebee4", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?some=query", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -1334,79 +1334,79 @@ exports[`patches image fields > otherRepositoryWithTypeThumbnail > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { - "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "alt": "Pellentesque nec nam aliquam sem", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, + "background": "#f72dec", + "x": 2027, + "y": 7, + "zoom": 1.9216652845315787, }, - "id": "4f4a69ff72d", + "id": "7a35bc5aa2f", "type": { - "alt": "Vitae sapien pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis", + "alt": "Pellentesque nec nam aliquam sem", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#c361cc", - "x": -751, - "y": -404, - "zoom": 1.6465649620272602, + "background": "#f72dec", + "x": 2027, + "y": 7, + "zoom": 1.9216652845315787, }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + "id": "7a35bc5aa2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", }, }, ], "primary": { "field": { - "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, + "background": "#60cbac", + "x": -484, + "y": -2038, + "zoom": 1.8400009569805118, }, - "id": "4f4a69ff72d", + "id": "7a35bc5aa2f", "type": { - "alt": "Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius sit", + "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#55e3be", - "x": -762, - "y": 991, - "zoom": 1.0207384987782544, + "background": "#60cbac", + "x": -484, + "y": -2038, + "zoom": 1.8400009569805118, }, - "id": "4f4a69ff72d", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=other&query=params", + "id": "7a35bc5aa2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?some=query", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -1415,36 +1415,36 @@ exports[`patches image fields > otherRepositoryWithTypeThumbnail > slice 1`] = ` exports[`patches image fields > otherRepositoryWithTypeThumbnail > static zone 1`] = ` { "field": { - "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "alt": "Gravida rutrum quisque non tellus orci", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, + "background": "#b1d17b", + "x": 3291, + "y": -730, + "zoom": 1.061566393836766, }, - "id": "9abffab1d17", + "id": "4a95cc61c37", "type": { - "alt": "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed", + "alt": "Gravida rutrum quisque non tellus orci", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#97acc6", - "x": 1922, - "y": -967, - "zoom": 1.9765406541937358, + "background": "#b1d17b", + "x": 3291, + "y": -730, + "zoom": 1.061566393836766, }, - "id": "9abffab1d17", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + "id": "4a95cc61c37", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=query", }, } `; diff --git a/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap index 2624fb3a..a6d213fd 100644 --- a/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap @@ -11,7 +11,7 @@ exports[`patches link to media fields > existing > group 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, }, @@ -23,7 +23,7 @@ exports[`patches link to media fields > existing > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { @@ -33,7 +33,7 @@ exports[`patches link to media fields > existing > shared slice 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, }, @@ -46,7 +46,7 @@ exports[`patches link to media fields > existing > shared slice 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "group": [ @@ -58,16 +58,16 @@ exports[`patches link to media fields > existing > shared slice 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -77,7 +77,7 @@ exports[`patches link to media fields > existing > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { @@ -104,8 +104,8 @@ exports[`patches link to media fields > existing > slice 1`] = ` "width": "1", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -120,7 +120,7 @@ exports[`patches link to media fields > existing > static zone 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", "width": "1", }, } @@ -136,7 +136,7 @@ exports[`patches link to media fields > existingNonImage > group 1`] = ` "link_type": "Media", "name": "foo.pdf", "size": "1", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", }, }, ], @@ -147,7 +147,7 @@ exports[`patches link to media fields > existingNonImage > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { @@ -156,7 +156,7 @@ exports[`patches link to media fields > existingNonImage > shared slice 1`] = ` "link_type": "Media", "name": "foo.pdf", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], @@ -167,7 +167,7 @@ exports[`patches link to media fields > existingNonImage > shared slice 1`] = ` "link_type": "Media", "name": "foo.pdf", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, "group": [ { @@ -177,15 +177,15 @@ exports[`patches link to media fields > existingNonImage > shared slice 1`] = ` "link_type": "Media", "name": "foo.pdf", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -195,7 +195,7 @@ exports[`patches link to media fields > existingNonImage > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { @@ -218,8 +218,8 @@ exports[`patches link to media fields > existingNonImage > slice 1`] = ` "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -233,7 +233,7 @@ exports[`patches link to media fields > existingNonImage > static zone 1`] = ` "link_type": "Media", "name": "foo.pdf", "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, } `; @@ -244,12 +244,12 @@ exports[`patches link to media fields > new > group 1`] = ` { "field": { "height": "1", - "id": "ef95d5daa4d", + "id": "dfdd322ca9d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, }, @@ -261,17 +261,17 @@ exports[`patches link to media fields > new > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { "height": "1", - "id": "f2c3f8bfc90", + "id": "0eddbd0255b", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, }, @@ -279,33 +279,33 @@ exports[`patches link to media fields > new > shared slice 1`] = ` "primary": { "field": { "height": "1", - "id": "f2c3f8bfc90", + "id": "0eddbd0255b", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, "group": [ { "field": { "height": "1", - "id": "f2c3f8bfc90", + "id": "0eddbd0255b", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -315,17 +315,17 @@ exports[`patches link to media fields > new > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { "height": "1", - "id": "45306297c5e", + "id": "2beb55ec2f4", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", "width": "1", }, }, @@ -333,17 +333,17 @@ exports[`patches link to media fields > new > slice 1`] = ` "primary": { "field": { "height": "1", - "id": "45306297c5e", + "id": "2beb55ec2f4", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", "width": "1", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -353,12 +353,12 @@ exports[`patches link to media fields > new > static zone 1`] = ` { "field": { "height": "1", - "id": "c5c95f8d3ac", + "id": "bbad670dad7", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", "width": "1", }, } @@ -370,12 +370,12 @@ exports[`patches link to media fields > otherRepository > group 1`] = ` { "field": { "height": "1", - "id": "fdb33894dcf", + "id": "cfc26fe8d09", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", "width": "1", }, }, @@ -387,17 +387,17 @@ exports[`patches link to media fields > otherRepository > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": { "height": "1", - "id": "ba97bfb16bf", + "id": "61adcf1f5db", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", "width": "1", }, }, @@ -405,33 +405,33 @@ exports[`patches link to media fields > otherRepository > shared slice 1`] = ` "primary": { "field": { "height": "1", - "id": "ba97bfb16bf", + "id": "61adcf1f5db", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", "width": "1", }, "group": [ { "field": { "height": "1", - "id": "ba97bfb16bf", + "id": "61adcf1f5db", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", "width": "1", }, }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -441,17 +441,17 @@ exports[`patches link to media fields > otherRepository > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": { "height": "1", - "id": "a57963dc361", + "id": "4f4a69ff72d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", "width": "1", }, }, @@ -459,17 +459,17 @@ exports[`patches link to media fields > otherRepository > slice 1`] = ` "primary": { "field": { "height": "1", - "id": "a57963dc361", + "id": "4f4a69ff72d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", "width": "1", }, }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -479,12 +479,12 @@ exports[`patches link to media fields > otherRepository > static zone 1`] = ` { "field": { "height": "1", - "id": "97acc6e9abf", + "id": "b1d17b04a95", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", "width": "1", }, } @@ -510,7 +510,7 @@ exports[`patches link to media fields > richTextExisting > group 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, "end": 5, @@ -531,7 +531,7 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ @@ -550,7 +550,7 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "end": 5, @@ -581,7 +581,7 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "end": 5, @@ -611,7 +611,7 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "end": 5, @@ -627,9 +627,9 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -639,7 +639,7 @@ exports[`patches link to media fields > richTextExisting > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ @@ -702,8 +702,8 @@ exports[`patches link to media fields > richTextExisting > slice 1`] = ` }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -727,7 +727,7 @@ exports[`patches link to media fields > richTextExisting > static zone 1`] = ` "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", "width": "1", }, "end": 5, @@ -757,12 +757,12 @@ exports[`patches link to media fields > richTextNew > group 1`] = ` { "data": { "height": "1", - "id": "ef95d5daa4d", + "id": "dfdd322ca9d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=2560&h=1705&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, "end": 5, @@ -783,7 +783,7 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ @@ -797,12 +797,12 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` { "data": { "height": "1", - "id": "f2c3f8bfc90", + "id": "0eddbd0255b", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, "end": 5, @@ -828,12 +828,12 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` { "data": { "height": "1", - "id": "f2c3f8bfc90", + "id": "0eddbd0255b", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, "end": 5, @@ -858,12 +858,12 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` { "data": { "height": "1", - "id": "f2c3f8bfc90", + "id": "0eddbd0255b", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", "width": "1", }, "end": 5, @@ -879,9 +879,9 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -891,7 +891,7 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ @@ -905,12 +905,12 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` { "data": { "height": "1", - "id": "45306297c5e", + "id": "2beb55ec2f4", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", "width": "1", }, "end": 5, @@ -936,12 +936,12 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` { "data": { "height": "1", - "id": "45306297c5e", + "id": "2beb55ec2f4", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", "width": "1", }, "end": 5, @@ -954,8 +954,8 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -974,12 +974,12 @@ exports[`patches link to media fields > richTextNew > static zone 1`] = ` { "data": { "height": "1", - "id": "c5c95f8d3ac", + "id": "bbad670dad7", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", "width": "1", }, "end": 5, @@ -1009,12 +1009,12 @@ exports[`patches link to media fields > richTextOtherRepository > group 1`] = ` { "data": { "height": "1", - "id": "fdb33894dcf", + "id": "cfc26fe8d09", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", "width": "1", }, "end": 5, @@ -1035,7 +1035,7 @@ exports[`patches link to media fields > richTextOtherRepository > shared slice 1 { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ @@ -1049,12 +1049,12 @@ exports[`patches link to media fields > richTextOtherRepository > shared slice 1 { "data": { "height": "1", - "id": "ba97bfb16bf", + "id": "61adcf1f5db", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", "width": "1", }, "end": 5, @@ -1080,12 +1080,12 @@ exports[`patches link to media fields > richTextOtherRepository > shared slice 1 { "data": { "height": "1", - "id": "ba97bfb16bf", + "id": "61adcf1f5db", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", "width": "1", }, "end": 5, @@ -1110,12 +1110,12 @@ exports[`patches link to media fields > richTextOtherRepository > shared slice 1 { "data": { "height": "1", - "id": "ba97bfb16bf", + "id": "61adcf1f5db", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", "width": "1", }, "end": 5, @@ -1131,9 +1131,9 @@ exports[`patches link to media fields > richTextOtherRepository > shared slice 1 ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -1143,7 +1143,7 @@ exports[`patches link to media fields > richTextOtherRepository > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ @@ -1157,12 +1157,12 @@ exports[`patches link to media fields > richTextOtherRepository > slice 1`] = ` { "data": { "height": "1", - "id": "a57963dc361", + "id": "4f4a69ff72d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", "width": "1", }, "end": 5, @@ -1188,12 +1188,12 @@ exports[`patches link to media fields > richTextOtherRepository > slice 1`] = ` { "data": { "height": "1", - "id": "a57963dc361", + "id": "4f4a69ff72d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", "width": "1", }, "end": 5, @@ -1206,8 +1206,8 @@ exports[`patches link to media fields > richTextOtherRepository > slice 1`] = ` }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -1226,12 +1226,12 @@ exports[`patches link to media fields > richTextOtherRepository > static zone 1` { "data": { "height": "1", - "id": "97acc6e9abf", + "id": "b1d17b04a95", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", "width": "1", }, "end": 5, diff --git a/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap index 28421765..6e445ca2 100644 --- a/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap @@ -7,8 +7,8 @@ exports[`patches rich text image nodes > existing > group 1`] = ` "field": [ { "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", + "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", + "type": "heading4", }, { "alt": null, @@ -25,7 +25,7 @@ exports[`patches rich text image nodes > existing > group 1`] = ` }, "id": "id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", }, ], }, @@ -37,14 +37,14 @@ exports[`patches rich text image nodes > existing > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ { "spans": [], - "text": "Eu Mi", - "type": "heading3", + "text": "A molestie lorem ipsum dolor sit amet consectetur adipiscing elit ut", + "type": "list-item", }, { "alt": null, @@ -61,7 +61,7 @@ exports[`patches rich text image nodes > existing > shared slice 1`] = ` }, "id": "id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, ], }, @@ -70,8 +70,8 @@ exports[`patches rich text image nodes > existing > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", + "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", + "type": "preformatted", }, { "alt": null, @@ -88,7 +88,7 @@ exports[`patches rich text image nodes > existing > shared slice 1`] = ` }, "id": "id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, ], "group": [ @@ -96,8 +96,8 @@ exports[`patches rich text image nodes > existing > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Arcu", - "type": "heading1", + "text": "Vel Elit Scelerisque Mauris Pellentesque Pulvinar Pellentesque", + "type": "heading3", }, { "alt": null, @@ -114,16 +114,16 @@ exports[`patches rich text image nodes > existing > shared slice 1`] = ` }, "id": "id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, ], }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -133,14 +133,14 @@ exports[`patches rich text image nodes > existing > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ { "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", + "text": "Diam Ut Venenatis Tellus", + "type": "heading6", }, { "alt": null, @@ -166,8 +166,8 @@ exports[`patches rich text image nodes > existing > slice 1`] = ` "field": [ { "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", + "text": "Tincidunt Vitae Semper Quis Lectus Nulla", + "type": "heading4", }, { "alt": null, @@ -188,8 +188,8 @@ exports[`patches rich text image nodes > existing > slice 1`] = ` }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -200,8 +200,8 @@ exports[`patches rich text image nodes > existing > static zone 1`] = ` "field": [ { "spans": [], - "text": "Elementum Integer", - "type": "heading6", + "text": "Interdum Velit Euismod", + "type": "heading5", }, { "alt": null, @@ -218,7 +218,7 @@ exports[`patches rich text image nodes > existing > static zone 1`] = ` }, "id": "id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, ], } @@ -231,8 +231,8 @@ exports[`patches rich text image nodes > new > group 1`] = ` "field": [ { "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", + "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", + "type": "heading4", }, { "alt": null, @@ -247,9 +247,9 @@ exports[`patches rich text image nodes > new > group 1`] = ` "y": 0, "zoom": 1, }, - "id": "cdfdd322ca9", + "id": "c1d5d24feae", "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", + "url": "https://images.unsplash.com/photo-1504198266287-1659872e6590?w=4272&h=2848&fit=crop", }, ], }, @@ -261,14 +261,14 @@ exports[`patches rich text image nodes > new > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ { "spans": [], - "text": "Eu Mi", - "type": "heading3", + "text": "A molestie lorem ipsum dolor sit amet consectetur adipiscing elit ut", + "type": "list-item", }, { "alt": null, @@ -283,9 +283,9 @@ exports[`patches rich text image nodes > new > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "bc31787853a", + "id": "961adcf1f5d", "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", }, ], }, @@ -294,8 +294,8 @@ exports[`patches rich text image nodes > new > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", + "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", + "type": "preformatted", }, { "alt": null, @@ -310,9 +310,9 @@ exports[`patches rich text image nodes > new > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "bc31787853a", + "id": "961adcf1f5d", "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", }, ], "group": [ @@ -320,8 +320,8 @@ exports[`patches rich text image nodes > new > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Arcu", - "type": "heading1", + "text": "Vel Elit Scelerisque Mauris Pellentesque Pulvinar Pellentesque", + "type": "heading3", }, { "alt": null, @@ -336,18 +336,18 @@ exports[`patches rich text image nodes > new > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "bc31787853a", + "id": "961adcf1f5d", "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", }, ], }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -357,14 +357,14 @@ exports[`patches rich text image nodes > new > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ { "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", + "text": "Diam Ut Venenatis Tellus", + "type": "heading6", }, { "alt": null, @@ -379,9 +379,9 @@ exports[`patches rich text image nodes > new > slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "7963dc361cc", + "id": "61cc54f4a69", "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, @@ -390,8 +390,8 @@ exports[`patches rich text image nodes > new > slice 1`] = ` "field": [ { "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", + "text": "Tincidunt Vitae Semper Quis Lectus Nulla", + "type": "heading4", }, { "alt": null, @@ -406,14 +406,14 @@ exports[`patches rich text image nodes > new > slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "7963dc361cc", + "id": "61cc54f4a69", "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -424,8 +424,8 @@ exports[`patches rich text image nodes > new > static zone 1`] = ` "field": [ { "spans": [], - "text": "Elementum Integer", - "type": "heading6", + "text": "Interdum Velit Euismod", + "type": "heading5", }, { "alt": null, @@ -440,9 +440,9 @@ exports[`patches rich text image nodes > new > static zone 1`] = ` "y": 0, "zoom": 1, }, - "id": "0bbad670dad", + "id": "ed908b1e225", "type": "image", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", }, ], } @@ -455,8 +455,8 @@ exports[`patches rich text image nodes > newLinkTo > group 1`] = ` "field": [ { "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", + "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", + "type": "heading4", }, { "alt": null, @@ -471,7 +471,7 @@ exports[`patches rich text image nodes > newLinkTo > group 1`] = ` "y": 0, "zoom": 1, }, - "id": "cdfdd322ca9", + "id": "c1d5d24feae", "linkTo": { "id": "id-existing", "isBroken": false, @@ -481,7 +481,7 @@ exports[`patches rich text image nodes > newLinkTo > group 1`] = ` "type": "orci", }, "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c?w=6000&h=4000&fit=crop", + "url": "https://images.unsplash.com/photo-1504198266287-1659872e6590?w=4272&h=2848&fit=crop", }, ], }, @@ -493,14 +493,14 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ { "spans": [], - "text": "Eu Mi", - "type": "heading3", + "text": "A molestie lorem ipsum dolor sit amet consectetur adipiscing elit ut", + "type": "list-item", }, { "alt": null, @@ -515,7 +515,7 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "bc31787853a", + "id": "961adcf1f5d", "linkTo": { "id": "id-existing", "isBroken": false, @@ -527,7 +527,7 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "type": "dui", }, "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", }, ], }, @@ -536,8 +536,8 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", + "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", + "type": "preformatted", }, { "alt": null, @@ -552,7 +552,7 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "bc31787853a", + "id": "961adcf1f5d", "linkTo": { "id": "id-existing", "isBroken": false, @@ -564,7 +564,7 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "type": "dui", }, "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", }, ], "group": [ @@ -572,8 +572,8 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Arcu", - "type": "heading1", + "text": "Vel Elit Scelerisque Mauris Pellentesque Pulvinar Pellentesque", + "type": "heading3", }, { "alt": null, @@ -588,7 +588,7 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "bc31787853a", + "id": "961adcf1f5d", "linkTo": { "id": "id-existing", "isBroken": false, @@ -600,16 +600,16 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "type": "dui", }, "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", }, ], }, ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -619,14 +619,14 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ { "spans": [], - "text": "Habitasse platea dictumst quisque sagittis purus sit", - "type": "o-list-item", + "text": "Diam Ut Venenatis Tellus", + "type": "heading6", }, { "alt": null, @@ -641,7 +641,7 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "7963dc361cc", + "id": "61cc54f4a69", "linkTo": { "id": "id-existing", "isBroken": false, @@ -653,7 +653,7 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "type": "amet", }, "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, @@ -662,8 +662,8 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "field": [ { "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", + "text": "Tincidunt Vitae Semper Quis Lectus Nulla", + "type": "heading4", }, { "alt": null, @@ -678,7 +678,7 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "7963dc361cc", + "id": "61cc54f4a69", "linkTo": { "id": "id-existing", "isBroken": false, @@ -690,12 +690,12 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "type": "amet", }, "type": "image", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -706,8 +706,8 @@ exports[`patches rich text image nodes > newLinkTo > static zone 1`] = ` "field": [ { "spans": [], - "text": "Elementum Integer", - "type": "heading6", + "text": "Interdum Velit Euismod", + "type": "heading5", }, { "alt": null, @@ -722,7 +722,7 @@ exports[`patches rich text image nodes > newLinkTo > static zone 1`] = ` "y": 0, "zoom": 1, }, - "id": "0bbad670dad", + "id": "ed908b1e225", "linkTo": { "id": "id-existing", "isBroken": false, @@ -732,7 +732,7 @@ exports[`patches rich text image nodes > newLinkTo > static zone 1`] = ` "type": "phasellus", }, "type": "image", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", }, ], } @@ -745,25 +745,25 @@ exports[`patches rich text image nodes > otherRepository > group 1`] = ` "field": [ { "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", + "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", + "type": "heading4", }, { - "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", + "alt": "Feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#7cfc26", - "x": 2977, - "y": -1163, - "zoom": 1.0472585898934068, + "background": "#5c0990", + "x": 2090, + "y": -1492, + "zoom": 1.854310159304717, }, - "id": "e8d0985c099", + "id": "6442e21ad60", "type": "image", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", }, ], }, @@ -775,29 +775,40 @@ exports[`patches rich text image nodes > otherRepository > shared slice 1`] = ` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ { - "spans": [], - "text": "Facilisis", - "type": "heading3", + "oembed": { + "embed_url": "https://twitter.com/prismicio/status/1354835716430319617", + "height": 113, + "html": " + +", + "thumbnail_height": null, + "thumbnail_url": null, + "thumbnail_width": null, + "type": "rich", + "version": "1.0", + "width": 200, + }, + "type": "embed", }, { - "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", + "alt": "Eros donec ac odio tempor orci dapibus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#37ae3f", - "x": -1505, - "y": 902, - "zoom": 1.8328975606320652, + "background": "#1eeca0", + "x": 1722, + "y": 1820, + "zoom": 1.8326837750693512, }, - "id": "3fc0dfa9fe9", + "id": "3a8ec9378bc", "type": "image", "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", }, @@ -808,23 +819,23 @@ exports[`patches rich text image nodes > otherRepository > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", + "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", + "type": "preformatted", }, { - "alt": "Proin libero nunc consequat interdum varius", + "alt": "Aliquet lectus proin nibh nisl condimentum id venenatis a", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#904f4f", - "x": 1462, - "y": 1324, - "zoom": 1.504938844941775, + "background": "#87853a", + "x": -1429, + "y": -2019, + "zoom": 1.75840565859303, }, - "id": "3fc0dfa9fe9", + "id": "3a8ec9378bc", "type": "image", "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", }, @@ -834,23 +845,23 @@ exports[`patches rich text image nodes > otherRepository > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Egestas Integer Eget Aliquet Nibh", + "text": "Nunc Non Blandit Massa Enim", "type": "heading5", }, { - "alt": "Arcu cursus vitae congue mauris", + "alt": "Id faucibus nisl tincidunt eget nullam non nisi est sit amet", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#5f7a9b", - "x": -119, - "y": -2667, - "zoom": 1.9681315715350518, + "background": "#252b35", + "x": 855, + "y": 1518, + "zoom": 1.5250952426635416, }, - "id": "3fc0dfa9fe9", + "id": "3a8ec9378bc", "type": "image", "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", }, @@ -859,9 +870,9 @@ exports[`patches rich text image nodes > otherRepository > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -871,29 +882,29 @@ exports[`patches rich text image nodes > otherRepository > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ { "spans": [], - "text": "Amet", - "type": "heading5", + "text": "At Ultrices Mi Tempus Imperdiet", + "type": "heading2", }, { - "alt": "Auctor neque vitae tempus quam", + "alt": "Sed id semper risus in hendrerit gravida rutrum", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#5bc5aa", - "x": -1072, - "y": -281, - "zoom": 1.3767766101231744, + "background": "#104dde", + "x": -1621, + "y": -870, + "zoom": 1.7278759511485409, }, - "id": "f70ca27104d", + "id": "2003a644c30", "type": "image", "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", }, @@ -904,30 +915,30 @@ exports[`patches rich text image nodes > otherRepository > slice 1`] = ` "field": [ { "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", + "text": "Tincidunt Vitae Semper Quis Lectus Nulla", + "type": "heading4", }, { - "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", + "alt": "Sed nisi lacus sed viverra tellus in hac habitasse", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#4860cb", - "x": 280, - "y": -379, - "zoom": 1.2389796902982004, + "background": "#63a6c9", + "x": -1609, + "y": -1609, + "zoom": 1.7054690955452316, }, - "id": "f70ca27104d", + "id": "2003a644c30", "type": "image", "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -938,25 +949,25 @@ exports[`patches rich text image nodes > otherRepository > static zone 1`] = ` "field": [ { "spans": [], - "text": "Elementum Integer", - "type": "heading6", + "text": "Interdum Velit Euismod", + "type": "heading5", }, { - "alt": "Interdum velit euismod in pellentesque", + "alt": "Tortor consequat id porta nibh", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#ab1d17", - "x": 3605, - "y": 860, - "zoom": 1.9465488211593005, + "background": "#c61c37", + "x": 349, + "y": -429, + "zoom": 1.4911347686990943, }, - "id": "04a95cc61c3", + "id": "ff1efb2b271", "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", }, ], } @@ -969,23 +980,23 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > group 1`] = ` "field": [ { "spans": [], - "text": "Nulla Aliquet Porttitor Lacus Luctus", - "type": "heading2", + "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", + "type": "heading4", }, { - "alt": "Donec enim diam vulputate ut pharetra sit amet aliquam id diam", + "alt": "Feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#7cfc26", - "x": 2977, - "y": -1163, - "zoom": 1.0472585898934068, + "background": "#5c0990", + "x": 2090, + "y": -1492, + "zoom": 1.854310159304717, }, - "id": "e8d0985c099", + "id": "6442e21ad60", "linkTo": { "id": "id-existing", "isBroken": false, @@ -995,7 +1006,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > group 1`] = ` "type": "orci", }, "type": "image", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", }, ], }, @@ -1007,29 +1018,40 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` { "slices": [ { - "id": "2ff58c741ba", + "id": "b9dd0f2c3f8", "items": [ { "field": [ { - "spans": [], - "text": "Facilisis", - "type": "heading3", + "oembed": { + "embed_url": "https://twitter.com/prismicio/status/1354835716430319617", + "height": 113, + "html": " + +", + "thumbnail_height": null, + "thumbnail_url": null, + "thumbnail_width": null, + "type": "rich", + "version": "1.0", + "width": 200, + }, + "type": "embed", }, { - "alt": "Nullam vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque", + "alt": "Eros donec ac odio tempor orci dapibus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#37ae3f", - "x": -1505, - "y": 902, - "zoom": 1.8328975606320652, + "background": "#1eeca0", + "x": 1722, + "y": 1820, + "zoom": 1.8326837750693512, }, - "id": "3fc0dfa9fe9", + "id": "3a8ec9378bc", "linkTo": { "id": "id-existing", "isBroken": false, @@ -1050,23 +1072,23 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` "field": [ { "spans": [], - "text": "Fringilla Phasellus Faucibus Scelerisque Eleifend Donec Pretium", - "type": "heading3", + "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", + "type": "preformatted", }, { - "alt": "Proin libero nunc consequat interdum varius", + "alt": "Aliquet lectus proin nibh nisl condimentum id venenatis a", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#904f4f", - "x": 1462, - "y": 1324, - "zoom": 1.504938844941775, + "background": "#87853a", + "x": -1429, + "y": -2019, + "zoom": 1.75840565859303, }, - "id": "3fc0dfa9fe9", + "id": "3a8ec9378bc", "linkTo": { "id": "id-existing", "isBroken": false, @@ -1086,23 +1108,23 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` "field": [ { "spans": [], - "text": "Egestas Integer Eget Aliquet Nibh", + "text": "Nunc Non Blandit Massa Enim", "type": "heading5", }, { - "alt": "Arcu cursus vitae congue mauris", + "alt": "Id faucibus nisl tincidunt eget nullam non nisi est sit amet", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#5f7a9b", - "x": -119, - "y": -2667, - "zoom": 1.9681315715350518, + "background": "#252b35", + "x": 855, + "y": 1518, + "zoom": 1.5250952426635416, }, - "id": "3fc0dfa9fe9", + "id": "3a8ec9378bc", "linkTo": { "id": "id-existing", "isBroken": false, @@ -1121,9 +1143,9 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` ], }, "slice_label": null, - "slice_type": "at", - "variation": "tortor", - "version": "a79b9dd", + "slice_type": "dignissim", + "variation": "vitae", + "version": "bfc9053", }, ], } @@ -1133,29 +1155,29 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > slice 1`] = ` { "slices": [ { - "id": "e93cc3e4f28", + "id": "306297c5eda", "items": [ { "field": [ { "spans": [], - "text": "Amet", - "type": "heading5", + "text": "At Ultrices Mi Tempus Imperdiet", + "type": "heading2", }, { - "alt": "Auctor neque vitae tempus quam", + "alt": "Sed id semper risus in hendrerit gravida rutrum", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#5bc5aa", - "x": -1072, - "y": -281, - "zoom": 1.3767766101231744, + "background": "#104dde", + "x": -1621, + "y": -870, + "zoom": 1.7278759511485409, }, - "id": "f70ca27104d", + "id": "2003a644c30", "linkTo": { "id": "id-existing", "isBroken": false, @@ -1176,23 +1198,23 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > slice 1`] = ` "field": [ { "spans": [], - "text": "Ac Orci Phasellus Egestas Tellus", - "type": "heading5", + "text": "Tincidunt Vitae Semper Quis Lectus Nulla", + "type": "heading4", }, { - "alt": "Urna id volutpat lacus laoreet non curabitur gravida arcu ac tortor", + "alt": "Sed nisi lacus sed viverra tellus in hac habitasse", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#4860cb", - "x": 280, - "y": -379, - "zoom": 1.2389796902982004, + "background": "#63a6c9", + "x": -1609, + "y": -1609, + "zoom": 1.7054690955452316, }, - "id": "f70ca27104d", + "id": "2003a644c30", "linkTo": { "id": "id-existing", "isBroken": false, @@ -1208,8 +1230,8 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > slice 1`] = ` }, ], }, - "slice_label": "Pellentesque", - "slice_type": "nulla_posuere", + "slice_label": "Aliquet", + "slice_type": "vel", }, ], } @@ -1220,23 +1242,23 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > static zone 1`] "field": [ { "spans": [], - "text": "Elementum Integer", - "type": "heading6", + "text": "Interdum Velit Euismod", + "type": "heading5", }, { - "alt": "Interdum velit euismod in pellentesque", + "alt": "Tortor consequat id porta nibh", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#ab1d17", - "x": 3605, - "y": 860, - "zoom": 1.9465488211593005, + "background": "#c61c37", + "x": 349, + "y": -429, + "zoom": 1.4911347686990943, }, - "id": "04a95cc61c3", + "id": "ff1efb2b271", "linkTo": { "id": "id-existing", "isBroken": false, @@ -1246,7 +1268,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > static zone 1`] "type": "phasellus", }, "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", }, ], } diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index e9650ea5..3dd88d58 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -22,12 +22,15 @@ type GetDataArgs = { migration: prismic.Migration existingAssets: Asset[] existingDocuments: prismic.PrismicDocument[] - migrationDocuments: prismic.MigrationDocument[] + migrationDocuments: { + other: prismic.PrismicMigrationDocument + otherRepository: prismic.PrismicMigrationDocument + } mockedDomain: string } type InternalTestMigrationFieldPatchingArgs = { - getData: (args: GetDataArgs) => prismic.MigrationDocumentValue["data"] + getData: (args: GetDataArgs) => prismic.ExistingPrismicDocument["data"] expectStrictEqual?: boolean } @@ -51,11 +54,15 @@ const internalTestMigrationFieldPatching = ( }) queryResponse[0].results[0].id = "id-existing" - const otherDocument = { + const { id: _id, ...otherDocument } = { ...ctx.mock.value.document(), uid: ctx.mock.value.keyText(), } - const newDocument = ctx.mock.value.document() + const otherRepositoryDocument = { + ...ctx.mock.value.document(), + uid: ctx.mock.value.keyText(), + } + const { id: __id, ...newDocument } = ctx.mock.value.document() const newID = "id-new" @@ -68,7 +75,11 @@ const internalTestMigrationFieldPatching = ( const { documentsDatabase } = mockPrismicMigrationAPI({ ctx, client, - newDocuments: [{ id: "id-migration" }, { id: newID }], + newDocuments: [ + { id: "id-other" }, + { id: "id-other-repository" }, + { id: newID }, + ], }) const mockedDomain = `https://${client.repositoryName}.example.com` @@ -85,12 +96,21 @@ const internalTestMigrationFieldPatching = ( "other", ) + const migrationOtherRepositoryDocument = + migration.createDocumentFromPrismic( + otherRepositoryDocument, + "other-repository", + ) + newDocument.data = args.getData({ ctx, migration, existingAssets: assetsDatabase.flat(), existingDocuments: queryResponse[0].results, - migrationDocuments: [migrationOtherDocument], + migrationDocuments: { + other: migrationOtherDocument, + otherRepository: migrationOtherRepositoryDocument, + }, mockedDomain, }) @@ -115,7 +135,7 @@ const internalTestMigrationFieldPatching = ( type TestMigrationFieldPatchingFactoryCases = Record< string, - (args: GetDataArgs) => prismic.MigrationDocumentValue["data"][string] + (args: GetDataArgs) => prismic.ExistingPrismicDocument["data"][string] > export const testMigrationFieldPatching = ( diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index 0877330b..7affcaca 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -4,12 +4,12 @@ import type { MockFactory } from "@prismicio/mock" import * as prismic from "../src" import type { MigrationAssetConfig } from "../src/types/migration/Asset" -import { MigrationDocument } from "../src/types/migration/Document" +import { PrismicMigrationDocument } from "../src/types/migration/Document" it("creates a document", () => { const migration = prismic.createMigration() - const document: prismic.MigrationDocumentValue = { + const document: prismic.PendingPrismicDocument = { type: "type", uid: "uid", lang: "lang", @@ -20,7 +20,7 @@ it("creates a document", () => { migration.createDocument(document, documentTitle) expect(migration._documents[0]).toStrictEqual( - new MigrationDocument(document, { documentTitle }), + new PrismicMigrationDocument(document, documentTitle, { dependencies: [] }), ) }) @@ -33,7 +33,7 @@ it("creates a document from an existing Prismic document", (ctx) => { migration.createDocument(document, documentTitle) expect(migration._documents[0]).toStrictEqual( - new MigrationDocument(document, { documentTitle }), + new PrismicMigrationDocument(document, documentTitle, { dependencies: [] }), ) }) @@ -184,7 +184,7 @@ describe.each<{ const { id, field, expected } = getField(mock) - const document: prismic.MigrationDocumentValue = { + const document: prismic.PendingPrismicDocument = { type: "type", uid: "uid", lang: "lang", @@ -204,7 +204,7 @@ describe.each<{ const { id, field, expected } = getField(mock) - const document: prismic.MigrationDocumentValue = { + const document: prismic.PendingPrismicDocument = { type: "type", uid: "uid", lang: "lang", @@ -234,7 +234,7 @@ describe.each<{ }, ] - const document: prismic.MigrationDocumentValue = { + const document: prismic.PendingPrismicDocument = { type: "type", uid: "uid", lang: "lang", @@ -264,7 +264,7 @@ describe.each<{ }, ] - const document: prismic.MigrationDocumentValue = { + const document: prismic.PendingPrismicDocument = { type: "type", uid: "uid", lang: "lang", diff --git a/test/migration-updateDocument.test.ts b/test/migration-updateDocument.test.ts index d48aaf58..ea93fcdb 100644 --- a/test/migration-updateDocument.test.ts +++ b/test/migration-updateDocument.test.ts @@ -1,7 +1,7 @@ import { expect, it } from "vitest" import * as prismic from "../src" -import { MigrationDocument } from "../src/types/migration/Document" +import { PrismicMigrationDocument } from "../src/types/migration/Document" it("updates a document", (ctx) => { const migration = prismic.createMigration() @@ -11,7 +11,7 @@ it("updates a document", (ctx) => { migration.updateDocument(document, documentTitle) - const expectedDocument = new MigrationDocument(document, { documentTitle }) - expectedDocument._mode = "update" - expect(migration._documents[0]).toStrictEqual(expectedDocument) + expect(migration._documents[0]).toStrictEqual( + new PrismicMigrationDocument(document, documentTitle, { dependencies: [] }), + ) }) diff --git a/test/types/migration-document.types.ts b/test/types/migration-document.types.ts index e43626a3..afe5ccea 100644 --- a/test/types/migration-document.types.ts +++ b/test/types/migration-document.types.ts @@ -3,7 +3,7 @@ import { expectNever, expectType } from "ts-expect" import type * as prismic from "../../src" -;(value: prismic.MigrationDocumentValue): true => { +;(value: prismic.PendingPrismicDocument): true => { switch (typeof value) { case "object": { if (value === null) { @@ -19,7 +19,7 @@ import type * as prismic from "../../src" } } -expectType({ +expectType({ uid: "", type: "", lang: "", @@ -29,7 +29,7 @@ expectType({ /** * Supports any field when generic. */ -expectType({ +expectType({ uid: "", type: "", lang: "", @@ -39,12 +39,12 @@ expectType({ }) /** - * `PrismicDocument` is assignable to `MigrationDocumentValue` with added + * `PrismicDocument` is assignable to `PendingPrismicDocument` with added * `title`. */ expectType< TypeOf< - prismic.MigrationDocumentValue, + prismic.PendingPrismicDocument, prismic.PrismicDocument & { title: string } > >(true) @@ -55,7 +55,7 @@ type BarDocument = prismic.PrismicDocument<{ bar: prismic.KeyTextField }, "bar"> type BazDocument = prismic.PrismicDocument, "baz"> type Documents = FooDocument | BarDocument | BazDocument -type MigrationDocuments = prismic.MigrationDocumentValue +type MigrationDocuments = prismic.PendingPrismicDocument /** * Infers data type from document type. @@ -128,7 +128,7 @@ type SliceDocument = prismic.PrismicDocument< > type AdvancedDocuments = StaticDocument | GroupDocument | SliceDocument type MigrationAdvancedDocuments = - prismic.MigrationDocumentValue + prismic.PendingPrismicDocument // Static expectType({ diff --git a/test/types/migration.types.ts b/test/types/migration.types.ts index f3c3bd2d..ca79adab 100644 --- a/test/types/migration.types.ts +++ b/test/types/migration.types.ts @@ -7,9 +7,18 @@ import * as prismic from "../../src" const defaultMigration = prismic.createMigration() // Migration Documents -type FooDocument = prismic.PrismicDocument, "foo"> -type BarDocument = prismic.PrismicDocument, "bar"> -type BazDocument = prismic.PrismicDocument, "baz"> +type FooDocument = prismic.PrismicDocumentWithUID< + { foo: prismic.KeyTextField }, + "foo" +> +type BarDocument = prismic.PrismicDocumentWithUID< + { bar: prismic.KeyTextField }, + "bar" +> +type BazDocument = prismic.PrismicDocumentWithoutUID< + { baz: prismic.KeyTextField }, + "baz" +> type Documents = FooDocument | BarDocument | BazDocument const documentsMigration = prismic.createMigration() @@ -123,7 +132,7 @@ expectType< >(true) /** - * createDocument - basic + * createDocument */ // Default @@ -136,9 +145,9 @@ const defaultCreateDocument = defaultMigration.createDocument( }, "", ) -expectType>( - true, -) +expectType< + TypeEqual +>(true) // Documents const documentsCreateDocument = documentsMigration.createDocument( @@ -146,13 +155,404 @@ const documentsCreateDocument = documentsMigration.createDocument( type: "foo", uid: "", lang: "", - data: {}, + data: { + foo: "", + }, }, "", ) expectType< TypeEqual< typeof documentsCreateDocument, - prismic.MigrationDocument + prismic.PrismicMigrationDocument + > +>(true) + +documentsMigration.createDocument( + { + type: "baz", + lang: "", + data: { + baz: "", + }, + }, + "", +) + +documentsMigration.createDocument( + { + type: "baz", + // @ts-expect-error - Type 'string' is not assignable to type 'null | undefined'. + uid: "", + lang: "", + data: { + baz: "", + }, + }, + "", +) + +documentsMigration.createDocument( + { + type: "foo", + uid: "", + lang: "", + // @ts-expect-error - Property 'foo' is missing + data: {}, + }, + "", +) + +documentsMigration.createDocument( + { + type: "foo", + uid: "", + lang: "", + data: { + // @ts-expect-error - Type 'number' is not assignable to type 'KeyTextField' + foo: 1, + }, + }, + "", +) + +documentsMigration.createDocument( + { + type: "foo", + uid: "", + lang: "", + data: { + foo: "", + // @ts-expect-error - Object literal may only specify known properties + bar: "", + }, + }, + "", +) +/** + * updateDocument + */ + +// Default +const defaultUpdateDocument = defaultMigration.updateDocument( + { + id: "", + type: "", + lang: "", + data: {}, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) +expectType< + TypeEqual +>(true) + +// Documents +const documentsUpdateDocument = documentsMigration.updateDocument( + { + id: "", + type: "foo", + uid: "", + lang: "", + data: { + foo: "", + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) +expectType< + TypeEqual< + typeof documentsUpdateDocument, + prismic.PrismicMigrationDocument > >(true) + +documentsMigration.updateDocument( + { + id: "", + type: "baz", + lang: "", + data: { + baz: "", + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +documentsMigration.updateDocument( + { + id: "", + type: "baz", + // @ts-expect-error - Type 'string' is not assignable to type 'null | undefined'. + uid: "", + lang: "", + data: { + baz: "", + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +documentsMigration.updateDocument( + { + id: "", + type: "foo", + uid: "", + lang: "", + // @ts-expect-error - Property 'foo' is missing + data: {}, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +documentsMigration.updateDocument( + { + id: "", + type: "foo", + uid: "", + lang: "", + data: { + // @ts-expect-error - Type 'number' is not assignable to type 'KeyTextField' + foo: 1, + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +documentsMigration.updateDocument( + { + id: "", + type: "foo", + uid: "", + lang: "", + data: { + foo: "", + // @ts-expect-error - Object literal may only specify known properties + bar: "", + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +/** + * createDocumentFromPrismic + */ + +// Default +const defaultCreateFromPrismicDocument = + defaultMigration.createDocumentFromPrismic( + { + id: "", + type: "", + uid: "", + lang: "", + data: {}, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", + ) +expectType< + TypeEqual< + typeof defaultCreateFromPrismicDocument, + prismic.PrismicMigrationDocument + > +>(true) + +// Documents +const documentsCreateFromPrismicDocument = + documentsMigration.createDocumentFromPrismic( + { + id: "", + type: "foo", + uid: "", + lang: "", + data: { + foo: "", + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", + ) +expectType< + TypeEqual< + typeof documentsCreateFromPrismicDocument, + prismic.PrismicMigrationDocument + > +>(true) + +documentsMigration.createDocumentFromPrismic( + { + id: "", + type: "baz", + lang: "", + data: { + baz: "", + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +documentsMigration.createDocumentFromPrismic( + { + id: "", + type: "baz", + // @ts-expect-error - Type 'string' is not assignable to type 'null | undefined'. + uid: "", + lang: "", + data: { + baz: "", + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +documentsMigration.createDocumentFromPrismic( + { + id: "", + type: "foo", + uid: "", + lang: "", + // @ts-expect-error - Property 'foo' is missing + data: {}, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +documentsMigration.createDocumentFromPrismic( + { + id: "", + type: "foo", + uid: "", + lang: "", + data: { + // @ts-expect-error - Type 'number' is not assignable to type 'KeyTextField' + foo: 1, + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) + +documentsMigration.createDocumentFromPrismic( + { + id: "", + type: "foo", + uid: "", + lang: "", + data: { + foo: "", + // @ts-expect-error - Object literal may only specify known properties + bar: "", + }, + tags: [], + href: "", + url: "", + last_publication_date: `0-0-0T0:0:0+0`, + first_publication_date: `0-0-0T0:0:0+0`, + slugs: [], + alternate_languages: [], + linked_documents: [], + }, + "", +) diff --git a/test/writeClient-migrate-documents.test.ts b/test/writeClient-migrate-documents.test.ts index a16c6028..73e25803 100644 --- a/test/writeClient-migrate-documents.test.ts +++ b/test/writeClient-migrate-documents.test.ts @@ -77,7 +77,7 @@ it.concurrent("discovers existing documents", async (ctx) => { }) }) -it.concurrent("skips creating existing documents (update)", async (ctx) => { +it.concurrent("skips creating existing documents", async (ctx) => { const client = createTestWriteClient({ ctx }) const queryResponse = createPagedQueryResponses({ @@ -102,48 +102,7 @@ it.concurrent("skips creating existing documents (update)", async (ctx) => { expect(reporter).toHaveBeenCalledWith({ type: "documents:skipping", data: { - reason: "already exists", - current: 1, - remaining: 0, - total: 1, - document: migrationDocument, - }, - }) - expect(reporter).toHaveBeenCalledWith({ - type: "documents:created", - data: { - created: 0, - documents: expect.anything(), - }, - }) -}) - -it.concurrent("skips creating existing documents (auto)", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const queryResponse = createPagedQueryResponses({ - ctx, - pages: 1, - pageSize: 1, - }) - const document = queryResponse[0].results[0] - - mockPrismicRestAPIV2({ ctx, queryResponse }) - mockPrismicAssetAPI({ ctx, client }) - mockPrismicMigrationAPI({ ctx, client, existingDocuments: [document] }) - - const migration = prismic.createMigration() - - const migrationDocument = migration.createDocument(document, "foo") - - const reporter = vi.fn() - - await client.migrate(migration, { reporter }) - - expect(reporter).toHaveBeenCalledWith({ - type: "documents:skipping", - data: { - reason: "already exists", + reason: "exists", current: 1, remaining: 0, total: 1, @@ -162,7 +121,7 @@ it.concurrent("skips creating existing documents (auto)", async (ctx) => { it.concurrent("creates new documents", async (ctx) => { const client = createTestWriteClient({ ctx }) - const document = ctx.mock.value.document() + const { id: _id, ...document } = ctx.mock.value.document() const newDocument = { id: "foo" } mockPrismicRestAPIV2({ ctx }) @@ -212,7 +171,7 @@ it.concurrent( }) const masterLanguageDocument = queryResponse[0].results[0] - const document = ctx.mock.value.document() + const { id: _id, ...document } = ctx.mock.value.document() const newDocument = { id: "foo", masterLanguageDocumentID: masterLanguageDocument.id, @@ -260,22 +219,19 @@ it.concurrent( async (ctx) => { const client = createTestWriteClient({ ctx }) - const masterLanguageDocument = - ctx.mock.value.document() as prismic.MigrationDocumentValue - const document = ctx.mock.value.document() as prismic.MigrationDocumentValue + const { id: masterLanguageDocumentID, ...masterLanguageDocument } = + ctx.mock.value.document() + const { id: documentID, ...document } = ctx.mock.value.document() const newDocuments = [ { - id: masterLanguageDocument.id!, + id: masterLanguageDocumentID, }, { - id: document.id!, - masterLanguageDocumentID: masterLanguageDocument.id!, + id: documentID, + masterLanguageDocumentID: masterLanguageDocumentID, }, ] - delete masterLanguageDocument.id - delete document.id - mockPrismicRestAPIV2({ ctx }) mockPrismicAssetAPI({ ctx, client }) mockPrismicMigrationAPI({ ctx, client, newDocuments: [...newDocuments] }) @@ -323,7 +279,7 @@ it.concurrent( }) const masterLanguageDocument = queryResponse[0].results[0] - const document = ctx.mock.value.document() + const { id: _id, ...document } = ctx.mock.value.document() const newDocument = { id: "foo", masterLanguageDocumentID: masterLanguageDocument.id, @@ -371,22 +327,19 @@ it.concurrent( async (ctx) => { const client = createTestWriteClient({ ctx }) - const masterLanguageDocument = - ctx.mock.value.document() as prismic.MigrationDocumentValue - const document = ctx.mock.value.document() as prismic.MigrationDocumentValue + const { id: masterLanguageDocumentID, ...masterLanguageDocument } = + ctx.mock.value.document() + const { id: documentID, ...document } = ctx.mock.value.document() const newDocuments = [ { - id: masterLanguageDocument.id!, + id: masterLanguageDocumentID, }, { - id: document.id!, - masterLanguageDocumentID: masterLanguageDocument.id!, + id: documentID, + masterLanguageDocumentID: masterLanguageDocumentID, }, ] - delete masterLanguageDocument.id - delete document.id - mockPrismicRestAPIV2({ ctx }) mockPrismicAssetAPI({ ctx, client }) mockPrismicMigrationAPI({ ctx, client, newDocuments: [...newDocuments] }) @@ -450,7 +403,10 @@ it.concurrent( const migration = prismic.createMigration() - const migrationDocument = migration.createDocument(document, "foo") + const migrationDocument = migration.createDocumentFromPrismic( + document, + "foo", + ) let documents: DocumentMap | undefined const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( @@ -482,15 +438,16 @@ it.concurrent("creates master locale documents first", async (ctx) => { const { repository, masterLocale } = createRepository(ctx) - const masterLanguageDocument = ctx.mock.value.document() + const { id: masterLanguageDocumentID, ...masterLanguageDocument } = + ctx.mock.value.document() masterLanguageDocument.lang = masterLocale - const document = ctx.mock.value.document() + const { id: documentID, ...document } = ctx.mock.value.document() const newDocuments = [ { - id: masterLanguageDocument.id, + id: masterLanguageDocumentID, }, { - id: document.id, + id: documentID, }, ] diff --git a/test/writeClient-migrate-patch-contentRelationship.test.ts b/test/writeClient-migrate-patch-contentRelationship.test.ts index 98713761..0bfc830d 100644 --- a/test/writeClient-migrate-patch-contentRelationship.test.ts +++ b/test/writeClient-migrate-patch-contentRelationship.test.ts @@ -6,37 +6,38 @@ testMigrationFieldPatching("patches link fields", { existing: ({ migration, existingDocuments }) => migration.createContentRelationship(existingDocuments[0]), migration: ({ migration, migrationDocuments }) => { - delete migrationDocuments[0].value.id + delete migrationDocuments.other.document.id - return migration.createContentRelationship(migrationDocuments[0]) + return migration.createContentRelationship(migrationDocuments.other) }, lazyExisting: ({ migration, existingDocuments }) => { return migration.createContentRelationship(() => existingDocuments[0]) }, lazyMigration: ({ migration, migrationDocuments }) => { - delete migrationDocuments[0].value.id + delete migrationDocuments.other.document.id return migration.createContentRelationship(() => migration.getByUID( - migrationDocuments[0].value.type, - migrationDocuments[0].value.uid!, + migrationDocuments.other.document.type, + migrationDocuments.other.document.uid!, ), ) }, migrationNoTags: ({ migration, migrationDocuments }) => { - migrationDocuments[0].value.tags = undefined + migrationDocuments.other.document.tags = undefined return migration.createContentRelationship(() => migration.getByUID( - migrationDocuments[0].value.type, - migrationDocuments[0].value.uid!, + migrationDocuments.other.document.type, + migrationDocuments.other.document.uid!, ), ) }, otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { const contentRelationship = ctx.mock.value.link({ type: "Document" }) // `migrationDocuments` contains documents from "another repository" - contentRelationship.id = migrationDocuments[0].value.id! + contentRelationship.id = + migrationDocuments.otherRepository.originalPrismicDocument!.id return contentRelationship }, diff --git a/test/writeClient-migrate-patch-simpleField.test.ts b/test/writeClient-migrate-patch-simpleField.test.ts index d21619f3..0e6268cc 100644 --- a/test/writeClient-migrate-patch-simpleField.test.ts +++ b/test/writeClient-migrate-patch-simpleField.test.ts @@ -1,5 +1,7 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" +import { RichTextNodeType } from "../src" + testMigrationFieldPatching( "does not patch simple fields", { @@ -10,7 +12,9 @@ testMigrationFieldPatching( number: ({ ctx }) => ctx.mock.value.number({ state: "filled" }), keyText: ({ ctx }) => ctx.mock.value.keyText({ state: "filled" }), richTextSimple: ({ ctx }) => - ctx.mock.value.richText({ state: "filled", pattern: "long" }), + ctx.mock.value + .richText({ state: "filled", pattern: "long" }) + .filter((node) => node.type !== RichTextNodeType.image), select: ({ ctx }) => ctx.mock.value.select({ model: ctx.mock.model.select({ options: ["foo", "bar"] }), diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index 4d70c967..d9e024f9 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -37,13 +37,14 @@ it.concurrent("performs migration", async (ctx) => { const migration = prismic.createMigration() - const documentFoo: prismic.MigrationDocumentValue = ctx.mock.value.document() + const { id: _id, ...documentFoo } = + ctx.mock.value.document() as prismic.ExistingPrismicDocument documentFoo.data = { image: migration.createAsset("foo", "foo.png"), link: () => migration.getByUID("bar", "bar"), } - const documentBar = ctx.mock.value.document() + const { id: __id, ...documentBar } = ctx.mock.value.document() documentBar.type = "bar" documentBar.uid = "bar" @@ -66,8 +67,7 @@ it.concurrent("performs migration", async (ctx) => { expect(assets?.size).toBe(1) expect(assetsDatabase.flat()).toHaveLength(1) - // Documents are indexed twice, on ID, and on reference - expect(documents?.size).toBe(4) + expect(documents?.size).toBe(2) expect(Object.keys(documentsDatabase)).toHaveLength(2) expect(reporter).toHaveBeenCalledWith({ diff --git a/test/writeClient-updateDocument.test.ts b/test/writeClient-updateDocument.test.ts index 4ac07452..6e6ee357 100644 --- a/test/writeClient-updateDocument.test.ts +++ b/test/writeClient-updateDocument.test.ts @@ -107,6 +107,7 @@ it.skip("is abortable with an AbortController", async (ctx) => { client.updateDocument( document.id, { + ...document, uid: "uid", data: {}, }, @@ -135,6 +136,7 @@ it.concurrent("supports custom headers", async (ctx) => { client.updateDocument( document.id, { + ...document, uid: "uid", data: {}, }, From ed42798b91062d9998669d00a5eba00914503ce9 Mon Sep 17 00:00:00 2001 From: lihbr Date: Sun, 15 Sep 2024 21:33:46 +0200 Subject: [PATCH 51/61] refactor: remove documents and assets maps --- src/Migration.ts | 81 ++++--- src/WriteClient.ts | 210 ++++++------------ src/types/migration/Asset.ts | 46 ++-- src/types/migration/ContentRelationship.ts | 14 +- src/types/migration/Document.ts | 27 +-- src/types/migration/Field.ts | 31 +-- ...ate-patch-contentRelationship.test.ts.snap | 48 +++- test/migration-createAsset.test.ts | 18 +- test/migration-createDocument.test.ts | 8 +- test/migration-getByUID.test.ts | 6 +- test/migration-getSingle.test.ts | 4 +- test/writeClient-migrate-assets.test.ts | 59 ++--- test/writeClient-migrate-documents.test.ts | 149 +++---------- ...-migrate-patch-contentRelationship.test.ts | 4 +- test/writeClient-migrate.test.ts | 22 +- 15 files changed, 269 insertions(+), 458 deletions(-) diff --git a/src/Migration.ts b/src/Migration.ts index 303ca967..e7bba6e0 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -3,6 +3,7 @@ import { validateAssetMetadata } from "./lib/validateAssetMetadata" import type { Asset } from "./types/api/asset/asset" import type { MigrationAssetConfig } from "./types/migration/Asset" +import type { MigrationAsset } from "./types/migration/Asset" import { MigrationImage } from "./types/migration/Asset" import type { UnresolvedMigrationContentRelationshipConfig } from "./types/migration/ContentRelationship" import { MigrationContentRelationship } from "./types/migration/ContentRelationship" @@ -43,7 +44,7 @@ export class Migration { * * @internal */ - _assets: Map = new Map() + _assets: Map = new Map() /** * Documents registered in the migration. @@ -143,7 +144,7 @@ export class Migration { tags?: string[] } = {}, ): MigrationImage { - let asset: MigrationAssetConfig + let config: MigrationAssetConfig let maybeInitialField: FilledImageFieldImage | undefined if (typeof fileOrAssetOrField === "object" && "url" in fileOrAssetOrField) { if ( @@ -168,7 +169,7 @@ export class Migration { maybeInitialField = fileOrAssetOrField } - asset = { + config = { id: fileOrAssetOrField.id, file: url, filename, @@ -178,7 +179,7 @@ export class Migration { tags: undefined, } } else { - asset = { + config = { id: fileOrAssetOrField.id, file: fileOrAssetOrField.url, filename: fileOrAssetOrField.filename, @@ -189,7 +190,7 @@ export class Migration { } } } else { - asset = { + config = { id: fileOrAssetOrField, file: fileOrAssetOrField, filename: filename!, @@ -200,26 +201,24 @@ export class Migration { } } - validateAssetMetadata(asset) - - const maybeAsset = this._assets.get(asset.id) + validateAssetMetadata(config) + const migrationAsset = new MigrationImage(config, maybeInitialField) + const maybeAsset = this._assets.get(config.id) if (maybeAsset) { // Consolidate existing asset with new asset value if possible - this._assets.set(asset.id, { - ...maybeAsset, - notes: asset.notes || maybeAsset.notes, - credits: asset.credits || maybeAsset.credits, - alt: asset.alt || maybeAsset.alt, - tags: Array.from( - new Set([...(maybeAsset.tags || []), ...(asset.tags || [])]), - ), - }) + maybeAsset.config.notes = config.notes || maybeAsset.config.notes + maybeAsset.config.credits = config.credits || maybeAsset.config.credits + maybeAsset.config.alt = config.alt || maybeAsset.config.alt + maybeAsset.config.tags = Array.from( + new Set([...(config.tags || []), ...(maybeAsset.config.tags || [])]), + ) } else { - this._assets.set(asset.id, asset) + this._assets.set(config.id, migrationAsset) } - return new MigrationImage(this._assets.get(asset.id)!, maybeInitialField) + // We returned a detached instance of the asset to serialize it properly + return migrationAsset } /** @@ -251,13 +250,13 @@ export class Migration { this.createAsset.bind(this), ) - const migrationDocument = new PrismicMigrationDocument< + const doc = new PrismicMigrationDocument< ExtractDocumentType >({ ...document, data }, title, { ...options, dependencies }) - this._documents.push(migrationDocument) + this._documents.push(doc) - return migrationDocument + return doc } /** @@ -285,13 +284,13 @@ export class Migration { this.createAsset.bind(this), ) - const migrationDocument = new PrismicMigrationDocument< + const doc = new PrismicMigrationDocument< ExtractDocumentType >({ ...document, data }, title, { dependencies }) - this._documents.push(migrationDocument) + this._documents.push(doc) - return migrationDocument + return doc } /** @@ -318,7 +317,7 @@ export class Migration { this.createAsset.bind(this), ) - const migrationDocument = new PrismicMigrationDocument( + const doc = new PrismicMigrationDocument( { type: document.type, lang: document.lang, @@ -332,9 +331,9 @@ export class Migration { { originalPrismicDocument: document, dependencies }, ) - this._documents.push(migrationDocument) + this._documents.push(doc) - return migrationDocument + return doc } /** @@ -441,17 +440,17 @@ export class Migration { * @returns The migration document instance for the original ID, if a matching * document is found. */ - // getByOriginalID( - // id: string, - // ): - // | PrismicMigrationDocument> - // | undefined { - // return this._documents.find( - // ( - // doc, - // ): doc is PrismicMigrationDocument< - // ExtractDocumentType - // > => doc.originalPrismicDocument?.id === id, - // ) - // } + getByOriginalID( + id: string, + ): + | PrismicMigrationDocument> + | undefined { + return this._documents.find( + ( + doc, + ): doc is PrismicMigrationDocument< + ExtractDocumentType + > => doc.originalPrismicDocument?.id === id, + ) + } } diff --git a/src/WriteClient.ts b/src/WriteClient.ts index c46c6fd0..0759ab06 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -22,9 +22,8 @@ import { type PostDocumentResult, type PutDocumentParams, } from "./types/api/migration/document" -import type { AssetMap, MigrationAssetConfig } from "./types/migration/Asset" +import type { MigrationAsset } from "./types/migration/Asset" import type { - DocumentMap, MigrationDocument, PendingPrismicDocument, PrismicMigrationDocument, @@ -93,31 +92,20 @@ type MigrateReporterEventMap = { current: number remaining: number total: number - asset: MigrationAssetConfig + asset: MigrationAsset } "assets:creating": { current: number remaining: number total: number - asset: MigrationAssetConfig + asset: MigrationAsset } "assets:created": { created: number - assets: AssetMap } "documents:masterLocale": { masterLocale: string } - "documents:existing": { - existing: number - } - "documents:skipping": { - reason: string - current: number - remaining: number - total: number - document: PrismicMigrationDocument - } "documents:creating": { current: number remaining: number @@ -126,7 +114,6 @@ type MigrateReporterEventMap = { } "documents:created": { created: number - documents: DocumentMap } "documents:updating": { current: number @@ -338,11 +325,9 @@ export class WriteClient< }, }) - const assets = await this.migrateCreateAssets(migration, params) - - const documents = await this.migrateCreateDocuments(migration, params) - - await this.migrateUpdateDocuments(migration, assets, documents, params) + await this.migrateCreateAssets(migration, params) + await this.migrateCreateDocuments(migration, params) + await this.migrateUpdateDocuments(migration, params) params.reporter?.({ type: "end", @@ -361,8 +346,6 @@ export class WriteClient< * @param migration - A migration prepared with {@link createMigration}. * @param params - An event listener and additional fetch parameters. * - * @returns A map of assets available in the Prismic repository. - * * @internal This method is one of the step performed by the {@link migrate} method. */ private async migrateCreateAssets( @@ -371,8 +354,8 @@ export class WriteClient< reporter, ...fetchParams }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, - ): Promise { - const assets: AssetMap = new Map() + ): Promise { + const existingAssets = new Map() // Get all existing assets let getAssetsResult: GetAssetsReturnType | undefined = undefined @@ -387,14 +370,14 @@ export class WriteClient< } for (const asset of getAssetsResult.results) { - assets.set(asset.id, asset) + existingAssets.set(asset.id, asset) } } while (getAssetsResult?.next) reporter?.({ type: "assets:existing", data: { - existing: assets.size, + existing: existingAssets.size, }, }) @@ -402,10 +385,10 @@ export class WriteClient< let i = 0 let created = 0 for (const [_, migrationAsset] of migration._assets) { - if ( - typeof migrationAsset.id === "string" && - assets.has(migrationAsset.id) - ) { + if (existingAssets.has(migrationAsset.config.id)) { + migrationAsset.asset = existingAssets.get(migrationAsset.config.id) + + // Is this essential for deduplication? reporter?.({ type: "assets:skipping", data: { @@ -428,7 +411,8 @@ export class WriteClient< }, }) - const { id, file, filename, ...params } = migrationAsset + const { file, filename, notes, credits, alt, tags } = + migrationAsset.config let resolvedFile: PostAssetParams["file"] | File if (typeof file === "string") { @@ -436,18 +420,21 @@ export class WriteClient< try { url = new URL(file) } catch (error) { - // noop + // noop only on invalid URL, fetch errors will throw in the next if statement } if (url) { + // File is a URL, fetch it resolvedFile = await this.fetchForeignAsset( url.toString(), fetchParams, ) } else { + // File is actual file content, use it as-is resolvedFile = file } } else if (file instanceof URL) { + // File is a URL instance, fetch it resolvedFile = await this.fetchForeignAsset( file.toString(), fetchParams, @@ -457,11 +444,11 @@ export class WriteClient< } const asset = await this.createAsset(resolvedFile, filename, { - ...params, + ...{ notes, credits, alt, tags }, ...fetchParams, }) - assets.set(id, asset) + migrationAsset.asset = asset } } @@ -469,11 +456,8 @@ export class WriteClient< type: "assets:created", data: { created, - assets, }, }) - - return assets } /** @@ -482,8 +466,6 @@ export class WriteClient< * @param migration - A migration prepared with {@link createMigration}. * @param params - An event listener and additional fetch parameters. * - * @returns A map of documents available in the Prismic repository. - * * @internal This method is one of the step performed by the {@link migrate} method. */ private async migrateCreateDocuments( @@ -492,7 +474,7 @@ export class WriteClient< reporter, ...fetchParams }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, - ): Promise> { + ): Promise { // Resolve master locale const repository = await this.getRepository(fetchParams) const masterLocale = repository.languages.find((lang) => lang.is_master)!.id @@ -503,109 +485,62 @@ export class WriteClient< }, }) - // Get all existing documents - const existingDocuments = await this.dangerouslyGetAll(fetchParams) - reporter?.({ - type: "documents:existing", - data: { - existing: existingDocuments.length, - }, - }) - - const documents: DocumentMap = new Map() - for (const document of existingDocuments) { - // Index on document ID - documents.set(document.id, document) - } - - const sortedMigrationDocuments: PrismicMigrationDocument[] = [] - + const documentsToCreate: PrismicMigrationDocument[] = [] // We create an array with non-master locale documents last because // we need their master locale document to be created first. - for (const migrationDocument of migration._documents) { - if (migrationDocument.document.lang === masterLocale) { - sortedMigrationDocuments.unshift(migrationDocument) - } else { - sortedMigrationDocuments.push(migrationDocument) + for (const doc of migration._documents) { + if (!doc.document.id) { + if (doc.document.lang === masterLocale) { + documentsToCreate.unshift(doc) + } else { + documentsToCreate.push(doc) + } } } - let i = 0 let created = 0 - for (const migrationDocument of sortedMigrationDocuments) { - if (migrationDocument.document.id) { - reporter?.({ - type: "documents:skipping", - data: { - reason: "exists", - current: ++i, - remaining: sortedMigrationDocuments.length - i, - total: sortedMigrationDocuments.length, - document: migrationDocument, - }, - }) + for (const doc of documentsToCreate) { + reporter?.({ + type: "documents:creating", + data: { + current: ++created, + remaining: documentsToCreate.length - created, + total: documentsToCreate.length, + document: doc, + }, + }) - // Index the migration document - documents.set( - migrationDocument, - documents.get(migrationDocument.document.id)!, - ) - } else { - created++ - reporter?.({ - type: "documents:creating", - data: { - current: ++i, - remaining: sortedMigrationDocuments.length - i, - total: sortedMigrationDocuments.length, - document: migrationDocument, - }, - }) + // Resolve master language document ID for non-master locale documents + let masterLanguageDocumentID: string | undefined + if (doc.masterLanguageDocument) { + const link = doc.masterLanguageDocument + + await link._resolve(migration) + masterLanguageDocumentID = link._field?.id + } else if (doc.originalPrismicDocument) { + masterLanguageDocumentID = + doc.originalPrismicDocument.alternate_languages.find( + ({ lang }) => lang === masterLocale, + )?.id + } - // Resolve master language document ID for non-master locale documents - let masterLanguageDocumentID: string | undefined - if (migrationDocument.masterLanguageDocument) { - const link = migrationDocument.masterLanguageDocument - - await link._resolve({ documents, assets: new Map() }) - masterLanguageDocumentID = link._field?.id - } else if (migrationDocument.originalPrismicDocument) { - masterLanguageDocumentID = - migrationDocument.originalPrismicDocument.alternate_languages.find( - ({ lang }) => lang === masterLocale, - )?.id - } + const { id } = await this.createDocument( + // We'll upload docuements data later on. + { ...doc.document, data: {} }, + doc.title, + { + masterLanguageDocumentID, + ...fetchParams, + }, + ) - const { id } = await this.createDocument( - // We'll upload docuements data later on. - { ...migrationDocument.document, data: {} }, - migrationDocument.title, - { - masterLanguageDocumentID, - ...fetchParams, - }, - ) - - // Index old ID for Prismic to Prismic migration - if (migrationDocument.originalPrismicDocument) { - documents.set(migrationDocument.originalPrismicDocument.id, { - ...migrationDocument.document, - id, - }) - } - documents.set(migrationDocument, { ...migrationDocument.document, id }) - } + doc.document.id = id } reporter?.({ type: "documents:created", - data: { - created, - documents, - }, + data: { created }, }) - - return documents } /** @@ -613,41 +548,36 @@ export class WriteClient< * patched data. * * @param migration - A migration prepared with {@link createMigration}. - * @param assets - A map of assets available in the Prismic repository. - * @param documents - A map of documents available in the Prismic repository. * @param params - An event listener and additional fetch parameters. * * @internal This method is one of the step performed by the {@link migrate} method. */ private async migrateUpdateDocuments( migration: Migration, - assets: AssetMap, - documents: DocumentMap, { reporter, ...fetchParams }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise { let i = 0 - for (const migrationDocument of migration._documents) { + for (const doc of migration._documents) { reporter?.({ type: "documents:updating", data: { current: ++i, remaining: migration._documents.length - i, total: migration._documents.length, - document: migrationDocument, + document: doc, }, }) - const { id } = documents.get(migrationDocument)! - await migrationDocument._resolve({ assets, documents }) + await doc._resolve(migration) await this.updateDocument( - id, + doc.document.id!, // We need to forward again document name and tags to update them // in case the document already existed during the previous step. - migrationDocument.document, + doc.document, fetchParams, ) } diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index 3ba70c93..62403de9 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -1,3 +1,5 @@ +import type { Migration } from "../../Migration" + import type { Asset } from "../api/asset/asset" import type { FilledImageFieldImage, ImageField } from "../value/image" import { type FilledLinkToWebField, LinkType } from "../value/link" @@ -5,7 +7,6 @@ import type { LinkToMediaField } from "../value/linkToMedia" import { type RTImageNode, RichTextNodeType } from "../value/richText" import type { MigrationContentRelationship } from "./ContentRelationship" -import type { ResolveArgs } from "./Field" import { MigrationField } from "./Field" /** @@ -62,9 +63,7 @@ const assetToImage = ( */ export type MigrationAssetConfig = { /** - * ID of the asset used to reference it in Prismic documents. - * - * @internal + * ID the assets is indexed with on the migration instance. */ id: string | URL | File | NonNullable[0]>[0] @@ -116,7 +115,12 @@ export abstract class MigrationAsset< * * @internal */ - _config: MigrationAssetConfig + config: MigrationAssetConfig + + /** + * Asset object from Prismic available once created. + */ + asset?: Asset /** * Creates a migration asset used with the Prismic Migration API. @@ -129,7 +133,7 @@ export abstract class MigrationAsset< constructor(config: MigrationAssetConfig, initialField?: ImageLike) { super(initialField) - this._config = config + this.config = config } /** @@ -138,7 +142,7 @@ export abstract class MigrationAsset< * @returns A migration image instance. */ asImage(): MigrationImage { - return new MigrationImage(this._config, this._initialField) + return new MigrationImage(this.config, this._initialField) } /** @@ -150,7 +154,7 @@ export abstract class MigrationAsset< * @returns A migration link to media instance. */ asLinkToMedia(text?: string): MigrationLinkToMedia { - return new MigrationLinkToMedia(this._config, text, this._initialField) + return new MigrationLinkToMedia(this.config, text, this._initialField) } /** @@ -167,7 +171,7 @@ export abstract class MigrationAsset< | MigrationContentRelationship | FilledLinkToWebField, ): MigrationRTImageNode { - return new MigrationRTImageNode(this._config, linkTo, this._initialField) + return new MigrationRTImageNode(this.config, linkTo, this._initialField) } } @@ -194,14 +198,14 @@ export class MigrationImage extends MigrationAsset { return this } - async _resolve({ assets, documents }: ResolveArgs): Promise { - const asset = assets.get(this._config.id) + async _resolve(migration: Migration): Promise { + const asset = migration._assets.get(this.config.id)?.asset if (asset) { this._field = assetToImage(asset, this._initialField) for (const name in this.#thumbnails) { - await this.#thumbnails[name]._resolve({ assets, documents }) + await this.#thumbnails[name]._resolve(migration) const thumbnail = this.#thumbnails[name]._field if (thumbnail) { @@ -243,8 +247,8 @@ export class MigrationLinkToMedia extends MigrationAsset< this.text = text } - _resolve({ assets }: ResolveArgs): void { - const asset = assets.get(this._config.id) + _resolve(migration: Migration): void { + const asset = migration._assets.get(this.config.id)?.asset if (asset) { this._field = { @@ -300,11 +304,11 @@ export class MigrationRTImageNode extends MigrationAsset { this.linkTo = linkTo } - async _resolve({ assets, documents }: ResolveArgs): Promise { - const asset = assets.get(this._config.id) + async _resolve(migration: Migration): Promise { + const asset = migration._assets.get(this.config.id)?.asset if (this.linkTo instanceof MigrationField) { - await this.linkTo._resolve({ assets, documents }) + await this.linkTo._resolve(migration) } if (asset) { @@ -319,11 +323,3 @@ export class MigrationRTImageNode extends MigrationAsset { } } } - -/** - * A map of asset IDs to asset used to resolve assets when patching migration - * Prismic documents. - * - * @internal - */ -export type AssetMap = Map diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts index 33abf26a..b3357da7 100644 --- a/src/types/migration/ContentRelationship.ts +++ b/src/types/migration/ContentRelationship.ts @@ -1,9 +1,10 @@ +import type { Migration } from "../../Migration" + import type { FilledContentRelationshipField } from "../value/contentRelationship" import type { PrismicDocument } from "../value/document" import { LinkType } from "../value/link" -import type { PrismicMigrationDocument } from "./Document" -import type { ResolveArgs } from "./Field" +import { PrismicMigrationDocument } from "./Document" import { MigrationField } from "./Field" /** @@ -67,7 +68,7 @@ export class MigrationContentRelationship extends MigrationField { + async _resolve(migration: Migration): Promise { const config = typeof this.#unresolvedConfig === "function" ? await this.#unresolvedConfig() @@ -75,9 +76,12 @@ export class MigrationContentRelationship extends MigrationField { + async _resolve(migration: Migration): Promise { for (const dependency of this.#dependencies) { - await dependency._resolve(args) + await dependency._resolve(migration) } } } - -/** - * A map of document IDs, documents, and migration documents to content - * relationship field used to resolve content relationships when patching - * migration Prismic documents. - * - * @typeParam TDocuments - Type of Prismic documents in the repository. - * - * @internal - */ -export type DocumentMap = - Map< - string | PrismicMigrationDocument, - PrismicDocument | (MigrationDocument & Pick) - > diff --git a/src/types/migration/Field.ts b/src/types/migration/Field.ts index 0f559a01..2c098747 100644 --- a/src/types/migration/Field.ts +++ b/src/types/migration/Field.ts @@ -1,27 +1,8 @@ import type { AnyRegularField } from "../value/types" -import type { RTBlockNode, RTInlineNode } from "../value/richText" - -import type { AssetMap } from "./Asset" -import type { DocumentMap } from "./Document" +import type { Migration } from "../../Migration" -/** - * Arguments passed to the `_resolve` method of a migration field. - */ -export type ResolveArgs = { - /** - * A map of asset IDs to asset used to resolve assets when patching migration - * Prismic documents. - */ - assets: AssetMap - - /** - * A map of document IDs, documents, and migration documents to content - * relationship field used to resolve content relationships when patching - * migration Prismic documents. - */ - documents: DocumentMap -} +import type { RTBlockNode, RTInlineNode } from "../value/richText" /** * Interface for migration fields that can be resolved. @@ -30,12 +11,12 @@ interface Resolvable { /** * Resolves the field's value with the provided maps. * - * @param args - A map of documents and a map of assets to resolve content - * with. + * @param migration - A migration instance with documents and assets to use + * for resolving the field's value * * @internal */ - _resolve(args: ResolveArgs): Promise | void + _resolve(migration: Migration): Promise | void } /** @@ -88,5 +69,5 @@ export abstract class MigrationField< return this._field } - abstract _resolve(args: ResolveArgs): Promise | void + abstract _resolve(migration: Migration): Promise | void } diff --git a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap index c15c15c2..01048f6a 100644 --- a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap @@ -3,7 +3,12 @@ exports[`patches link fields > brokenLink > group 1`] = ` { "group": [ - {}, + { + "field": { + "isBroken": true, + "type": "Document", + }, + }, ], } `; @@ -14,11 +19,25 @@ exports[`patches link fields > brokenLink > shared slice 1`] = ` { "id": "b9dd0f2c3f8", "items": [ - {}, + { + "field": { + "isBroken": true, + "type": "Document", + }, + }, ], "primary": { + "field": { + "isBroken": true, + "type": "Document", + }, "group": [ - {}, + { + "field": { + "isBroken": true, + "type": "Document", + }, + }, ], }, "slice_label": null, @@ -36,9 +55,19 @@ exports[`patches link fields > brokenLink > slice 1`] = ` { "id": "306297c5eda", "items": [ - {}, + { + "field": { + "isBroken": true, + "type": "Document", + }, + }, ], - "primary": {}, + "primary": { + "field": { + "isBroken": true, + "type": "Document", + }, + }, "slice_label": "Aliquet", "slice_type": "vel", }, @@ -46,7 +75,14 @@ exports[`patches link fields > brokenLink > slice 1`] = ` } `; -exports[`patches link fields > brokenLink > static zone 1`] = `{}`; +exports[`patches link fields > brokenLink > static zone 1`] = ` +{ + "field": { + "isBroken": true, + "type": "Document", + }, +} +`; exports[`patches link fields > existing > group 1`] = ` { diff --git a/test/migration-createAsset.test.ts b/test/migration-createAsset.test.ts index ce3749e5..b6c57773 100644 --- a/test/migration-createAsset.test.ts +++ b/test/migration-createAsset.test.ts @@ -17,7 +17,7 @@ it("creates an asset from a url", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)).toEqual({ + expect(migration._assets.get(file)?.config).toEqual({ id: file, file, filename, @@ -32,7 +32,7 @@ it("creates an asset from a file", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)).toEqual({ + expect(migration._assets.get(file)?.config).toEqual({ id: file, file, filename, @@ -69,7 +69,7 @@ it("creates an asset from an existing asset", () => { migration.createAsset(asset) - expect(migration._assets.get(asset.id)).toStrictEqual({ + expect(migration._assets.get(asset.id)?.config).toStrictEqual({ id: asset.id, file: asset.url, filename: asset.filename, @@ -94,7 +94,7 @@ it("creates an asset from an image field", () => { migration.createAsset(image) - expect(migration._assets.get(image.id)).toEqual({ + expect(migration._assets.get(image.id)?.config).toEqual({ id: image.id, file: image.url, filename: image.url.split("/").pop(), @@ -119,7 +119,7 @@ it("creates an asset from a link to media field", () => { migration.createAsset(link) - expect(migration._assets.get(link.id)).toEqual({ + expect(migration._assets.get(link.id)?.config).toEqual({ id: link.id, file: link.url, filename: link.name, @@ -169,7 +169,7 @@ it("consolidates existing assets with additional metadata", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)).toStrictEqual({ + expect(migration._assets.get(file)?.config).toStrictEqual({ id: file, file, filename, @@ -186,7 +186,7 @@ it("consolidates existing assets with additional metadata", () => { tags: ["tag"], }) - expect(migration._assets.get(file)).toStrictEqual({ + expect(migration._assets.get(file)?.config).toStrictEqual({ id: file, file, filename, @@ -203,7 +203,7 @@ it("consolidates existing assets with additional metadata", () => { tags: ["tag", "tag 2"], }) - expect(migration._assets.get(file)).toStrictEqual({ + expect(migration._assets.get(file)?.config).toStrictEqual({ id: file, file, filename, @@ -215,7 +215,7 @@ it("consolidates existing assets with additional metadata", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)).toStrictEqual({ + expect(migration._assets.get(file)?.config).toStrictEqual({ id: file, file, filename, diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index 7affcaca..796d48ad 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -196,7 +196,7 @@ describe.each<{ migration.createDocument(document, documentTitle) - expect(migration._assets.get(id)).toStrictEqual(expected) + expect(migration._assets.get(id)?.config).toStrictEqual(expected) }) it("group fields", ({ mock }) => { @@ -216,7 +216,7 @@ describe.each<{ migration.createDocument(document, documentTitle) - expect(migration._assets.get(id)).toStrictEqual(expected) + expect(migration._assets.get(id)?.config).toStrictEqual(expected) }) it("slice's primary zone", ({ mock }) => { @@ -246,7 +246,7 @@ describe.each<{ migration.createDocument(document, documentTitle) - expect(migration._assets.get(id)).toStrictEqual(expected) + expect(migration._assets.get(id)?.config).toStrictEqual(expected) }) it("slice's repeatable zone", ({ mock }) => { @@ -276,6 +276,6 @@ describe.each<{ migration.createDocument(document, documentTitle) - expect(migration._assets.get(id)).toStrictEqual(expected) + expect(migration._assets.get(id)?.config).toStrictEqual(expected) }) }) diff --git a/test/migration-getByUID.test.ts b/test/migration-getByUID.test.ts index 496e7b22..e60ad352 100644 --- a/test/migration-getByUID.test.ts +++ b/test/migration-getByUID.test.ts @@ -13,11 +13,9 @@ it("returns a document with a matching UID", () => { } const documentName = "documentName" - const migrationDocument = migration.createDocument(document, documentName) + const doc = migration.createDocument(document, documentName) - expect(migration.getByUID(document.type, document.uid)).toStrictEqual( - migrationDocument, - ) + expect(migration.getByUID(document.type, document.uid)).toStrictEqual(doc) }) it("returns `undefined` if a document is not found", () => { diff --git a/test/migration-getSingle.test.ts b/test/migration-getSingle.test.ts index 4dbcc129..ff173cb5 100644 --- a/test/migration-getSingle.test.ts +++ b/test/migration-getSingle.test.ts @@ -12,9 +12,9 @@ it("returns a document of a given singleton type", () => { } const documentName = "documentName" - const migrationDocument = migration.createDocument(document, documentName) + const doc = migration.createDocument(document, documentName) - expect(migration.getSingle(document.type)).toStrictEqual(migrationDocument) + expect(migration.getSingle(document.type)).toStrictEqual(doc) }) it("returns `undefined` if a document is not found", () => { diff --git a/test/writeClient-migrate-assets.test.ts b/test/writeClient-migrate-assets.test.ts index 298fe6e5..5c9ed198 100644 --- a/test/writeClient-migrate-assets.test.ts +++ b/test/writeClient-migrate-assets.test.ts @@ -76,7 +76,7 @@ it.concurrent("skips creating existing assets", async (ctx) => { const migration = prismic.createMigration() const asset = assetsDatabase[0][0] - migration.createAsset(asset) + const migrationAsset = migration.createAsset(asset) const reporter = vi.fn() @@ -89,7 +89,7 @@ it.concurrent("skips creating existing assets", async (ctx) => { current: 1, remaining: 0, total: 1, - asset: expect.objectContaining({ id: asset.id, file: asset.url }), + asset: migrationAsset, }, }) }) @@ -109,7 +109,7 @@ it.concurrent("creates new asset from string file data", async (ctx) => { mockPrismicMigrationAPI({ ctx, client }) const migration = prismic.createMigration() - migration.createAsset(dummyFileData, asset.filename) + const migrationAsset = migration.createAsset(dummyFileData, asset.filename) const reporter = vi.fn() @@ -123,10 +123,7 @@ it.concurrent("creates new asset from string file data", async (ctx) => { current: 1, remaining: 0, total: 1, - asset: expect.objectContaining({ - file: dummyFileData, - filename: asset.filename, - }), + asset: migrationAsset, }, }) expect(assetsDatabase.flat()).toHaveLength(1) @@ -147,7 +144,7 @@ it.concurrent("creates new asset from a File instance", async (ctx) => { mockPrismicMigrationAPI({ ctx, client }) const migration = prismic.createMigration() - migration.createAsset(dummyFile, asset.filename) + const migrationAsset = migration.createAsset(dummyFile, asset.filename) const reporter = vi.fn() @@ -161,10 +158,7 @@ it.concurrent("creates new asset from a File instance", async (ctx) => { current: 1, remaining: 0, total: 1, - asset: expect.objectContaining({ - file: dummyFile, - filename: asset.filename, - }), + asset: migrationAsset, }, }) expect(assetsDatabase.flat()).toHaveLength(1) @@ -192,7 +186,10 @@ it.concurrent( ) const migration = prismic.createMigration() - migration.createAsset(new URL(asset.url), asset.filename) + const migrationAsset = migration.createAsset( + new URL(asset.url), + asset.filename, + ) const reporter = vi.fn() @@ -206,10 +203,7 @@ it.concurrent( current: 1, remaining: 0, total: 1, - asset: expect.objectContaining({ - file: new URL(asset.url), - filename: asset.filename, - }), + asset: migrationAsset, }, }) expect(assetsDatabase.flat()).toHaveLength(1) @@ -238,7 +232,7 @@ it.concurrent( ) const migration = prismic.createMigration() - migration.createAsset(asset.url, asset.filename) + const migrationAsset = migration.createAsset(asset.url, asset.filename) const reporter = vi.fn() @@ -252,10 +246,7 @@ it.concurrent( current: 1, remaining: 0, total: 1, - asset: expect.objectContaining({ - file: asset.url, - filename: asset.filename, - }), + asset: migrationAsset, }, }) expect(assetsDatabase.flat()).toHaveLength(1) @@ -284,7 +275,10 @@ it.concurrent( ) const migration = prismic.createMigration() - migration.createAsset(new URL(asset.url), asset.filename) + const migrationAsset = migration.createAsset( + new URL(asset.url), + asset.filename, + ) const reporter = vi.fn() @@ -298,10 +292,7 @@ it.concurrent( current: 1, remaining: 0, total: 1, - asset: expect.objectContaining({ - file: new URL(asset.url), - filename: asset.filename, - }), + asset: migrationAsset, }, }) expect(assetsDatabase.flat()).toHaveLength(1) @@ -330,7 +321,7 @@ it.concurrent( ) const migration = prismic.createMigration() - migration.createAsset(asset.url, asset.filename) + const migrationAsset = migration.createAsset(asset.url, asset.filename) const reporter = vi.fn() @@ -344,10 +335,7 @@ it.concurrent( current: 1, remaining: 0, total: 1, - asset: expect.objectContaining({ - file: asset.url, - filename: asset.filename, - }), + asset: migrationAsset, }, }) expect(assetsDatabase.flat()).toHaveLength(1) @@ -418,7 +406,7 @@ it.concurrent( ) const migration = prismic.createMigration() - migration.createAsset(asset.url, asset.filename) + const migrationAsset = migration.createAsset(asset.url, asset.filename) const reporter = vi.fn() @@ -432,10 +420,7 @@ it.concurrent( current: 1, remaining: 0, total: 1, - asset: expect.objectContaining({ - file: asset.url, - filename: asset.filename, - }), + asset: migrationAsset, }, }) }, diff --git a/test/writeClient-migrate-documents.test.ts b/test/writeClient-migrate-documents.test.ts index 73e25803..8d10f287 100644 --- a/test/writeClient-migrate-documents.test.ts +++ b/test/writeClient-migrate-documents.test.ts @@ -8,7 +8,6 @@ import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" import * as prismic from "../src" -import type { DocumentMap } from "../src/types/migration/Document" // Skip test on Node 16 and 18 (File and FormData support) const isNode16 = process.version.startsWith("v16") @@ -50,33 +49,6 @@ it.concurrent("infers master locale", async (ctx) => { }) }) -it.concurrent("discovers existing documents", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const queryResponse = createPagedQueryResponses({ - ctx, - pages: 2, - pageSize: 1, - }) - - mockPrismicRestAPIV2({ ctx, queryResponse }) - mockPrismicAssetAPI({ ctx, client }) - mockPrismicMigrationAPI({ ctx, client }) - - const migration = prismic.createMigration() - - const reporter = vi.fn() - - await client.migrate(migration, { reporter }) - - expect(reporter).toHaveBeenCalledWith({ - type: "documents:existing", - data: { - existing: 2, - }, - }) -}) - it.concurrent("skips creating existing documents", async (ctx) => { const client = createTestWriteClient({ ctx }) @@ -93,27 +65,16 @@ it.concurrent("skips creating existing documents", async (ctx) => { const migration = prismic.createMigration() - const migrationDocument = migration.updateDocument(document, "foo") + migration.updateDocument(document, "foo") const reporter = vi.fn() await client.migrate(migration, { reporter }) - expect(reporter).toHaveBeenCalledWith({ - type: "documents:skipping", - data: { - reason: "exists", - current: 1, - remaining: 0, - total: 1, - document: migrationDocument, - }, - }) expect(reporter).toHaveBeenCalledWith({ type: "documents:created", data: { created: 0, - documents: expect.anything(), }, }) }) @@ -134,16 +95,9 @@ it.concurrent("creates new documents", async (ctx) => { const migration = prismic.createMigration() - const migrationDocument = migration.createDocument(document, "foo") + const doc = migration.createDocument(document, "foo") - let documents: DocumentMap | undefined - const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( - (event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }, - ) + const reporter = vi.fn() await client.migrate(migration, { reporter }) @@ -153,10 +107,10 @@ it.concurrent("creates new documents", async (ctx) => { current: 1, remaining: 0, total: 1, - document: migrationDocument, + document: doc, }, }) - expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) + expect(doc.document.id).toBe(newDocument.id) }) it.concurrent( @@ -183,20 +137,13 @@ it.concurrent( const migration = prismic.createMigration() - const migrationDocument = migration.createDocument(document, "foo", { + const doc = migration.createDocument(document, "foo", { masterLanguageDocument: migration.createContentRelationship( masterLanguageDocument, ), }) - let documents: DocumentMap | undefined - const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( - (event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }, - ) + const reporter = vi.fn() await client.migrate(migration, { reporter }) @@ -206,10 +153,10 @@ it.concurrent( current: 1, remaining: 0, total: 1, - document: migrationDocument, + document: doc, }, }) - ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) + ctx.expect(migration._documents[0].document.id).toBe(newDocument.id) ctx.expect.assertions(3) }, ) @@ -242,27 +189,20 @@ it.concurrent( masterLanguageDocument, "foo", ) - const migrationDocument = migration.createDocument(document, "bar", { + const doc = migration.createDocument(document, "bar", { masterLanguageDocument: migration.createContentRelationship( masterLanguageMigrationDocument, ), }) - let documents: DocumentMap | undefined - const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( - (event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }, - ) + const reporter = vi.fn() await client.migrate(migration, { reporter }) ctx - .expect(documents?.get(masterLanguageMigrationDocument)?.id) + .expect(masterLanguageMigrationDocument.document.id) .toBe(newDocuments[0].id) - ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocuments[1].id) + ctx.expect(doc.document.id).toBe(newDocuments[1].id) ctx.expect.assertions(3) }, ) @@ -291,20 +231,13 @@ it.concurrent( const migration = prismic.createMigration() - const migrationDocument = migration.createDocument(document, "foo", { + const doc = migration.createDocument(document, "foo", { masterLanguageDocument: migration.createContentRelationship( () => masterLanguageDocument, ), }) - let documents: DocumentMap | undefined - const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( - (event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }, - ) + const reporter = vi.fn() await client.migrate(migration, { reporter }) @@ -314,10 +247,10 @@ it.concurrent( current: 1, remaining: 0, total: 1, - document: migrationDocument, + document: doc, }, }) - ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) + ctx.expect(doc.document.id).toBe(newDocument.id) ctx.expect.assertions(3) }, ) @@ -350,27 +283,20 @@ it.concurrent( masterLanguageDocument, "foo", ) - const migrationDocument = migration.createDocument(document, "bar", { + const doc = migration.createDocument(document, "bar", { masterLanguageDocument: migration.createContentRelationship( () => masterLanguageMigrationDocument, ), }) - let documents: DocumentMap | undefined - const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( - (event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }, - ) + const reporter = vi.fn() await client.migrate(migration, { reporter }) ctx - .expect(documents?.get(masterLanguageMigrationDocument)?.id) + .expect(masterLanguageMigrationDocument.document.id) .toBe(newDocuments[0].id) - ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocuments[1].id) + ctx.expect(doc.document.id).toBe(newDocuments[1].id) ctx.expect.assertions(3) }, ) @@ -403,19 +329,9 @@ it.concurrent( const migration = prismic.createMigration() - const migrationDocument = migration.createDocumentFromPrismic( - document, - "foo", - ) + const doc = migration.createDocumentFromPrismic(document, "foo") - let documents: DocumentMap | undefined - const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( - (event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }, - ) + const reporter = vi.fn() await client.migrate(migration, { reporter }) @@ -425,10 +341,10 @@ it.concurrent( current: 1, remaining: 0, total: 1, - document: migrationDocument, + document: doc, }, }) - ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocument.id) + ctx.expect(doc.document.id).toBe(newDocument.id) ctx.expect.assertions(3) }, ) @@ -457,25 +373,18 @@ it.concurrent("creates master locale documents first", async (ctx) => { const migration = prismic.createMigration() - const migrationDocument = migration.createDocument(document, "bar") + const doc = migration.createDocument(document, "bar") const masterLanguageMigrationDocument = migration.createDocument( masterLanguageDocument, "foo", ) - let documents: DocumentMap | undefined - const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( - (event) => { - if (event.type === "documents:created") { - documents = event.data.documents - } - }, - ) + const reporter = vi.fn() await client.migrate(migration, { reporter }) ctx - .expect(documents?.get(masterLanguageMigrationDocument)?.id) + .expect(masterLanguageMigrationDocument.document.id) .toBe(newDocuments[0].id) - ctx.expect(documents?.get(migrationDocument)?.id).toBe(newDocuments[1].id) + ctx.expect(doc.document.id).toBe(newDocuments[1].id) }) diff --git a/test/writeClient-migrate-patch-contentRelationship.test.ts b/test/writeClient-migrate-patch-contentRelationship.test.ts index 0bfc830d..73a5def5 100644 --- a/test/writeClient-migrate-patch-contentRelationship.test.ts +++ b/test/writeClient-migrate-patch-contentRelationship.test.ts @@ -41,7 +41,9 @@ testMigrationFieldPatching("patches link fields", { return contentRelationship }, - brokenLink: ({ ctx }) => ctx.mock.value.link({ type: "Document" }), + brokenLink: () => { + return { type: "Document", isBroken: true } + }, richTextLinkNode: ({ migration, existingDocuments }) => [ { type: RichTextNodeType.paragraph, diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index d9e024f9..660aafdf 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -9,8 +9,6 @@ import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" import * as prismic from "../src" -import type { AssetMap } from "../src/types/migration/Asset" -import type { DocumentMap } from "../src/types/migration/Document" // Skip test on Node 16 and 18 (File and FormData support) const isNode16 = process.version.startsWith("v16") @@ -51,23 +49,13 @@ it.concurrent("performs migration", async (ctx) => { migration.createDocument(documentFoo, "foo") migration.createDocument(documentBar, "bar") - let documents: DocumentMap | undefined - let assets: AssetMap | undefined - const reporter = vi.fn<(event: prismic.MigrateReporterEvents) => void>( - (event) => { - if (event.type === "assets:created") { - assets = event.data.assets - } else if (event.type === "documents:created") { - documents = event.data.documents - } - }, - ) + const reporter = vi.fn() await client.migrate(migration, { reporter }) - expect(assets?.size).toBe(1) + expect(migration._assets?.size).toBe(1) expect(assetsDatabase.flat()).toHaveLength(1) - expect(documents?.size).toBe(2) + expect(migration._documents.length).toBe(2) expect(Object.keys(documentsDatabase)).toHaveLength(2) expect(reporter).toHaveBeenCalledWith({ @@ -84,7 +72,6 @@ it.concurrent("performs migration", async (ctx) => { type: "assets:created", data: { created: 1, - assets: expect.any(Map), }, }) @@ -92,7 +79,6 @@ it.concurrent("performs migration", async (ctx) => { type: "documents:created", data: { created: 2, - documents: expect.any(Map), }, }) @@ -142,7 +128,6 @@ it.concurrent("migrates nothing when migration is empty", async (ctx) => { type: "assets:created", data: { created: 0, - assets: expect.any(Map), }, }) @@ -150,7 +135,6 @@ it.concurrent("migrates nothing when migration is empty", async (ctx) => { type: "documents:created", data: { created: 0, - documents: expect.any(Map), }, }) From 54056ef80e701d1abecb0352fb786453cbc4905e Mon Sep 17 00:00:00 2001 From: lihbr Date: Sun, 15 Sep 2024 22:21:14 +0200 Subject: [PATCH 52/61] refactor: remove existing assets querying --- src/Migration.ts | 1 + src/WriteClient.ts | 288 +++-------------- src/lib/prepareMigrationDocumentData.ts | 16 +- src/types/migration/Asset.ts | 44 ++- .../testMigrationFieldPatching.ts | 12 +- test/migration-createDocument.test.ts | 261 +--------------- ...igration-createDocumentFromPrismic.test.ts | 256 +++++++++++++++ test/writeClient-getAssets.test.ts | 295 ------------------ test/writeClient-migrate-assets.test.ts | 77 ----- ...-migrate-patch-contentRelationship.test.ts | 23 +- test/writeClient-migrate-patch-image.test.ts | 102 +++--- ...teClient-migrate-patch-linkToMedia.test.ts | 71 +++-- ...teClient-migrate-patch-rtImageNode.test.ts | 57 ++-- 13 files changed, 489 insertions(+), 1014 deletions(-) create mode 100644 test/migration-createDocumentFromPrismic.test.ts delete mode 100644 test/writeClient-getAssets.test.ts diff --git a/src/Migration.ts b/src/Migration.ts index e7bba6e0..0fe7d838 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -315,6 +315,7 @@ export class Migration { const { record: data, dependencies } = prepareMigrationDocumentData( document.data, this.createAsset.bind(this), + true, ) const doc = new PrismicMigrationDocument( diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 0759ab06..d1c46adc 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -3,8 +3,6 @@ import { pLimit } from "./lib/pLimit" import type { Asset, - GetAssetsParams, - GetAssetsResult, PatchAssetParams, PatchAssetResult, PostAssetParams, @@ -84,16 +82,6 @@ type MigrateReporterEventMap = { assets: number } } - "assets:existing": { - existing: number - } - "assets:skipping": { - reason: string - current: number - remaining: number - total: number - asset: MigrationAsset - } "assets:creating": { current: number remaining: number @@ -142,32 +130,6 @@ export type MigrateReporterEvents = { > }[MigrateReporterEventTypes] -/** - * A query response from the Prismic Asset API. The response contains pagination - * metadata and a list of matching results for the query. - */ -type GetAssetsReturnType = { - /** - * Found assets for the query. - */ - results: Asset[] - - /** - * Total number of assets found for the query. - */ - total_results_size: number - - /** - * IDs of assets that were not found when filtering by IDs. - */ - missing_ids?: string[] - - /** - * A function to fectch the next page of assets if available. - */ - next?: () => Promise -} - /** * Additional parameters for creating an asset in the Prismic media library. */ @@ -355,101 +317,56 @@ export class WriteClient< ...fetchParams }: { reporter?: (event: MigrateReporterEvents) => void } & FetchParams = {}, ): Promise { - const existingAssets = new Map() - - // Get all existing assets - let getAssetsResult: GetAssetsReturnType | undefined = undefined - do { - if (getAssetsResult) { - getAssetsResult = await getAssetsResult.next!() - } else { - getAssetsResult = await this.getAssets({ - pageSize: 100, - ...fetchParams, - }) - } + let created = 0 + for (const [_, migrationAsset] of migration._assets) { + reporter?.({ + type: "assets:creating", + data: { + current: ++created, + remaining: migration._assets.size - created, + total: migration._assets.size, + asset: migrationAsset, + }, + }) - for (const asset of getAssetsResult.results) { - existingAssets.set(asset.id, asset) - } - } while (getAssetsResult?.next) + const { file, filename, notes, credits, alt, tags } = + migrationAsset.config - reporter?.({ - type: "assets:existing", - data: { - existing: existingAssets.size, - }, - }) + let resolvedFile: PostAssetParams["file"] | File + if (typeof file === "string") { + let url: URL | undefined + try { + url = new URL(file) + } catch (error) { + // noop only on invalid URL, fetch errors will throw in the next if statement + } - // Create assets - let i = 0 - let created = 0 - for (const [_, migrationAsset] of migration._assets) { - if (existingAssets.has(migrationAsset.config.id)) { - migrationAsset.asset = existingAssets.get(migrationAsset.config.id) - - // Is this essential for deduplication? - reporter?.({ - type: "assets:skipping", - data: { - reason: "already exists", - current: ++i, - remaining: migration._assets.size - i, - total: migration._assets.size, - asset: migrationAsset, - }, - }) - } else { - created++ - reporter?.({ - type: "assets:creating", - data: { - current: ++i, - remaining: migration._assets.size - i, - total: migration._assets.size, - asset: migrationAsset, - }, - }) - - const { file, filename, notes, credits, alt, tags } = - migrationAsset.config - - let resolvedFile: PostAssetParams["file"] | File - if (typeof file === "string") { - let url: URL | undefined - try { - url = new URL(file) - } catch (error) { - // noop only on invalid URL, fetch errors will throw in the next if statement - } - - if (url) { - // File is a URL, fetch it - resolvedFile = await this.fetchForeignAsset( - url.toString(), - fetchParams, - ) - } else { - // File is actual file content, use it as-is - resolvedFile = file - } - } else if (file instanceof URL) { - // File is a URL instance, fetch it + if (url) { + // File is a URL, fetch it resolvedFile = await this.fetchForeignAsset( - file.toString(), + url.toString(), fetchParams, ) } else { + // File is actual file content, use it as-is resolvedFile = file } + } else if (file instanceof URL) { + // File is a URL instance, fetch it + resolvedFile = await this.fetchForeignAsset( + file.toString(), + fetchParams, + ) + } else { + resolvedFile = file + } - const asset = await this.createAsset(resolvedFile, filename, { - ...{ notes, credits, alt, tags }, - ...fetchParams, - }) + const asset = await this.createAsset(resolvedFile, filename, { + ...{ notes, credits, alt, tags }, + ...fetchParams, + }) - migrationAsset.asset = asset - } + migrationAsset.asset = asset } reporter?.({ @@ -590,83 +507,6 @@ export class WriteClient< }) } - /** - * Queries assets from the Prismic repository's media library. - * - * @param params - Parameters to filter, sort, and paginate results. - * - * @returns A paginated response containing the result of the query. - */ - private async getAssets({ - pageSize, - cursor, - // assetType, - // keyword, - // ids, - // tags, - ...params - }: Pick & - FetchParams = {}): Promise { - // Resolve tags if any - // if (tags && tags.length) { - // tags = await this.resolveAssetTagIDs(tags, params) - // } - - const url = new URL("assets", this.assetAPIEndpoint) - - if (pageSize) { - url.searchParams.set("pageSize", pageSize.toString()) - } - - if (cursor) { - url.searchParams.set("cursor", cursor) - } - - // if (assetType) { - // url.searchParams.set("assetType", assetType) - // } - - // if (keyword) { - // url.searchParams.set("keyword", keyword) - // } - - // if (ids) { - // ids.forEach((id) => url.searchParams.append("ids", id)) - // } - - // if (tags) { - // tags.forEach((tag) => url.searchParams.append("tags", tag)) - // } - - const { - items, - total, - // missing_ids, - cursor: nextCursor, - } = await this.fetch( - url.toString(), - this.buildAssetAPIQueryParams({ params }), - ) - - return { - results: items, - total_results_size: total, - // missing_ids: missing_ids || [], - next: nextCursor - ? () => - this.getAssets({ - pageSize, - cursor: nextCursor, - // assetType, - // keyword, - // ids, - // tags, - ...params, - }) - : undefined, - } - } - /** * Creates an asset in the Prismic media library. * @@ -770,56 +610,6 @@ export class WriteClient< ) } - // We don't want to expose those utilities for now, - // and we don't have any internal use for them yet. - - /** - * Deletes an asset from the Prismic media library. - * - * @param assetOrID - The asset or ID of the asset to delete. - * @param params - Additional fetch parameters. - */ - // private async deleteAsset( - // assetOrID: string | Asset, - // params?: FetchParams, - // ): Promise { - // const url = new URL( - // `assets/${typeof assetOrID === "string" ? assetOrID : assetOrID.id}`, - // this.assetAPIEndpoint, - // ) - - // await this.fetch( - // url.toString(), - // this.buildAssetAPIQueryParams({ method: "DELETE", params }), - // ) - // } - - /** - * Deletes multiple assets from the Prismic media library. - * - * @param assetsOrIDs - An array of asset IDs or assets to delete. - * @param params - Additional fetch parameters. - */ - // private async deleteAssets( - // assetsOrIDs: (string | Asset)[], - // params?: FetchParams, - // ): Promise { - // const url = new URL("assets/bulk-delete", this.assetAPIEndpoint) - - // await this.fetch( - // url.toString(), - // this.buildAssetAPIQueryParams({ - // method: "POST", - // body: { - // ids: assetsOrIDs.map((assetOrID) => - // typeof assetOrID === "string" ? assetOrID : assetOrID.id, - // ), - // }, - // params, - // }), - // ) - // } - /** * Fetches a foreign asset from a URL. * diff --git a/src/lib/prepareMigrationDocumentData.ts b/src/lib/prepareMigrationDocumentData.ts index 4bfd01de..89dbcb18 100644 --- a/src/lib/prepareMigrationDocumentData.ts +++ b/src/lib/prepareMigrationDocumentData.ts @@ -18,6 +18,8 @@ import * as is from "./isValue" * * @param record - Record of Prismic fields to work with. * @param onAsset - Callback that is called for each asset found. + * @param fromPrismic - Whether the record is from another Prismic repository or + * not. * * @returns An object containing the record with replaced assets and links and a * list of dependencies found and/or created. @@ -30,6 +32,7 @@ export const prepareMigrationDocumentData = < onAsset: ( asset: FilledImageFieldImage | FilledLinkToMediaField, ) => MigrationImage, + fromPrismic?: boolean, ): { record: TRecord; dependencies: MigrationField[] } => { const result = {} as Record const dependencies: MigrationField[] = [] @@ -41,7 +44,7 @@ export const prepareMigrationDocumentData = < // Existing migration fields dependencies.push(field) result[key] = field - } else if (is.linkToMedia(field)) { + } else if (fromPrismic && is.linkToMedia(field)) { // Link to media // TODO: Remove when link text PR is merged // @ts-expect-error - Future-proofing for link text @@ -49,7 +52,7 @@ export const prepareMigrationDocumentData = < dependencies.push(linkToMedia) result[key] = linkToMedia - } else if (is.rtImageNode(field)) { + } else if (fromPrismic && is.rtImageNode(field)) { // Rich text image nodes const rtImageNode = onAsset(field).asRTImageNode() @@ -58,6 +61,7 @@ export const prepareMigrationDocumentData = < rtImageNode.linkTo = prepareMigrationDocumentData( { linkTo: field.linkTo }, onAsset, + fromPrismic, ).record.linkTo as | MigrationContentRelationship | MigrationLinkToMedia @@ -66,7 +70,7 @@ export const prepareMigrationDocumentData = < dependencies.push(rtImageNode) result[key] = rtImageNode - } else if (is.image(field)) { + } else if (fromPrismic && is.image(field)) { // Image fields const image = onAsset(field).asImage() @@ -89,7 +93,7 @@ export const prepareMigrationDocumentData = < dependencies.push(image) result[key] = image - } else if (is.contentRelationship(field)) { + } else if (fromPrismic && is.contentRelationship(field)) { // Content relationships const contentRelationship = new MigrationContentRelationship( field, @@ -106,7 +110,7 @@ export const prepareMigrationDocumentData = < for (const item of field) { const { record, dependencies: itemDependencies } = - prepareMigrationDocumentData({ item }, onAsset) + prepareMigrationDocumentData({ item }, onAsset, fromPrismic) array.push(record.item) dependencies.push(...itemDependencies) @@ -116,7 +120,7 @@ export const prepareMigrationDocumentData = < } else if (field && typeof field === "object") { // Traverse objects const { record, dependencies: fieldDependencies } = - prepareMigrationDocumentData({ ...field }, onAsset) + prepareMigrationDocumentData({ ...field }, onAsset, fromPrismic) dependencies.push(...fieldDependencies) result[key] = record diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index 62403de9..a3eaafd9 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -26,7 +26,7 @@ type ImageLike = * * @returns Equivalent image field. */ -const assetToImage = ( +export const assetToImage = ( asset: Asset, maybeInitialField?: ImageLike, ): FilledImageFieldImage => { @@ -58,6 +58,33 @@ const assetToImage = ( } } +/** + * Converts an asset to a link to media field. + * + * @param asset - Asset to convert. + * @param text - Link text for the link to media field if any. + * + * @returns Equivalent link to media field. + */ +export const assetToLinkToMedia = ( + asset: Asset, + text?: string, +): LinkToMediaField<"filled"> => { + return { + id: asset.id, + link_type: LinkType.Media, + name: asset.filename, + kind: asset.kind, + url: asset.url, + size: `${asset.size}`, + height: typeof asset.height === "number" ? `${asset.height}` : undefined, + width: typeof asset.width === "number" ? `${asset.width}` : undefined, + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + text, + } +} + /** * An asset to be uploaded to Prismic media library. */ @@ -251,20 +278,7 @@ export class MigrationLinkToMedia extends MigrationAsset< const asset = migration._assets.get(this.config.id)?.asset if (asset) { - this._field = { - id: asset.id, - link_type: LinkType.Media, - name: asset.filename, - kind: asset.kind, - url: asset.url, - size: `${asset.size}`, - height: - typeof asset.height === "number" ? `${asset.height}` : undefined, - width: typeof asset.width === "number" ? `${asset.width}` : undefined, - // TODO: Remove when link text PR is merged - // @ts-expect-error - Future-proofing for link text - text: this.text, - } + this._field = assetToLinkToMedia(asset, this.text) } } } diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index 3dd88d58..3f59b0c2 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -32,6 +32,7 @@ type GetDataArgs = { type InternalTestMigrationFieldPatchingArgs = { getData: (args: GetDataArgs) => prismic.ExistingPrismicDocument["data"] expectStrictEqual?: boolean + mode?: "new" | "fromPrismic" } const internalTestMigrationFieldPatching = ( @@ -62,7 +63,7 @@ const internalTestMigrationFieldPatching = ( ...ctx.mock.value.document(), uid: ctx.mock.value.keyText(), } - const { id: __id, ...newDocument } = ctx.mock.value.document() + const { id: originalID, ...newDocument } = ctx.mock.value.document() const newID = "id-new" @@ -114,7 +115,14 @@ const internalTestMigrationFieldPatching = ( mockedDomain, }) - migration.createDocument(newDocument, "new") + if (!args.mode || args.mode === "new") { + migration.createDocument(newDocument, "new") + } else { + migration.createDocumentFromPrismic( + { ...newDocument, id: originalID }, + args.mode, + ) + } // We speed up the internal rate limiter to make these tests run faster (from 4500ms to nearly instant) const migrationProcess = client.migrate(migration) diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index 796d48ad..c1199258 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -1,9 +1,6 @@ -import { describe, expect, it } from "vitest" - -import type { MockFactory } from "@prismicio/mock" +import { expect, it } from "vitest" import * as prismic from "../src" -import type { MigrationAssetConfig } from "../src/types/migration/Asset" import { PrismicMigrationDocument } from "../src/types/migration/Document" it("creates a document", () => { @@ -23,259 +20,3 @@ it("creates a document", () => { new PrismicMigrationDocument(document, documentTitle, { dependencies: [] }), ) }) - -it("creates a document from an existing Prismic document", (ctx) => { - const migration = prismic.createMigration() - - const document = ctx.mock.value.document() - const documentTitle = "documentTitle" - - migration.createDocument(document, documentTitle) - - expect(migration._documents[0]).toStrictEqual( - new PrismicMigrationDocument(document, documentTitle, { dependencies: [] }), - ) -}) - -describe.each<{ - fieldType: string - getField: (mock: MockFactory) => { - id: string - field: - | prismic.FilledImageFieldImage - | prismic.FilledLinkToMediaField - | prismic.RichTextField<"filled"> - expected: MigrationAssetConfig - } -}>([ - { - fieldType: "image", - getField: (mock) => { - const image = mock.value.image({ state: "filled" }) - - return { - id: image.id, - field: image, - expected: { - alt: image.alt || undefined, - credits: image.copyright || undefined, - file: image.url.split("?")[0], - filename: image.url.split("/").pop()!.split("?")[0], - id: image.id, - notes: undefined, - tags: undefined, - }, - } - }, - }, - { - fieldType: "link to media", - getField: (mock) => { - const linkToMedia = mock.value.linkToMedia({ state: "filled" }) - - return { - id: linkToMedia.id, - field: linkToMedia, - expected: { - alt: undefined, - credits: undefined, - file: linkToMedia.url.split("?")[0], - filename: linkToMedia.name, - id: linkToMedia.id, - notes: undefined, - tags: undefined, - }, - } - }, - }, - { - fieldType: "rich text (image)", - getField: (mock) => { - const image = mock.value.image({ state: "filled" }) - const richText: prismic.RichTextField<"filled"> = [ - { - type: prismic.RichTextNodeType.image, - ...image, - }, - ] - - return { - id: image.id, - field: richText, - expected: { - alt: image.alt || undefined, - credits: image.copyright || undefined, - file: image.url.split("?")[0], - filename: image.url.split("/").pop()!.split("?")[0], - id: image.id, - notes: undefined, - tags: undefined, - }, - } - }, - }, - { - fieldType: "rich text (link to media span)", - getField: (mock) => { - const linkToMedia = mock.value.linkToMedia({ state: "filled" }) - const richText = mock.value.richText({ - state: "filled", - }) as prismic.RichTextField<"filled"> - - richText.push({ - type: prismic.RichTextNodeType.paragraph, - text: "lorem", - spans: [ - { - start: 0, - end: 5, - type: prismic.RichTextNodeType.hyperlink, - data: linkToMedia, - }, - ], - }) - - return { - id: linkToMedia.id, - field: richText, - expected: { - alt: undefined, - credits: undefined, - file: linkToMedia.url.split("?")[0], - filename: linkToMedia.name, - id: linkToMedia.id, - notes: undefined, - tags: undefined, - }, - } - }, - }, - { - fieldType: "rich text (image's link to media)", - getField: (mock) => { - const image = mock.value.image({ state: "filled" }) - const linkToMedia = mock.value.linkToMedia({ state: "filled" }) - const richText: prismic.RichTextField<"filled"> = [ - { - type: prismic.RichTextNodeType.image, - linkTo: linkToMedia, - ...image, - }, - ] - - return { - id: linkToMedia.id, - field: richText, - expected: { - alt: undefined, - credits: undefined, - file: linkToMedia.url.split("?")[0], - filename: linkToMedia.name, - id: linkToMedia.id, - notes: undefined, - tags: undefined, - }, - } - }, - }, -])("extracts assets from image fields ($fieldType)", ({ getField }) => { - it("regular fields", ({ mock }) => { - const migration = prismic.createMigration() - - const { id, field, expected } = getField(mock) - - const document: prismic.PendingPrismicDocument = { - type: "type", - uid: "uid", - lang: "lang", - data: { field }, - } - const documentTitle = "documentTitle" - - expect(migration._assets.size).toBe(0) - - migration.createDocument(document, documentTitle) - - expect(migration._assets.get(id)?.config).toStrictEqual(expected) - }) - - it("group fields", ({ mock }) => { - const migration = prismic.createMigration() - - const { id, field, expected } = getField(mock) - - const document: prismic.PendingPrismicDocument = { - type: "type", - uid: "uid", - lang: "lang", - data: { group: [{ field }] }, - } - const documentTitle = "documentTitle" - - expect(migration._assets.size).toBe(0) - - migration.createDocument(document, documentTitle) - - expect(migration._assets.get(id)?.config).toStrictEqual(expected) - }) - - it("slice's primary zone", ({ mock }) => { - const migration = prismic.createMigration() - - const { id, field, expected } = getField(mock) - - const slices: prismic.SliceZone = [ - { - id: "id", - slice_type: "slice_type", - slice_label: "slice_label", - primary: { field }, - items: [], - }, - ] - - const document: prismic.PendingPrismicDocument = { - type: "type", - uid: "uid", - lang: "lang", - data: { slices }, - } - const documentTitle = "documentTitle" - - expect(migration._assets.size).toBe(0) - - migration.createDocument(document, documentTitle) - - expect(migration._assets.get(id)?.config).toStrictEqual(expected) - }) - - it("slice's repeatable zone", ({ mock }) => { - const migration = prismic.createMigration() - - const { id, field, expected } = getField(mock) - - const slices: prismic.SliceZone = [ - { - id: "id", - slice_type: "slice_type", - slice_label: "slice_label", - primary: {}, - items: [{ field }], - }, - ] - - const document: prismic.PendingPrismicDocument = { - type: "type", - uid: "uid", - lang: "lang", - data: { slices }, - } - const documentTitle = "documentTitle" - - expect(migration._assets.size).toBe(0) - - migration.createDocument(document, documentTitle) - - expect(migration._assets.get(id)?.config).toStrictEqual(expected) - }) -}) diff --git a/test/migration-createDocumentFromPrismic.test.ts b/test/migration-createDocumentFromPrismic.test.ts new file mode 100644 index 00000000..d26af3ea --- /dev/null +++ b/test/migration-createDocumentFromPrismic.test.ts @@ -0,0 +1,256 @@ +import { describe, expect, it } from "vitest" + +import type { MockFactory } from "@prismicio/mock" + +import * as prismic from "../src" +import type { MigrationAssetConfig } from "../src/types/migration/Asset" +import { PrismicMigrationDocument } from "../src/types/migration/Document" + +it("creates a document from an existing Prismic document", (ctx) => { + const migration = prismic.createMigration() + + const document = ctx.mock.value.document() + const documentTitle = "documentTitle" + + const { type, uid, lang, tags, data } = document + + migration.createDocumentFromPrismic(document, documentTitle) + + expect(migration._documents[0]).toStrictEqual( + new PrismicMigrationDocument( + { type, uid, lang, tags, data }, + documentTitle, + { + dependencies: [], + originalPrismicDocument: document, + }, + ), + ) +}) + +describe.each<{ + fieldType: string + getField: (mock: MockFactory) => { + id: string + field: + | prismic.FilledImageFieldImage + | prismic.FilledLinkToMediaField + | prismic.RichTextField<"filled"> + expected: MigrationAssetConfig + } +}>([ + { + fieldType: "image", + getField: (mock) => { + const image = mock.value.image({ state: "filled" }) + + return { + id: image.id, + field: image, + expected: { + alt: image.alt || undefined, + credits: image.copyright || undefined, + file: image.url.split("?")[0], + filename: image.url.split("/").pop()!.split("?")[0], + id: image.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, + { + fieldType: "link to media", + getField: (mock) => { + const linkToMedia = mock.value.linkToMedia({ state: "filled" }) + + return { + id: linkToMedia.id, + field: linkToMedia, + expected: { + alt: undefined, + credits: undefined, + file: linkToMedia.url.split("?")[0], + filename: linkToMedia.name, + id: linkToMedia.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, + { + fieldType: "rich text (image)", + getField: (mock) => { + const image = mock.value.image({ state: "filled" }) + const richText: prismic.RichTextField<"filled"> = [ + { + type: prismic.RichTextNodeType.image, + ...image, + }, + ] + + return { + id: image.id, + field: richText, + expected: { + alt: image.alt || undefined, + credits: image.copyright || undefined, + file: image.url.split("?")[0], + filename: image.url.split("/").pop()!.split("?")[0], + id: image.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, + { + fieldType: "rich text (link to media span)", + getField: (mock) => { + const linkToMedia = mock.value.linkToMedia({ state: "filled" }) + const richText = mock.value.richText({ + state: "filled", + }) as prismic.RichTextField<"filled"> + + richText.push({ + type: prismic.RichTextNodeType.paragraph, + text: "lorem", + spans: [ + { + start: 0, + end: 5, + type: prismic.RichTextNodeType.hyperlink, + data: linkToMedia, + }, + ], + }) + + return { + id: linkToMedia.id, + field: richText, + expected: { + alt: undefined, + credits: undefined, + file: linkToMedia.url.split("?")[0], + filename: linkToMedia.name, + id: linkToMedia.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, + { + fieldType: "rich text (image's link to media)", + getField: (mock) => { + const image = mock.value.image({ state: "filled" }) + const linkToMedia = mock.value.linkToMedia({ state: "filled" }) + const richText: prismic.RichTextField<"filled"> = [ + { + type: prismic.RichTextNodeType.image, + linkTo: linkToMedia, + ...image, + }, + ] + + return { + id: linkToMedia.id, + field: richText, + expected: { + alt: undefined, + credits: undefined, + file: linkToMedia.url.split("?")[0], + filename: linkToMedia.name, + id: linkToMedia.id, + notes: undefined, + tags: undefined, + }, + } + }, + }, +])("extracts assets from image fields ($fieldType)", ({ getField }) => { + it("regular fields", ({ mock }) => { + const migration = prismic.createMigration() + + const { id, field, expected } = getField(mock) + + const document: prismic.PrismicDocument = mock.value.document() + document.data = { field } + const documentTitle = "documentTitle" + + expect(migration._assets.size).toBe(0) + + migration.createDocumentFromPrismic(document, documentTitle) + + expect(migration._assets.get(id)?.config).toStrictEqual(expected) + }) + + it("group fields", ({ mock }) => { + const migration = prismic.createMigration() + + const { id, field, expected } = getField(mock) + + const document: prismic.PrismicDocument = mock.value.document() + document.data = { group: [{ field }] } + const documentTitle = "documentTitle" + + expect(migration._assets.size).toBe(0) + + migration.createDocumentFromPrismic(document, documentTitle) + + expect(migration._assets.get(id)?.config).toStrictEqual(expected) + }) + + it("slice's primary zone", ({ mock }) => { + const migration = prismic.createMigration() + + const { id, field, expected } = getField(mock) + + const slices: prismic.SliceZone = [ + { + id: "id", + slice_type: "slice_type", + slice_label: "slice_label", + primary: { field }, + items: [], + }, + ] + + const document: prismic.PrismicDocument = mock.value.document() + document.data = { slices } + const documentTitle = "documentTitle" + + expect(migration._assets.size).toBe(0) + + migration.createDocumentFromPrismic(document, documentTitle) + + expect(migration._assets.get(id)?.config).toStrictEqual(expected) + }) + + it("slice's repeatable zone", ({ mock }) => { + const migration = prismic.createMigration() + + const { id, field, expected } = getField(mock) + + const slices: prismic.SliceZone = [ + { + id: "id", + slice_type: "slice_type", + slice_label: "slice_label", + primary: {}, + items: [{ field }], + }, + ] + + const document: prismic.PrismicDocument = mock.value.document() + document.data = { slices } + const documentTitle = "documentTitle" + + expect(migration._assets.size).toBe(0) + + migration.createDocumentFromPrismic(document, documentTitle) + + expect(migration._assets.get(id)?.config).toStrictEqual(expected) + }) +}) diff --git a/test/writeClient-getAssets.test.ts b/test/writeClient-getAssets.test.ts deleted file mode 100644 index 34186fe4..00000000 --- a/test/writeClient-getAssets.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { it as _it, expect } from "vitest" - -import { createTestWriteClient } from "./__testutils__/createWriteClient" -import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" - -import { ForbiddenError } from "../src" -import { AssetType } from "../src/types/api/asset/asset" - -// Skip test on Node 16 (FormData support) -const isNode16 = process.version.startsWith("v16") -const it = _it.skipIf(isNode16) - -it.concurrent("get assets", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2], - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets() - - expect(results).toStrictEqual(assetsDatabase[0]) -}) - -it.concurrent("supports `pageSize` parameter", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2], - }) - - ctx.server.events.on("request:start", (req) => { - if (req.url.hostname.startsWith(client.repositoryName)) { - ctx.expect(req.url.searchParams.get("pageSize")).toBe("10") - } - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets({ - pageSize: 10, - }) - - ctx.expect(results).toStrictEqual(assetsDatabase[0]) - ctx.expect.assertions(2) -}) - -it.concurrent("supports `cursor` parameter", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2, 2], - }) - - ctx.server.events.on("request:start", (req) => { - if (req.url.hostname.startsWith(client.repositoryName)) { - ctx.expect(req.url.searchParams.get("cursor")).toBe("1") - } - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets({ cursor: "1" }) - - ctx.expect(results).toStrictEqual(assetsDatabase[1]) - ctx.expect.assertions(2) -}) - -it.concurrent.skip("supports `assetType` parameter", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2], - }) - - ctx.server.events.on("request:start", (req) => { - if (req.url.hostname.startsWith(client.repositoryName)) { - ctx.expect(req.url.searchParams.get("assetType")).toBe(AssetType.Image) - } - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets({ assetType: AssetType.Image }) - - ctx.expect(results).toStrictEqual(assetsDatabase[0]) - ctx.expect.assertions(2) -}) - -it.concurrent.skip("supports `keyword` parameter", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2], - }) - - ctx.server.events.on("request:start", (req) => { - if (req.url.hostname.startsWith(client.repositoryName)) { - ctx.expect(req.url.searchParams.get("keyword")).toBe("foo") - } - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets({ keyword: "foo" }) - - ctx.expect(results).toStrictEqual(assetsDatabase[0]) - ctx.expect.assertions(2) -}) - -it.concurrent.skip("supports `ids` parameter", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2], - }) - - const ids = ["foo", "bar"] - - ctx.server.events.on("request:start", (req) => { - if (req.url.hostname.startsWith(client.repositoryName)) { - ctx.expect(req.url.searchParams.getAll("ids")).toStrictEqual(ids) - } - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets({ ids }) - - ctx.expect(results).toStrictEqual(assetsDatabase[0]) - ctx.expect.assertions(2) -}) - -it.concurrent.skip("supports `tags` parameter", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2], - existingTags: [ - { - id: "00000000-4444-4444-4444-121212121212", - name: "foo", - created_at: 0, - last_modified: 0, - }, - { - id: "10000000-4444-4444-4444-121212121212", - name: "bar", - created_at: 0, - last_modified: 0, - }, - ], - }) - - ctx.server.events.on("request:start", (req) => { - if (req.url.hostname.startsWith(client.repositoryName)) { - ctx - .expect(req.url.searchParams.getAll("tags")) - .toStrictEqual([ - "00000000-4444-4444-4444-121212121212", - "10000000-4444-4444-4444-121212121212", - ]) - } - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets({ tags: ["foo", "bar"] }) - - ctx.expect(results).toStrictEqual(assetsDatabase[0]) - ctx.expect.assertions(2) -}) - -it.concurrent.skip("supports `tags` parameter (missing)", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2], - existingTags: [ - { - id: "00000000-4444-4444-4444-121212121212", - name: "foo", - created_at: 0, - last_modified: 0, - }, - ], - }) - - ctx.server.events.on("request:start", (req) => { - if (req.url.hostname.startsWith(client.repositoryName)) { - ctx - .expect(req.url.searchParams.getAll("tags")) - .toStrictEqual(["00000000-4444-4444-4444-121212121212"]) - } - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets({ tags: ["foo", "bar"] }) - - ctx.expect(results).toStrictEqual(assetsDatabase[0]) - ctx.expect.assertions(2) -}) - -it.concurrent("returns `next` when next `cursor` is available", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - mockPrismicAssetAPI({ ctx, client }) - - // @ts-expect-error - testing purposes - const { next: next1 } = await client.getAssets() - - ctx.expect(next1).toBeUndefined() - - mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2, 2], - }) - - // @ts-expect-error - testing purposes - const { next: next2 } = await client.getAssets() - - ctx.expect(next2).toBeInstanceOf(Function) - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2, 2], - }) - - ctx.server.events.on("request:start", (req) => { - if (req.url.hostname.startsWith(client.repositoryName)) { - ctx.expect(req.url.searchParams.get("cursor")).toBe("1") - } - }) - - const { results } = await next2!() - ctx.expect(results).toStrictEqual(assetsDatabase[1]) - ctx.expect.assertions(4) -}) - -it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - mockPrismicAssetAPI({ ctx, client, writeToken: "invalid" }) - - // @ts-expect-error - testing purposes - await expect(() => client.getAssets()).rejects.toThrow(ForbiddenError) -}) - -it.concurrent("is abortable with an AbortController", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const controller = new AbortController() - controller.abort() - - mockPrismicAssetAPI({ ctx, client }) - - await expect(() => - // @ts-expect-error - testing purposes - client.getAssets({ - fetchOptions: { signal: controller.signal }, - }), - ).rejects.toThrow(/aborted/i) -}) - -it.concurrent("supports custom headers", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - const headers = { "x-custom": "foo" } - - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [2], - requiredHeaders: headers, - }) - - // @ts-expect-error - testing purposes - const { results } = await client.getAssets({ fetchOptions: { headers } }) - - ctx.expect(results).toStrictEqual(assetsDatabase[0]) - ctx.expect.assertions(2) -}) diff --git a/test/writeClient-migrate-assets.test.ts b/test/writeClient-migrate-assets.test.ts index 5c9ed198..e9ac1a0c 100644 --- a/test/writeClient-migrate-assets.test.ts +++ b/test/writeClient-migrate-assets.test.ts @@ -17,83 +17,6 @@ const isNode16 = process.version.startsWith("v16") const isNode18 = process.version.startsWith("v18") const it = _it.skipIf(isNode16 || isNode18) -it.concurrent("discovers existing assets", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client, existingAssets: [2] }) - mockPrismicMigrationAPI({ ctx, client }) - - const migration = prismic.createMigration() - - const reporter = vi.fn() - - await client.migrate(migration, { reporter }) - - expect(reporter).toHaveBeenCalledWith({ - type: "assets:existing", - data: { - existing: 2, - }, - }) -}) - -it.concurrent( - "discovers existing assets and crawl pages if any", - async (ctx) => { - const client = createTestWriteClient({ ctx }) - - mockPrismicRestAPIV2({ ctx }) - mockPrismicAssetAPI({ ctx, client, existingAssets: [2, 3] }) - mockPrismicMigrationAPI({ ctx, client }) - - const migration = prismic.createMigration() - - const reporter = vi.fn() - - await client.migrate(migration, { reporter }) - - expect(reporter).toHaveBeenCalledWith({ - type: "assets:existing", - data: { - existing: 5, - }, - }) - }, -) - -it.concurrent("skips creating existing assets", async (ctx) => { - const client = createTestWriteClient({ ctx }) - - mockPrismicRestAPIV2({ ctx }) - const { assetsDatabase } = mockPrismicAssetAPI({ - ctx, - client, - existingAssets: [1], - }) - mockPrismicMigrationAPI({ ctx, client }) - - const migration = prismic.createMigration() - - const asset = assetsDatabase[0][0] - const migrationAsset = migration.createAsset(asset) - - const reporter = vi.fn() - - await client.migrate(migration, { reporter }) - - expect(reporter).toHaveBeenCalledWith({ - type: "assets:skipping", - data: { - reason: "already exists", - current: 1, - remaining: 0, - total: 1, - asset: migrationAsset, - }, - }) -}) - it.concurrent("creates new asset from string file data", async (ctx) => { const client = createTestWriteClient({ ctx }) diff --git a/test/writeClient-migrate-patch-contentRelationship.test.ts b/test/writeClient-migrate-patch-contentRelationship.test.ts index 73a5def5..78491393 100644 --- a/test/writeClient-migrate-patch-contentRelationship.test.ts +++ b/test/writeClient-migrate-patch-contentRelationship.test.ts @@ -33,14 +33,6 @@ testMigrationFieldPatching("patches link fields", { ), ) }, - otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { - const contentRelationship = ctx.mock.value.link({ type: "Document" }) - // `migrationDocuments` contains documents from "another repository" - contentRelationship.id = - migrationDocuments.otherRepository.originalPrismicDocument!.id - - return contentRelationship - }, brokenLink: () => { return { type: "Document", isBroken: true } }, @@ -60,3 +52,18 @@ testMigrationFieldPatching("patches link fields", { }, ], }) + +testMigrationFieldPatching( + "patches link fields", + { + otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { + const contentRelationship = ctx.mock.value.link({ type: "Document" }) + // `migrationDocuments` contains documents from "another repository" + contentRelationship.id = + migrationDocuments.otherRepository.originalPrismicDocument!.id + + return contentRelationship + }, + }, + { mode: "fromPrismic" }, +) diff --git a/test/writeClient-migrate-patch-image.test.ts b/test/writeClient-migrate-patch-image.test.ts index f0897927..e0bdc7fe 100644 --- a/test/writeClient-migrate-patch-image.test.ts +++ b/test/writeClient-migrate-patch-image.test.ts @@ -1,61 +1,69 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" +import { assetToImage } from "../src/types/migration/Asset" + testMigrationFieldPatching("patches image fields", { new: ({ migration }) => migration.createAsset("foo", "foo.png"), - existing: ({ migration, existingAssets }) => - migration.createAsset(existingAssets[0]), - otherRepository: ({ ctx, mockedDomain }) => { - return { - ...ctx.mock.value.image({ state: "filled" }), - id: "foo-id", - url: `${mockedDomain}/foo.png`, - } - }, - otherRepositoryWithThumbnails: ({ ctx, mockedDomain }) => { - const image = ctx.mock.value.image({ state: "filled" }) - const id = "foo-id" + existing: ({ existingAssets }) => assetToImage(existingAssets[0]), +}) + +testMigrationFieldPatching( + "patches image fields", + { + otherRepository: ({ ctx, mockedDomain }) => { + return { + ...ctx.mock.value.image({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + } + }, + otherRepositoryWithThumbnails: ({ ctx, mockedDomain }) => { + const image = ctx.mock.value.image({ state: "filled" }) + const id = "foo-id" - return { - ...image, - id, - url: `${mockedDomain}/foo.png?some=query`, - square: { + return { ...image, id, - url: `${mockedDomain}/foo.png?some=other&query=params`, - }, - } - }, - otherRepositoryWithThumbnailsNoAlt: ({ ctx, mockedDomain }) => { - const image = ctx.mock.value.image({ state: "filled" }) - image.alt = null - const id = "foo-id" + url: `${mockedDomain}/foo.png?some=query`, + square: { + ...image, + id, + url: `${mockedDomain}/foo.png?some=other&query=params`, + }, + } + }, + otherRepositoryWithThumbnailsNoAlt: ({ ctx, mockedDomain }) => { + const image = ctx.mock.value.image({ state: "filled" }) + image.alt = null + const id = "foo-id" - return { - ...image, - id, - url: `${mockedDomain}/foo.png?some=query`, - square: { + return { ...image, id, - url: `${mockedDomain}/foo.png?some=other&query=params`, - }, - } - }, - otherRepositoryWithTypeThumbnail: ({ ctx, mockedDomain }) => { - const image = ctx.mock.value.image({ state: "filled" }) - const id = "foo-id" + url: `${mockedDomain}/foo.png?some=query`, + square: { + ...image, + id, + url: `${mockedDomain}/foo.png?some=other&query=params`, + }, + } + }, + otherRepositoryWithTypeThumbnail: ({ ctx, mockedDomain }) => { + const image = ctx.mock.value.image({ state: "filled" }) + const id = "foo-id" - return { - ...image, - id, - url: `${mockedDomain}/foo.png?some=query`, - type: { + return { ...image, id, - url: `${mockedDomain}/foo.png?some=other&query=params`, - }, - } + url: `${mockedDomain}/foo.png?some=query`, + type: { + ...image, + id, + url: `${mockedDomain}/foo.png?some=other&query=params`, + }, + } + }, + otherRepositoryEmpty: ({ ctx }) => ctx.mock.value.image({ state: "empty" }), }, - otherRepositoryEmpty: ({ ctx }) => ctx.mock.value.image({ state: "empty" }), -}) + { mode: "fromPrismic" }, +) diff --git a/test/writeClient-migrate-patch-linkToMedia.test.ts b/test/writeClient-migrate-patch-linkToMedia.test.ts index 8a19025c..5f01837a 100644 --- a/test/writeClient-migrate-patch-linkToMedia.test.ts +++ b/test/writeClient-migrate-patch-linkToMedia.test.ts @@ -2,27 +2,20 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPa import { RichTextNodeType } from "../src" import { AssetType } from "../src/types/api/asset/asset" +import { assetToLinkToMedia } from "../src/types/migration/Asset" testMigrationFieldPatching("patches link to media fields", { new: ({ migration }) => migration.createAsset("foo", "foo.png").asLinkToMedia(), - existing: ({ migration, existingAssets }) => - migration.createAsset(existingAssets[0]).asLinkToMedia(), - existingNonImage: ({ migration, existingAssets }) => { + existing: ({ existingAssets }) => assetToLinkToMedia(existingAssets[0]), + existingNonImage: ({ existingAssets }) => { existingAssets[0].filename = "foo.pdf" existingAssets[0].extension = "pdf" existingAssets[0].kind = AssetType.Document existingAssets[0].width = undefined existingAssets[0].height = undefined - return migration.createAsset(existingAssets[0]).asLinkToMedia() - }, - otherRepository: ({ ctx, mockedDomain }) => { - return { - ...ctx.mock.value.linkToMedia({ state: "filled" }), - id: "foo-id", - url: `${mockedDomain}/foo.png`, - } + return assetToLinkToMedia(existingAssets[0]) }, richTextNew: ({ migration }) => [ { @@ -39,7 +32,7 @@ testMigrationFieldPatching("patches link to media fields", { ], }, ], - richTextExisting: ({ migration, existingAssets }) => [ + richTextExisting: ({ existingAssets }) => [ { type: RichTextNodeType.paragraph, text: "lorem", @@ -49,28 +42,42 @@ testMigrationFieldPatching("patches link to media fields", { type: RichTextNodeType.hyperlink, start: 0, end: 5, - data: migration.createAsset(existingAssets[0]).asLinkToMedia(), - }, - ], - }, - ], - richTextOtherRepository: ({ ctx, mockedDomain }) => [ - { - type: RichTextNodeType.paragraph, - text: "lorem", - spans: [ - { type: RichTextNodeType.strong, start: 0, end: 5 }, - { - type: RichTextNodeType.hyperlink, - start: 0, - end: 5, - data: { - ...ctx.mock.value.linkToMedia({ state: "filled" }), - id: "foo-id", - url: `${mockedDomain}/foo.png`, - }, + data: assetToLinkToMedia(existingAssets[0]), }, ], }, ], }) + +testMigrationFieldPatching( + "patches link to media fields", + { + otherRepository: ({ ctx, mockedDomain }) => { + return { + ...ctx.mock.value.linkToMedia({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + } + }, + richTextOtherRepository: ({ ctx, mockedDomain }) => [ + { + type: RichTextNodeType.paragraph, + text: "lorem", + spans: [ + { type: RichTextNodeType.strong, start: 0, end: 5 }, + { + type: RichTextNodeType.hyperlink, + start: 0, + end: 5, + data: { + ...ctx.mock.value.linkToMedia({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + }, + }, + ], + }, + ], + }, + { mode: "fromPrismic" }, +) diff --git a/test/writeClient-migrate-patch-rtImageNode.test.ts b/test/writeClient-migrate-patch-rtImageNode.test.ts index 47d9ace3..b4138805 100644 --- a/test/writeClient-migrate-patch-rtImageNode.test.ts +++ b/test/writeClient-migrate-patch-rtImageNode.test.ts @@ -1,6 +1,7 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" import { RichTextNodeType } from "../src" +import { assetToImage } from "../src/types/migration/Asset" import { MigrationContentRelationship } from "../src/types/migration/ContentRelationship" testMigrationFieldPatching("patches rich text image nodes", { @@ -8,17 +9,11 @@ testMigrationFieldPatching("patches rich text image nodes", { ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), migration.createAsset("foo", "foo.png").asRTImageNode(), ], - existing: ({ ctx, migration, existingAssets }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - migration.createAsset(existingAssets[0]).asRTImageNode(), - ], - otherRepository: ({ ctx, mockedDomain }) => [ + existing: ({ ctx, existingAssets }) => [ ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), { type: RichTextNodeType.image, - ...ctx.mock.value.image({ state: "filled" }), - id: "foo-id", - url: `${mockedDomain}/foo.png`, + ...assetToImage(existingAssets[0]), }, ], newLinkTo: ({ ctx, migration, existingDocuments }) => [ @@ -27,19 +22,35 @@ testMigrationFieldPatching("patches rich text image nodes", { .createAsset("foo", "foo.png") .asRTImageNode(new MigrationContentRelationship(existingDocuments[0])), ], - otherRepositoryLinkTo: ({ - ctx, - migration, - mockedDomain, - existingDocuments, - }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - { - type: RichTextNodeType.image, - ...ctx.mock.value.image({ state: "filled" }), - id: "foo-id", - url: `${mockedDomain}/foo.png`, - linkTo: migration.createContentRelationship(existingDocuments[0]), - }, - ], }) + +testMigrationFieldPatching( + "patches rich text image nodes", + { + otherRepository: ({ ctx, mockedDomain }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + ...ctx.mock.value.image({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + }, + ], + otherRepositoryLinkTo: ({ + ctx, + migration, + mockedDomain, + existingDocuments, + }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + ...ctx.mock.value.image({ state: "filled" }), + id: "foo-id", + url: `${mockedDomain}/foo.png`, + linkTo: migration.createContentRelationship(existingDocuments[0]), + }, + ], + }, + { mode: "fromPrismic" }, +) From db4e8a733223be91cf36826560dc273ea3a742d4 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 16 Sep 2024 10:38:05 +0200 Subject: [PATCH 53/61] refactor: remove `dependencies` concept --- src/Migration.ts | 14 +++--- src/WriteClient.ts | 14 ++++-- src/lib/prepareMigrationDocumentData.ts | 34 ++++++------- src/lib/resolveMigrationDocumentData.ts | 49 +++++++++++++++++++ src/types/migration/Asset.ts | 28 +++++------ src/types/migration/ContentRelationship.ts | 6 ++- src/types/migration/Document.ts | 32 ++---------- src/types/migration/Field.ts | 33 +++++-------- test/migration-createDocument.test.ts | 2 +- ...igration-createDocumentFromPrismic.test.ts | 1 - test/migration-updateDocument.test.ts | 2 +- 11 files changed, 114 insertions(+), 101 deletions(-) create mode 100644 src/lib/resolveMigrationDocumentData.ts diff --git a/src/Migration.ts b/src/Migration.ts index 0fe7d838..dce1be98 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -245,14 +245,14 @@ export class Migration { masterLanguageDocument?: MigrationContentRelationship }, ): PrismicMigrationDocument> { - const { record: data, dependencies } = prepareMigrationDocumentData( + const data = prepareMigrationDocumentData( document.data!, this.createAsset.bind(this), ) const doc = new PrismicMigrationDocument< ExtractDocumentType - >({ ...document, data }, title, { ...options, dependencies }) + >({ ...document, data }, title, options) this._documents.push(doc) @@ -279,14 +279,14 @@ export class Migration { document: ExtractDocumentType, TType>, title: string, ): PrismicMigrationDocument> { - const { record: data, dependencies } = prepareMigrationDocumentData( + const data = prepareMigrationDocumentData( document.data, this.createAsset.bind(this), ) const doc = new PrismicMigrationDocument< ExtractDocumentType - >({ ...document, data }, title, { dependencies }) + >({ ...document, data }, title) this._documents.push(doc) @@ -312,7 +312,7 @@ export class Migration { document: ExtractDocumentType, TType>, title: string, ): PrismicMigrationDocument> { - const { record: data, dependencies } = prepareMigrationDocumentData( + const data = prepareMigrationDocumentData( document.data, this.createAsset.bind(this), true, @@ -324,12 +324,12 @@ export class Migration { lang: document.lang, uid: document.uid, tags: document.tags, - data: data, + data, } as unknown as PendingPrismicDocument< ExtractDocumentType >, title, - { originalPrismicDocument: document, dependencies }, + { originalPrismicDocument: document }, ) this._documents.push(doc) diff --git a/src/WriteClient.ts b/src/WriteClient.ts index d1c46adc..5d29300f 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -1,5 +1,6 @@ import { devMsg } from "./lib/devMsg" import { pLimit } from "./lib/pLimit" +import { resolveMigrationDocumentData } from "./lib/resolveMigrationDocumentData" import type { Asset, @@ -432,8 +433,7 @@ export class WriteClient< if (doc.masterLanguageDocument) { const link = doc.masterLanguageDocument - await link._resolve(migration) - masterLanguageDocumentID = link._field?.id + masterLanguageDocumentID = (await link._resolve(migration))?.id } else if (doc.originalPrismicDocument) { masterLanguageDocumentID = doc.originalPrismicDocument.alternate_languages.find( @@ -488,13 +488,17 @@ export class WriteClient< }, }) - await doc._resolve(migration) - await this.updateDocument( doc.document.id!, // We need to forward again document name and tags to update them // in case the document already existed during the previous step. - doc.document, + { + ...doc.document, + data: await resolveMigrationDocumentData( + doc.document.data, + migration, + ), + }, fetchParams, ) } diff --git a/src/lib/prepareMigrationDocumentData.ts b/src/lib/prepareMigrationDocumentData.ts index 89dbcb18..0d0730e0 100644 --- a/src/lib/prepareMigrationDocumentData.ts +++ b/src/lib/prepareMigrationDocumentData.ts @@ -11,8 +11,7 @@ import type { FilledLinkToMediaField } from "../types/value/linkToMedia" import * as is from "./isValue" /** - * Replaces existings assets and links in a record of Prismic fields and get all - * dependencies to them. + * Replaces existings assets and links in a record of Prismic fields. * * @typeParam TRecord - Record of values to work with. * @@ -21,8 +20,7 @@ import * as is from "./isValue" * @param fromPrismic - Whether the record is from another Prismic repository or * not. * - * @returns An object containing the record with replaced assets and links and a - * list of dependencies found and/or created. + * @returns An object containing the record with replaced assets and links. */ export const prepareMigrationDocumentData = < // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -33,16 +31,14 @@ export const prepareMigrationDocumentData = < asset: FilledImageFieldImage | FilledLinkToMediaField, ) => MigrationImage, fromPrismic?: boolean, -): { record: TRecord; dependencies: MigrationField[] } => { +): TRecord => { const result = {} as Record - const dependencies: MigrationField[] = [] for (const key in record) { const field: unknown = record[key] if (field instanceof MigrationField) { // Existing migration fields - dependencies.push(field) result[key] = field } else if (fromPrismic && is.linkToMedia(field)) { // Link to media @@ -50,7 +46,6 @@ export const prepareMigrationDocumentData = < // @ts-expect-error - Future-proofing for link text const linkToMedia = onAsset(field).asLinkToMedia(field.text) - dependencies.push(linkToMedia) result[key] = linkToMedia } else if (fromPrismic && is.rtImageNode(field)) { // Rich text image nodes @@ -62,13 +57,12 @@ export const prepareMigrationDocumentData = < { linkTo: field.linkTo }, onAsset, fromPrismic, - ).record.linkTo as + ).linkTo as | MigrationContentRelationship | MigrationLinkToMedia | FilledLinkToWebField } - dependencies.push(rtImageNode) result[key] = rtImageNode } else if (fromPrismic && is.image(field)) { // Image fields @@ -91,7 +85,6 @@ export const prepareMigrationDocumentData = < } } - dependencies.push(image) result[key] = image } else if (fromPrismic && is.contentRelationship(field)) { // Content relationships @@ -102,27 +95,30 @@ export const prepareMigrationDocumentData = < field.text, ) - dependencies.push(contentRelationship) result[key] = contentRelationship } else if (Array.isArray(field)) { // Traverse arrays const array = [] for (const item of field) { - const { record, dependencies: itemDependencies } = - prepareMigrationDocumentData({ item }, onAsset, fromPrismic) + const record = prepareMigrationDocumentData( + { item }, + onAsset, + fromPrismic, + ) array.push(record.item) - dependencies.push(...itemDependencies) } result[key] = array } else if (field && typeof field === "object") { // Traverse objects - const { record, dependencies: fieldDependencies } = - prepareMigrationDocumentData({ ...field }, onAsset, fromPrismic) + const record = prepareMigrationDocumentData( + { ...field }, + onAsset, + fromPrismic, + ) - dependencies.push(...fieldDependencies) result[key] = record } else { // Primitives @@ -130,5 +126,5 @@ export const prepareMigrationDocumentData = < } } - return { record: result as TRecord, dependencies } + return result as TRecord } diff --git a/src/lib/resolveMigrationDocumentData.ts b/src/lib/resolveMigrationDocumentData.ts new file mode 100644 index 00000000..adbc4bf7 --- /dev/null +++ b/src/lib/resolveMigrationDocumentData.ts @@ -0,0 +1,49 @@ +import { MigrationField } from "../types/migration/Field" + +import type { Migration } from "../Migration" + +export async function resolveMigrationDocumentData( + input: unknown, + migration: Migration, +): Promise { + if (input === null) { + return input + } + + if (input instanceof MigrationField) { + return input._resolve(migration) + } + + // if (input instanceof PrismicMigrationDocument || isPrismicDocument(input)) { + // return resolveMigrationContentRelationship(input) + // } + + // if (typeof input === "function") { + // return await resolveMigrationDocumentData(await input()) + // } + + if (Array.isArray(input)) { + const res = [] + + for (const element of input) { + res.push(await resolveMigrationDocumentData(element, migration)) + } + + return res + } + + if (typeof input === "object") { + const res: Record = {} + + for (const key in input) { + res[key] = await resolveMigrationDocumentData( + input[key as keyof typeof input], + migration, + ) + } + + return res + } + + return input +} diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index a3eaafd9..3d4d3856 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -225,20 +225,22 @@ export class MigrationImage extends MigrationAsset { return this } - async _resolve(migration: Migration): Promise { + async _resolve( + migration: Migration, + ): Promise { const asset = migration._assets.get(this.config.id)?.asset if (asset) { - this._field = assetToImage(asset, this._initialField) + const field = assetToImage(asset, this._initialField) for (const name in this.#thumbnails) { - await this.#thumbnails[name]._resolve(migration) - - const thumbnail = this.#thumbnails[name]._field + const thumbnail = await this.#thumbnails[name]._resolve(migration) if (thumbnail) { - ;(this._field as ImageField)[name] = thumbnail + ;(field as ImageField)[name] = thumbnail } } + + return field } } } @@ -274,11 +276,11 @@ export class MigrationLinkToMedia extends MigrationAsset< this.text = text } - _resolve(migration: Migration): void { + _resolve(migration: Migration): LinkToMediaField<"filled"> | undefined { const asset = migration._assets.get(this.config.id)?.asset if (asset) { - this._field = assetToLinkToMedia(asset, this.text) + return assetToLinkToMedia(asset, this.text) } } } @@ -318,20 +320,16 @@ export class MigrationRTImageNode extends MigrationAsset { this.linkTo = linkTo } - async _resolve(migration: Migration): Promise { + async _resolve(migration: Migration): Promise { const asset = migration._assets.get(this.config.id)?.asset - if (this.linkTo instanceof MigrationField) { - await this.linkTo._resolve(migration) - } - if (asset) { - this._field = { + return { ...assetToImage(asset, this._initialField), type: RichTextNodeType.image, linkTo: this.linkTo instanceof MigrationField - ? this.linkTo._field + ? await this.linkTo._resolve(migration) : this.linkTo, } } diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts index b3357da7..946da3e6 100644 --- a/src/types/migration/ContentRelationship.ts +++ b/src/types/migration/ContentRelationship.ts @@ -68,7 +68,9 @@ export class MigrationContentRelationship extends MigrationField { + async _resolve( + migration: Migration, + ): Promise { const config = typeof this.#unresolvedConfig === "function" ? await this.#unresolvedConfig() @@ -82,7 +84,7 @@ export class MigrationContentRelationship extends MigrationField - /** - * Asset and content relationship fields that this document depends on. - */ - #dependencies: MigrationField[] - /** * Creates a Prismic migration document instance. * @@ -132,38 +124,20 @@ export class PrismicMigrationDocument< * @param title - The name of the document displayed in the editor. * @param options - Parameters to create/update the document with on the * Migration API. - * @param dependencies - Asset and content relationship fields that this - * document depends on. * * @returns A Prismic migration document instance. */ constructor( document: MigrationDocument, title: string, - options: { + options?: { masterLanguageDocument?: MigrationContentRelationship originalPrismicDocument?: ExistingPrismicDocument - dependencies: MigrationField[] }, ) { this.document = document this.title = title - this.masterLanguageDocument = options.masterLanguageDocument - this.originalPrismicDocument = options.originalPrismicDocument - this.#dependencies = options.dependencies - } - - /** - * Resolves each dependencies of the document with the provided maps. - * - * @param migration - A migration instance with documents and assets to use - * for resolving the field's value - * - * @internal - */ - async _resolve(migration: Migration): Promise { - for (const dependency of this.#dependencies) { - await dependency._resolve(migration) - } + this.masterLanguageDocument = options?.masterLanguageDocument + this.originalPrismicDocument = options?.originalPrismicDocument } } diff --git a/src/types/migration/Field.ts b/src/types/migration/Field.ts index 2c098747..a53984ec 100644 --- a/src/types/migration/Field.ts +++ b/src/types/migration/Field.ts @@ -7,7 +7,12 @@ import type { RTBlockNode, RTInlineNode } from "../value/richText" /** * Interface for migration fields that can be resolved. */ -interface Resolvable { +interface Resolvable< + TField extends AnyRegularField | RTBlockNode | RTInlineNode = + | AnyRegularField + | RTBlockNode + | RTInlineNode, +> { /** * Resolves the field's value with the provided maps. * @@ -16,7 +21,9 @@ interface Resolvable { * * @internal */ - _resolve(migration: Migration): Promise | void + _resolve( + migration: Migration, + ): Promise | TField | undefined } /** @@ -33,13 +40,6 @@ export abstract class MigrationField< TInitialField = TField, > implements Resolvable { - /** - * The resolved field value. - * - * @internal - */ - _field?: TField - /** * The initial field value this migration field was created with. * @@ -58,16 +58,7 @@ export abstract class MigrationField< this._initialField = initialField } - /** - * Prepares the field to be stringified with {@link JSON.stringify} - * - * @returns The value of the field to be stringified. - * - * @internal - */ - toJSON(): TField | undefined { - return this._field - } - - abstract _resolve(migration: Migration): Promise | void + abstract _resolve( + migration: Migration, + ): Promise | TField | undefined } diff --git a/test/migration-createDocument.test.ts b/test/migration-createDocument.test.ts index c1199258..3ee412c4 100644 --- a/test/migration-createDocument.test.ts +++ b/test/migration-createDocument.test.ts @@ -17,6 +17,6 @@ it("creates a document", () => { migration.createDocument(document, documentTitle) expect(migration._documents[0]).toStrictEqual( - new PrismicMigrationDocument(document, documentTitle, { dependencies: [] }), + new PrismicMigrationDocument(document, documentTitle), ) }) diff --git a/test/migration-createDocumentFromPrismic.test.ts b/test/migration-createDocumentFromPrismic.test.ts index d26af3ea..d453f022 100644 --- a/test/migration-createDocumentFromPrismic.test.ts +++ b/test/migration-createDocumentFromPrismic.test.ts @@ -21,7 +21,6 @@ it("creates a document from an existing Prismic document", (ctx) => { { type, uid, lang, tags, data }, documentTitle, { - dependencies: [], originalPrismicDocument: document, }, ), diff --git a/test/migration-updateDocument.test.ts b/test/migration-updateDocument.test.ts index ea93fcdb..8c553c04 100644 --- a/test/migration-updateDocument.test.ts +++ b/test/migration-updateDocument.test.ts @@ -12,6 +12,6 @@ it("updates a document", (ctx) => { migration.updateDocument(document, documentTitle) expect(migration._documents[0]).toStrictEqual( - new PrismicMigrationDocument(document, documentTitle, { dependencies: [] }), + new PrismicMigrationDocument(document, documentTitle), ) }) From 3577499354e019bf1d62dd827ed1f74f9938cdd2 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 16 Sep 2024 11:49:29 +0200 Subject: [PATCH 54/61] refactor: remove `MigrationContentRelationship` class --- src/Migration.ts | 130 +++++--- src/WriteClient.ts | 10 +- src/lib/isValue.ts | 37 ++- src/lib/prepareMigrationDocumentData.ts | 130 -------- src/lib/resolveMigrationDocumentData.ts | 43 ++- src/types/migration/Asset.ts | 17 +- src/types/migration/ContentRelationship.ts | 101 +----- src/types/migration/Document.ts | 2 +- ...ate-patch-contentRelationship.test.ts.snap | 297 +----------------- ...ent-migrate-patch-rtImageNode.test.ts.snap | 76 ----- test/writeClient-migrate-documents.test.ts | 16 +- ...-migrate-patch-contentRelationship.test.ts | 38 ++- ...teClient-migrate-patch-rtImageNode.test.ts | 14 +- 13 files changed, 222 insertions(+), 689 deletions(-) delete mode 100644 src/lib/prepareMigrationDocumentData.ts diff --git a/src/Migration.ts b/src/Migration.ts index dce1be98..4f44ae6b 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -1,12 +1,14 @@ -import { prepareMigrationDocumentData } from "./lib/prepareMigrationDocumentData" +import * as is from "./lib/isValue" import { validateAssetMetadata } from "./lib/validateAssetMetadata" import type { Asset } from "./types/api/asset/asset" -import type { MigrationAssetConfig } from "./types/migration/Asset" +import type { + MigrationAssetConfig, + MigrationLinkToMedia, +} from "./types/migration/Asset" import type { MigrationAsset } from "./types/migration/Asset" import { MigrationImage } from "./types/migration/Asset" -import type { UnresolvedMigrationContentRelationshipConfig } from "./types/migration/ContentRelationship" -import { MigrationContentRelationship } from "./types/migration/ContentRelationship" +import type { MigrationContentRelationship } from "./types/migration/ContentRelationship" import { PrismicMigrationDocument } from "./types/migration/Document" import type { ExistingPrismicDocument, @@ -14,6 +16,7 @@ import type { } from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" import type { FilledImageFieldImage } from "./types/value/image" +import type { FilledLinkToWebField } from "./types/value/link" import type { FilledLinkToMediaField } from "./types/value/linkToMedia" /** @@ -245,14 +248,9 @@ export class Migration { masterLanguageDocument?: MigrationContentRelationship }, ): PrismicMigrationDocument> { - const data = prepareMigrationDocumentData( - document.data!, - this.createAsset.bind(this), - ) - const doc = new PrismicMigrationDocument< ExtractDocumentType - >({ ...document, data }, title, options) + >(document, title, options) this._documents.push(doc) @@ -279,14 +277,9 @@ export class Migration { document: ExtractDocumentType, TType>, title: string, ): PrismicMigrationDocument> { - const data = prepareMigrationDocumentData( - document.data, - this.createAsset.bind(this), - ) - const doc = new PrismicMigrationDocument< ExtractDocumentType - >({ ...document, data }, title) + >(document, title) this._documents.push(doc) @@ -312,22 +305,14 @@ export class Migration { document: ExtractDocumentType, TType>, title: string, ): PrismicMigrationDocument> { - const data = prepareMigrationDocumentData( - document.data, - this.createAsset.bind(this), - true, - ) - const doc = new PrismicMigrationDocument( - { + this.#migratePrismicDocumentData({ type: document.type, lang: document.lang, uid: document.uid, tags: document.tags, - data, - } as unknown as PendingPrismicDocument< - ExtractDocumentType - >, + data: document.data, + }) as PendingPrismicDocument>, title, { originalPrismicDocument: document }, ) @@ -337,24 +322,6 @@ export class Migration { return doc } - /** - * Creates a content relationship fields that can be used in documents to link - * to other documents. - * - * @param config - A Prismic document, a migration document instance, an - * existing content relationship field's content, or a function that returns - * one of the previous. - * @param text - Link text associated with the content relationship field. - * - * @returns A migration content relationship field instance. - */ - createContentRelationship( - config: UnresolvedMigrationContentRelationshipConfig, - text?: string, - ): MigrationContentRelationship { - return new MigrationContentRelationship(config, text, undefined) - } - /** * Queries a document from the migration instance with a specific UID and * custom type. @@ -423,6 +390,77 @@ export class Migration { ) } + #migratePrismicDocumentData(input: unknown): unknown { + if (is.filledContentRelationship(input)) { + if (input.isBroken) { + return { link_type: "Document", id: "__broken__", isBroken: true } + } + + return () => this.#getByOriginalID(input.id) + } + + if (is.filledLinkToMedia(input)) { + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + return this.createAsset(input).asLinkToMedia(input.text) + } + + if (is.rtImageNode(input)) { + // Rich text image nodes + const rtImageNode = this.createAsset(input).asRTImageNode() + + if (input.linkTo) { + // Node `linkTo` dependency is tracked internally + rtImageNode.linkTo = this.#migratePrismicDocumentData(input.linkTo) as + | MigrationContentRelationship + | MigrationLinkToMedia + | FilledLinkToWebField + } + + return rtImageNode + } + + if (is.filledImage(input)) { + const image = 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.addThumbnail(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. * @@ -441,7 +479,7 @@ export class Migration { * @returns The migration document instance for the original ID, if a matching * document is found. */ - getByOriginalID( + #getByOriginalID( id: string, ): | PrismicMigrationDocument> diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 5d29300f..32cce269 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -1,6 +1,9 @@ import { devMsg } from "./lib/devMsg" import { pLimit } from "./lib/pLimit" -import { resolveMigrationDocumentData } from "./lib/resolveMigrationDocumentData" +import { + resolveMigrationContentRelationship, + resolveMigrationDocumentData, +} from "./lib/resolveMigrationDocumentData" import type { Asset, @@ -431,9 +434,10 @@ export class WriteClient< // Resolve master language document ID for non-master locale documents let masterLanguageDocumentID: string | undefined if (doc.masterLanguageDocument) { - const link = doc.masterLanguageDocument + const masterLanguageDocument = + await resolveMigrationContentRelationship(doc.masterLanguageDocument) - masterLanguageDocumentID = (await link._resolve(migration))?.id + masterLanguageDocumentID = masterLanguageDocument?.id } else if (doc.originalPrismicDocument) { masterLanguageDocumentID = doc.originalPrismicDocument.alternate_languages.find( diff --git a/src/lib/isValue.ts b/src/lib/isValue.ts index 90b72cbc..c4b5bd11 100644 --- a/src/lib/isValue.ts +++ b/src/lib/isValue.ts @@ -30,7 +30,7 @@ type UnknownValue = * @internal * This is not an official helper function and it's only designed to work with internal processes. */ -export const linkToMedia = ( +export const filledLinkToMedia = ( value: UnknownValue, ): value is FilledLinkToMediaField => { if (value && typeof value === "object" && !("version" in value)) { @@ -96,7 +96,7 @@ const imageLike = ( * @internal * This is not an official helper function and it's only designed to work with internal processes. */ -export const image = ( +export const filledImage = ( value: UnknownValue, ): value is ImageField => { if ( @@ -145,7 +145,7 @@ export const rtImageNode = (value: UnknownValue): value is RTImageNode => { * @internal * This is not an official helper function and it's only designed to work with internal processes. */ -export const contentRelationship = ( +export const filledContentRelationship = ( value: UnknownValue, ): value is FilledContentRelationshipField => { if (value && typeof value === "object" && !("version" in value)) { @@ -163,3 +163,34 @@ export const contentRelationship = ( return false } + +/** + * Checks if a value is a Prismic document. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a Prismic document, `false` otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const prismicDocument = ( + value: UnknownValue, +): value is PrismicDocument => { + try { + return ( + typeof value === "object" && + value !== null && + "id" in value && + "href" in value && + typeof value.href === "string" && + new URL(value.href) && + "type" in value && + "lang" in value && + "tags" in value && + Array.isArray(value.tags) + ) + } catch { + return false + } +} diff --git a/src/lib/prepareMigrationDocumentData.ts b/src/lib/prepareMigrationDocumentData.ts deleted file mode 100644 index 0d0730e0..00000000 --- a/src/lib/prepareMigrationDocumentData.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { - MigrationImage, - MigrationLinkToMedia, -} from "../types/migration/Asset" -import { MigrationContentRelationship } from "../types/migration/ContentRelationship" -import { MigrationField } from "../types/migration/Field" -import type { FilledImageFieldImage } from "../types/value/image" -import type { FilledLinkToWebField } from "../types/value/link" -import type { FilledLinkToMediaField } from "../types/value/linkToMedia" - -import * as is from "./isValue" - -/** - * Replaces existings assets and links in a record of Prismic fields. - * - * @typeParam TRecord - Record of values to work with. - * - * @param record - Record of Prismic fields to work with. - * @param onAsset - Callback that is called for each asset found. - * @param fromPrismic - Whether the record is from another Prismic repository or - * not. - * - * @returns An object containing the record with replaced assets and links. - */ -export const prepareMigrationDocumentData = < - // eslint-disable-next-line @typescript-eslint/no-explicit-any - TRecord extends Record, ->( - record: TRecord, - onAsset: ( - asset: FilledImageFieldImage | FilledLinkToMediaField, - ) => MigrationImage, - fromPrismic?: boolean, -): TRecord => { - const result = {} as Record - - for (const key in record) { - const field: unknown = record[key] - - if (field instanceof MigrationField) { - // Existing migration fields - result[key] = field - } else if (fromPrismic && is.linkToMedia(field)) { - // Link to media - // TODO: Remove when link text PR is merged - // @ts-expect-error - Future-proofing for link text - const linkToMedia = onAsset(field).asLinkToMedia(field.text) - - result[key] = linkToMedia - } else if (fromPrismic && is.rtImageNode(field)) { - // Rich text image nodes - const rtImageNode = onAsset(field).asRTImageNode() - - if (field.linkTo) { - // Node `linkTo` dependency is tracked internally - rtImageNode.linkTo = prepareMigrationDocumentData( - { linkTo: field.linkTo }, - onAsset, - fromPrismic, - ).linkTo as - | MigrationContentRelationship - | MigrationLinkToMedia - | FilledLinkToWebField - } - - result[key] = rtImageNode - } else if (fromPrismic && is.image(field)) { - // Image fields - const image = onAsset(field).asImage() - - const { - id: _id, - url: _url, - dimensions: _dimensions, - edit: _edit, - alt: _alt, - copyright: _copyright, - ...thumbnails - } = field - - for (const name in thumbnails) { - if (is.image(thumbnails[name])) { - // Node thumbnails dependencies are tracked internally - image.addThumbnail(name, onAsset(thumbnails[name]).asImage()) - } - } - - result[key] = image - } else if (fromPrismic && is.contentRelationship(field)) { - // Content relationships - const contentRelationship = new MigrationContentRelationship( - field, - // TODO: Remove when link text PR is merged - // @ts-expect-error - Future-proofing for link text - field.text, - ) - - result[key] = contentRelationship - } else if (Array.isArray(field)) { - // Traverse arrays - const array = [] - - for (const item of field) { - const record = prepareMigrationDocumentData( - { item }, - onAsset, - fromPrismic, - ) - - array.push(record.item) - } - - result[key] = array - } else if (field && typeof field === "object") { - // Traverse objects - const record = prepareMigrationDocumentData( - { ...field }, - onAsset, - fromPrismic, - ) - - result[key] = record - } else { - // Primitives - result[key] = field - } - } - - return result as TRecord -} diff --git a/src/lib/resolveMigrationDocumentData.ts b/src/lib/resolveMigrationDocumentData.ts index adbc4bf7..7df8b8f0 100644 --- a/src/lib/resolveMigrationDocumentData.ts +++ b/src/lib/resolveMigrationDocumentData.ts @@ -1,26 +1,47 @@ +import type { + MigrationContentRelationship, + MigrationContentRelationshipField, +} from "../types/migration/ContentRelationship" +import { PrismicMigrationDocument } from "../types/migration/Document" import { MigrationField } from "../types/migration/Field" import type { Migration } from "../Migration" +import * as is from "./isValue" + +export async function resolveMigrationContentRelationship( + relation: MigrationContentRelationship, +): Promise { + if (typeof relation === "function") { + return resolveMigrationContentRelationship(await relation()) + } + + if (relation instanceof PrismicMigrationDocument) { + return relation.document.id + ? { link_type: "Document", id: relation.document.id } + : undefined + } + + if (relation) { + return { link_type: "Document", id: relation.id } + } +} + export async function resolveMigrationDocumentData( input: unknown, migration: Migration, ): Promise { - if (input === null) { - return input - } - if (input instanceof MigrationField) { return input._resolve(migration) } - // if (input instanceof PrismicMigrationDocument || isPrismicDocument(input)) { - // return resolveMigrationContentRelationship(input) - // } + if (input instanceof PrismicMigrationDocument || is.prismicDocument(input)) { + return resolveMigrationContentRelationship(input) + } - // if (typeof input === "function") { - // return await resolveMigrationDocumentData(await input()) - // } + if (typeof input === "function") { + return await resolveMigrationDocumentData(await input(), migration) + } if (Array.isArray(input)) { const res = [] @@ -32,7 +53,7 @@ export async function resolveMigrationDocumentData( return res } - if (typeof input === "object") { + if (input && typeof input === "object") { const res: Record = {} for (const key in input) { diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index 3d4d3856..e6a1e069 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -1,9 +1,15 @@ +import { resolveMigrationDocumentData } from "../../lib/resolveMigrationDocumentData" + import type { Migration } from "../../Migration" import type { Asset } from "../api/asset/asset" +import type { FilledContentRelationshipField } from "../value/contentRelationship" import type { FilledImageFieldImage, ImageField } from "../value/image" import { type FilledLinkToWebField, LinkType } from "../value/link" -import type { LinkToMediaField } from "../value/linkToMedia" +import type { + FilledLinkToMediaField, + LinkToMediaField, +} from "../value/linkToMedia" import { type RTImageNode, RichTextNodeType } from "../value/richText" import type { MigrationContentRelationship } from "./ContentRelationship" @@ -327,10 +333,11 @@ export class MigrationRTImageNode extends MigrationAsset { return { ...assetToImage(asset, this._initialField), type: RichTextNodeType.image, - linkTo: - this.linkTo instanceof MigrationField - ? await this.linkTo._resolve(migration) - : this.linkTo, + linkTo: (await resolveMigrationDocumentData(this.linkTo, migration)) as + | FilledLinkToWebField + | FilledLinkToMediaField + | FilledContentRelationshipField + | undefined, } } } diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts index 946da3e6..17177672 100644 --- a/src/types/migration/ContentRelationship.ts +++ b/src/types/migration/ContentRelationship.ts @@ -1,102 +1,21 @@ -import type { Migration } from "../../Migration" - import type { FilledContentRelationshipField } from "../value/contentRelationship" import type { PrismicDocument } from "../value/document" -import { LinkType } from "../value/link" -import { PrismicMigrationDocument } from "./Document" -import { MigrationField } from "./Field" +import type { PrismicMigrationDocument } from "./Document" -/** - * Configuration used to create a content relationship field in a migration. - */ -type MigrationContentRelationshipConfig< - TDocuments extends PrismicDocument = PrismicDocument, -> = - | TDocuments - | PrismicMigrationDocument - | FilledContentRelationshipField - | undefined +type ValueOrThunk = T | (() => Promise | T) /** - * Unresolved configuration used to create a content relationship field in a - * migration allowing for lazy resolution. + * A content relationship field in a migration. */ -export type UnresolvedMigrationContentRelationshipConfig< +export type MigrationContentRelationship< TDocuments extends PrismicDocument = PrismicDocument, -> = - | MigrationContentRelationshipConfig - | (() => - | Promise> - | MigrationContentRelationshipConfig) +> = ValueOrThunk | undefined> /** - * A migration content relationship field used with the Prismic Migration API. + * The minimum amount of information needed to represent a content relationship + * field with the migration API. */ -export class MigrationContentRelationship extends MigrationField { - /** - * Unresolved configuration used to resolve the content relationship field's - * value - */ - #unresolvedConfig: UnresolvedMigrationContentRelationshipConfig - - /** - * Link text for the content relationship if any. - */ - text?: string - - /** - * Creates a migration content relationship field used with the Prismic - * Migration API. - * - * @param unresolvedConfig - A Prismic document, a migration document - * instance, an existing content relationship field's content, or a function - * that returns one of the previous. - * @param text - Link text for the content relationship if any. - * @param initialField - The initial field value if any. - * - * @returns A migration content relationship field instance. - */ - constructor( - unresolvedConfig: UnresolvedMigrationContentRelationshipConfig, - text?: string, - initialField?: FilledContentRelationshipField, - ) { - super(initialField) - - this.#unresolvedConfig = unresolvedConfig - this.text = text - } - - async _resolve( - migration: Migration, - ): Promise { - const config = - typeof this.#unresolvedConfig === "function" - ? await this.#unresolvedConfig() - : this.#unresolvedConfig - - if (config) { - const document = - config instanceof PrismicMigrationDocument - ? config.document - : // Not sure we need `getByOriginalID` here - migration.getByOriginalID(config.id)?.document || config - - if (document?.id) { - return { - link_type: LinkType.Document, - id: document.id, - uid: document.uid || undefined, - type: document.type, - tags: document.tags || [], - lang: document.lang, - isBroken: false, - // TODO: Remove when link text PR is merged - // @ts-expect-error - Future-proofing for link text - text: this.text, - } - } - } - } -} +export type MigrationContentRelationshipField = + | Pick + | undefined diff --git a/src/types/migration/Document.ts b/src/types/migration/Document.ts index fd1d0889..2bd1b5ea 100644 --- a/src/types/migration/Document.ts +++ b/src/types/migration/Document.ts @@ -24,7 +24,7 @@ export type InjectMigrationSpecificTypes = T extends RTImageNode : T extends FilledLinkToMediaField ? T | MigrationLinkToMedia | undefined : T extends FilledContentRelationshipField - ? T | MigrationContentRelationship | undefined + ? T | MigrationContentRelationship : // eslint-disable-next-line @typescript-eslint/no-explicit-any T extends Record ? { [P in keyof T]: InjectMigrationSpecificTypes } diff --git a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap index 01048f6a..ff7b4689 100644 --- a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap @@ -5,8 +5,9 @@ exports[`patches link fields > brokenLink > group 1`] = ` "group": [ { "field": { + "id": "__broken__", "isBroken": true, - "type": "Document", + "link_type": "Document", }, }, ], @@ -21,21 +22,24 @@ exports[`patches link fields > brokenLink > shared slice 1`] = ` "items": [ { "field": { + "id": "__broken__", "isBroken": true, - "type": "Document", + "link_type": "Document", }, }, ], "primary": { "field": { + "id": "__broken__", "isBroken": true, - "type": "Document", + "link_type": "Document", }, "group": [ { "field": { + "id": "__broken__", "isBroken": true, - "type": "Document", + "link_type": "Document", }, }, ], @@ -57,15 +61,17 @@ exports[`patches link fields > brokenLink > slice 1`] = ` "items": [ { "field": { + "id": "__broken__", "isBroken": true, - "type": "Document", + "link_type": "Document", }, }, ], "primary": { "field": { + "id": "__broken__", "isBroken": true, - "type": "Document", + "link_type": "Document", }, }, "slice_label": "Aliquet", @@ -78,8 +84,9 @@ exports[`patches link fields > brokenLink > slice 1`] = ` exports[`patches link fields > brokenLink > static zone 1`] = ` { "field": { + "id": "__broken__", "isBroken": true, - "type": "Document", + "link_type": "Document", }, } `; @@ -90,11 +97,7 @@ exports[`patches link fields > existing > group 1`] = ` { "field": { "id": "id-existing", - "isBroken": false, - "lang": "laoreet", "link_type": "Document", - "tags": [], - "type": "orci", }, }, ], @@ -110,38 +113,20 @@ exports[`patches link fields > existing > shared slice 1`] = ` { "field": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, }, ], "primary": { "field": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "group": [ { "field": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, }, ], @@ -164,26 +149,14 @@ exports[`patches link fields > existing > slice 1`] = ` { "field": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, }, ], "primary": { "field": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, }, "slice_label": "Aliquet", @@ -197,11 +170,7 @@ exports[`patches link fields > existing > static zone 1`] = ` { "field": { "id": "id-existing", - "isBroken": false, - "lang": "eget", "link_type": "Document", - "tags": [], - "type": "phasellus", }, } `; @@ -212,11 +181,7 @@ exports[`patches link fields > lazyExisting > group 1`] = ` { "field": { "id": "id-existing", - "isBroken": false, - "lang": "laoreet", "link_type": "Document", - "tags": [], - "type": "orci", }, }, ], @@ -232,38 +197,20 @@ exports[`patches link fields > lazyExisting > shared slice 1`] = ` { "field": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, }, ], "primary": { "field": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "group": [ { "field": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, }, ], @@ -286,26 +233,14 @@ exports[`patches link fields > lazyExisting > slice 1`] = ` { "field": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, }, ], "primary": { "field": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, }, "slice_label": "Aliquet", @@ -319,11 +254,7 @@ exports[`patches link fields > lazyExisting > static zone 1`] = ` { "field": { "id": "id-existing", - "isBroken": false, - "lang": "eget", "link_type": "Document", - "tags": [], - "type": "phasellus", }, } `; @@ -334,14 +265,7 @@ exports[`patches link fields > lazyMigration > group 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "a", "link_type": "Document", - "tags": [ - "Odio Eu", - ], - "type": "amet", - "uid": "Non sodales neque", }, }, ], @@ -357,35 +281,20 @@ exports[`patches link fields > lazyMigration > shared slice 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, }, ], "primary": { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, "group": [ { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, }, ], @@ -408,28 +317,14 @@ exports[`patches link fields > lazyMigration > slice 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "ultrices", "link_type": "Document", - "tags": [ - "Ipsum Consequat", - ], - "type": "eget", - "uid": "Amet dictum sit", }, }, ], "primary": { "field": { "id": "id-other", - "isBroken": false, - "lang": "ultrices", "link_type": "Document", - "tags": [ - "Ipsum Consequat", - ], - "type": "eget", - "uid": "Amet dictum sit", }, }, "slice_label": "Aliquet", @@ -443,12 +338,7 @@ exports[`patches link fields > lazyMigration > static zone 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "nisi,", "link_type": "Document", - "tags": [], - "type": "eros", - "uid": "Scelerisque mauris pellentesque", }, } `; @@ -459,14 +349,7 @@ exports[`patches link fields > migration > group 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "a", "link_type": "Document", - "tags": [ - "Odio Eu", - ], - "type": "amet", - "uid": "Non sodales neque", }, }, ], @@ -482,35 +365,20 @@ exports[`patches link fields > migration > shared slice 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, }, ], "primary": { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, "group": [ { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, }, ], @@ -533,28 +401,14 @@ exports[`patches link fields > migration > slice 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "ultrices", "link_type": "Document", - "tags": [ - "Ipsum Consequat", - ], - "type": "eget", - "uid": "Amet dictum sit", }, }, ], "primary": { "field": { "id": "id-other", - "isBroken": false, - "lang": "ultrices", "link_type": "Document", - "tags": [ - "Ipsum Consequat", - ], - "type": "eget", - "uid": "Amet dictum sit", }, }, "slice_label": "Aliquet", @@ -568,12 +422,7 @@ exports[`patches link fields > migration > static zone 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "nisi,", "link_type": "Document", - "tags": [], - "type": "eros", - "uid": "Scelerisque mauris pellentesque", }, } `; @@ -584,12 +433,7 @@ exports[`patches link fields > migrationNoTags > group 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "a", "link_type": "Document", - "tags": [], - "type": "amet", - "uid": "Non sodales neque", }, }, ], @@ -605,35 +449,20 @@ exports[`patches link fields > migrationNoTags > shared slice 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, }, ], "primary": { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, "group": [ { "field": { "id": "id-other", - "isBroken": false, - "lang": "gravida", "link_type": "Document", - "tags": [], - "type": "leo", - "uid": "Blandit volutpat maecenas", }, }, ], @@ -656,24 +485,14 @@ exports[`patches link fields > migrationNoTags > slice 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "ultrices", "link_type": "Document", - "tags": [], - "type": "eget", - "uid": "Amet dictum sit", }, }, ], "primary": { "field": { "id": "id-other", - "isBroken": false, - "lang": "ultrices", "link_type": "Document", - "tags": [], - "type": "eget", - "uid": "Amet dictum sit", }, }, "slice_label": "Aliquet", @@ -687,12 +506,7 @@ exports[`patches link fields > migrationNoTags > static zone 1`] = ` { "field": { "id": "id-other", - "isBroken": false, - "lang": "nisi,", "link_type": "Document", - "tags": [], - "type": "eros", - "uid": "Scelerisque mauris pellentesque", }, } `; @@ -703,12 +517,7 @@ exports[`patches link fields > otherRepositoryContentRelationship > group 1`] = { "field": { "id": "id-other-repository", - "isBroken": false, - "lang": "faucibus", "link_type": "Document", - "tags": [], - "type": "lorem", - "uid": "Pharetra et ultrices", }, }, ], @@ -724,41 +533,20 @@ exports[`patches link fields > otherRepositoryContentRelationship > shared slice { "field": { "id": "id-other-repository", - "isBroken": false, - "lang": "eu", "link_type": "Document", - "tags": [ - "Pharetra", - ], - "type": "pellentesque", - "uid": "Volutpat ac tincidunt", }, }, ], "primary": { "field": { "id": "id-other-repository", - "isBroken": false, - "lang": "eu", "link_type": "Document", - "tags": [ - "Pharetra", - ], - "type": "pellentesque", - "uid": "Volutpat ac tincidunt", }, "group": [ { "field": { "id": "id-other-repository", - "isBroken": false, - "lang": "eu", "link_type": "Document", - "tags": [ - "Pharetra", - ], - "type": "pellentesque", - "uid": "Volutpat ac tincidunt", }, }, ], @@ -781,28 +569,14 @@ exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = { "field": { "id": "id-other-repository", - "isBroken": false, - "lang": "dignissim", "link_type": "Document", - "tags": [ - "Arcu Vitae", - ], - "type": "a", - "uid": "Tristique senectus et", }, }, ], "primary": { "field": { "id": "id-other-repository", - "isBroken": false, - "lang": "dignissim", "link_type": "Document", - "tags": [ - "Arcu Vitae", - ], - "type": "a", - "uid": "Tristique senectus et", }, }, "slice_label": "Aliquet", @@ -816,12 +590,7 @@ exports[`patches link fields > otherRepositoryContentRelationship > static zone { "field": { "id": "id-other-repository", - "isBroken": false, - "lang": "quam", "link_type": "Document", - "tags": [], - "type": "semper", - "uid": "Sed viverra ipsum", }, } `; @@ -841,11 +610,7 @@ exports[`patches link fields > richTextLinkNode > group 1`] = ` { "data": { "id": "id-existing", - "isBroken": false, - "lang": "laoreet", "link_type": "Document", - "tags": [], - "type": "orci", }, "end": 5, "start": 0, @@ -879,13 +644,7 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` { "data": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "end": 5, "start": 0, @@ -910,13 +669,7 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` { "data": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "end": 5, "start": 0, @@ -940,13 +693,7 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` { "data": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "end": 5, "start": 0, @@ -987,13 +734,7 @@ exports[`patches link fields > richTextLinkNode > slice 1`] = ` { "data": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, "end": 5, "start": 0, @@ -1018,13 +759,7 @@ exports[`patches link fields > richTextLinkNode > slice 1`] = ` { "data": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, "end": 5, "start": 0, @@ -1056,11 +791,7 @@ exports[`patches link fields > richTextLinkNode > static zone 1`] = ` { "data": { "id": "id-existing", - "isBroken": false, - "lang": "eget", "link_type": "Document", - "tags": [], - "type": "phasellus", }, "end": 5, "start": 0, diff --git a/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap index 6e445ca2..5df8c17c 100644 --- a/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap @@ -474,11 +474,7 @@ exports[`patches rich text image nodes > newLinkTo > group 1`] = ` "id": "c1d5d24feae", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "laoreet", "link_type": "Document", - "tags": [], - "type": "orci", }, "type": "image", "url": "https://images.unsplash.com/photo-1504198266287-1659872e6590?w=4272&h=2848&fit=crop", @@ -518,13 +514,7 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "id": "961adcf1f5d", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "type": "image", "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", @@ -555,13 +545,7 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "id": "961adcf1f5d", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "type": "image", "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", @@ -591,13 +575,7 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "id": "961adcf1f5d", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "type": "image", "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", @@ -644,13 +622,7 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "id": "61cc54f4a69", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, "type": "image", "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", @@ -681,13 +653,7 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "id": "61cc54f4a69", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, "type": "image", "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", @@ -725,11 +691,7 @@ exports[`patches rich text image nodes > newLinkTo > static zone 1`] = ` "id": "ed908b1e225", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "eget", "link_type": "Document", - "tags": [], - "type": "phasellus", }, "type": "image", "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", @@ -999,11 +961,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > group 1`] = ` "id": "6442e21ad60", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "laoreet", "link_type": "Document", - "tags": [], - "type": "orci", }, "type": "image", "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", @@ -1054,13 +1012,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` "id": "3a8ec9378bc", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "type": "image", "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", @@ -1091,13 +1043,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` "id": "3a8ec9378bc", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "type": "image", "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", @@ -1127,13 +1073,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` "id": "3a8ec9378bc", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "tortor,", "link_type": "Document", - "tags": [ - "Risus In", - ], - "type": "dui", }, "type": "image", "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", @@ -1180,13 +1120,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > slice 1`] = ` "id": "2003a644c30", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, "type": "image", "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", @@ -1217,13 +1151,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > slice 1`] = ` "id": "2003a644c30", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "sapien", "link_type": "Document", - "tags": [ - "Aenean", - ], - "type": "amet", }, "type": "image", "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", @@ -1261,11 +1189,7 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > static zone 1`] "id": "ff1efb2b271", "linkTo": { "id": "id-existing", - "isBroken": false, - "lang": "eget", "link_type": "Document", - "tags": [], - "type": "phasellus", }, "type": "image", "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", diff --git a/test/writeClient-migrate-documents.test.ts b/test/writeClient-migrate-documents.test.ts index 8d10f287..af48bfc9 100644 --- a/test/writeClient-migrate-documents.test.ts +++ b/test/writeClient-migrate-documents.test.ts @@ -138,9 +138,7 @@ it.concurrent( const migration = prismic.createMigration() const doc = migration.createDocument(document, "foo", { - masterLanguageDocument: migration.createContentRelationship( - masterLanguageDocument, - ), + masterLanguageDocument, }) const reporter = vi.fn() @@ -190,9 +188,7 @@ it.concurrent( "foo", ) const doc = migration.createDocument(document, "bar", { - masterLanguageDocument: migration.createContentRelationship( - masterLanguageMigrationDocument, - ), + masterLanguageDocument: masterLanguageMigrationDocument, }) const reporter = vi.fn() @@ -232,9 +228,7 @@ it.concurrent( const migration = prismic.createMigration() const doc = migration.createDocument(document, "foo", { - masterLanguageDocument: migration.createContentRelationship( - () => masterLanguageDocument, - ), + masterLanguageDocument: () => masterLanguageDocument, }) const reporter = vi.fn() @@ -284,9 +278,7 @@ it.concurrent( "foo", ) const doc = migration.createDocument(document, "bar", { - masterLanguageDocument: migration.createContentRelationship( - () => masterLanguageMigrationDocument, - ), + masterLanguageDocument: () => masterLanguageMigrationDocument, }) const reporter = vi.fn() diff --git a/test/writeClient-migrate-patch-contentRelationship.test.ts b/test/writeClient-migrate-patch-contentRelationship.test.ts index 78491393..56a87278 100644 --- a/test/writeClient-migrate-patch-contentRelationship.test.ts +++ b/test/writeClient-migrate-patch-contentRelationship.test.ts @@ -3,40 +3,34 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPa import { RichTextNodeType } from "../src" testMigrationFieldPatching("patches link fields", { - existing: ({ migration, existingDocuments }) => - migration.createContentRelationship(existingDocuments[0]), - migration: ({ migration, migrationDocuments }) => { + existing: ({ existingDocuments }) => existingDocuments[0], + migration: ({ migrationDocuments }) => { delete migrationDocuments.other.document.id - return migration.createContentRelationship(migrationDocuments.other) + return migrationDocuments.other }, - lazyExisting: ({ migration, existingDocuments }) => { - return migration.createContentRelationship(() => existingDocuments[0]) + lazyExisting: ({ existingDocuments }) => { + return () => existingDocuments[0] }, lazyMigration: ({ migration, migrationDocuments }) => { delete migrationDocuments.other.document.id - return migration.createContentRelationship(() => + return () => migration.getByUID( migrationDocuments.other.document.type, migrationDocuments.other.document.uid!, - ), - ) + ) }, migrationNoTags: ({ migration, migrationDocuments }) => { migrationDocuments.other.document.tags = undefined - return migration.createContentRelationship(() => + return () => migration.getByUID( migrationDocuments.other.document.type, migrationDocuments.other.document.uid!, - ), - ) + ) }, - brokenLink: () => { - return { type: "Document", isBroken: true } - }, - richTextLinkNode: ({ migration, existingDocuments }) => [ + richTextLinkNode: ({ existingDocuments }) => [ { type: RichTextNodeType.paragraph, text: "lorem", @@ -46,7 +40,7 @@ testMigrationFieldPatching("patches link fields", { type: RichTextNodeType.hyperlink, start: 0, end: 5, - data: migration.createContentRelationship(existingDocuments[0]), + data: existingDocuments[0], }, ], }, @@ -56,6 +50,16 @@ testMigrationFieldPatching("patches link fields", { testMigrationFieldPatching( "patches link fields", { + brokenLink: () => { + return { + link_type: "Document", + id: "id", + type: "type", + tags: [], + lang: "lang", + isBroken: true, + } + }, otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { const contentRelationship = ctx.mock.value.link({ type: "Document" }) // `migrationDocuments` contains documents from "another repository" diff --git a/test/writeClient-migrate-patch-rtImageNode.test.ts b/test/writeClient-migrate-patch-rtImageNode.test.ts index b4138805..41d36393 100644 --- a/test/writeClient-migrate-patch-rtImageNode.test.ts +++ b/test/writeClient-migrate-patch-rtImageNode.test.ts @@ -2,7 +2,6 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPa import { RichTextNodeType } from "../src" import { assetToImage } from "../src/types/migration/Asset" -import { MigrationContentRelationship } from "../src/types/migration/ContentRelationship" testMigrationFieldPatching("patches rich text image nodes", { new: ({ ctx, migration }) => [ @@ -18,9 +17,7 @@ testMigrationFieldPatching("patches rich text image nodes", { ], newLinkTo: ({ ctx, migration, existingDocuments }) => [ ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - migration - .createAsset("foo", "foo.png") - .asRTImageNode(new MigrationContentRelationship(existingDocuments[0])), + migration.createAsset("foo", "foo.png").asRTImageNode(existingDocuments[0]), ], }) @@ -36,19 +33,14 @@ testMigrationFieldPatching( url: `${mockedDomain}/foo.png`, }, ], - otherRepositoryLinkTo: ({ - ctx, - migration, - mockedDomain, - existingDocuments, - }) => [ + otherRepositoryLinkTo: ({ ctx, mockedDomain, existingDocuments }) => [ ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), { type: RichTextNodeType.image, ...ctx.mock.value.image({ state: "filled" }), id: "foo-id", url: `${mockedDomain}/foo.png`, - linkTo: migration.createContentRelationship(existingDocuments[0]), + linkTo: existingDocuments[0], }, ], }, From c0c4419c3e63026decc5e3f5b885f4ab36034181 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 16 Sep 2024 16:47:17 +0200 Subject: [PATCH 55/61] refactor: remove all child asset classes --- src/Migration.ts | 58 +++- src/WriteClient.ts | 11 +- src/index.ts | 3 +- src/lib/isMigrationValue.ts | 118 +++++++ src/lib/resolveMigrationDocumentData.ts | 185 +++++++++- src/types/migration/Asset.ts | 322 ++++-------------- src/types/migration/ContentRelationship.ts | 19 +- src/types/migration/Document.ts | 6 +- src/types/migration/Field.ts | 64 ---- .../testMigrationFieldPatching.ts | 13 +- test/migration-createAsset.test.ts | 18 +- ...igration-createDocumentFromPrismic.test.ts | 8 +- test/types/migration.types.ts | 81 +---- ...-migrate-patch-contentRelationship.test.ts | 20 +- test/writeClient-migrate-patch-image.test.ts | 26 +- ...teClient-migrate-patch-linkToMedia.test.ts | 50 ++- ...teClient-migrate-patch-rtImageNode.test.ts | 56 +-- ...teClient-migrate-patch-simpleField.test.ts | 7 +- 18 files changed, 586 insertions(+), 479 deletions(-) create mode 100644 src/lib/isMigrationValue.ts delete mode 100644 src/types/migration/Field.ts diff --git a/src/Migration.ts b/src/Migration.ts index 4f44ae6b..968748ac 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -5,9 +5,10 @@ import type { Asset } from "./types/api/asset/asset" import type { MigrationAssetConfig, MigrationLinkToMedia, + MigrationRTImageNode, } from "./types/migration/Asset" -import type { MigrationAsset } from "./types/migration/Asset" -import { MigrationImage } from "./types/migration/Asset" +import { PrismicMigrationAsset } from "./types/migration/Asset" +import type { MigrationImage } from "./types/migration/Asset" import type { MigrationContentRelationship } from "./types/migration/ContentRelationship" import { PrismicMigrationDocument } from "./types/migration/Document" import type { @@ -16,8 +17,9 @@ import type { } from "./types/migration/Document" import type { PrismicDocument } from "./types/value/document" import type { FilledImageFieldImage } from "./types/value/image" -import type { FilledLinkToWebField } from "./types/value/link" +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 @@ -47,7 +49,7 @@ export class Migration { * * @internal */ - _assets: Map = new Map() + _assets: Map = new Map() /** * Documents registered in the migration. @@ -205,22 +207,23 @@ export class Migration { } validateAssetMetadata(config) - const migrationAsset = new MigrationImage(config, maybeInitialField) + + // 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 = config.notes || maybeAsset.config.notes - maybeAsset.config.credits = config.credits || maybeAsset.config.credits - maybeAsset.config.alt = config.alt || maybeAsset.config.alt - maybeAsset.config.tags = Array.from( - new Set([...(config.tags || []), ...(maybeAsset.config.tags || [])]), + maybeAsset._config.notes = config.notes || maybeAsset._config.notes + maybeAsset._config.credits = config.credits || maybeAsset._config.credits + maybeAsset._config.alt = config.alt || maybeAsset._config.alt + maybeAsset._config.tags = Array.from( + new Set([...(config.tags || []), ...(maybeAsset._config.tags || [])]), ) } else { this._assets.set(config.id, migrationAsset) } - // We returned a detached instance of the asset to serialize it properly return migrationAsset } @@ -393,24 +396,43 @@ export class Migration { #migratePrismicDocumentData(input: unknown): unknown { if (is.filledContentRelationship(input)) { if (input.isBroken) { - return { link_type: "Document", id: "__broken__", isBroken: true } + return { + link_type: LinkType.Document, + id: "__broken__", + isBroken: true, + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + text: input.text, + } } - return () => this.#getByOriginalID(input.id) + return { + link_type: LinkType.Document, + id: () => this.#getByOriginalID(input.id), + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + text: input.text, + } } if (is.filledLinkToMedia(input)) { - // TODO: Remove when link text PR is merged - // @ts-expect-error - Future-proofing for link text - return this.createAsset(input).asLinkToMedia(input.text) + return { + link_type: LinkType.Media, + id: this.createAsset(input), + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + text: input.text, + } } if (is.rtImageNode(input)) { // Rich text image nodes - const rtImageNode = this.createAsset(input).asRTImageNode() + const rtImageNode: MigrationRTImageNode = { + type: RichTextNodeType.image, + id: this.createAsset(input), + } if (input.linkTo) { - // Node `linkTo` dependency is tracked internally rtImageNode.linkTo = this.#migratePrismicDocumentData(input.linkTo) as | MigrationContentRelationship | MigrationLinkToMedia diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 32cce269..96609182 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -24,7 +24,7 @@ import { type PostDocumentResult, type PutDocumentParams, } from "./types/api/migration/document" -import type { MigrationAsset } from "./types/migration/Asset" +import type { PrismicMigrationAsset } from "./types/migration/Asset" import type { MigrationDocument, PendingPrismicDocument, @@ -90,7 +90,7 @@ type MigrateReporterEventMap = { current: number remaining: number total: number - asset: MigrationAsset + asset: PrismicMigrationAsset } "assets:created": { created: number @@ -334,7 +334,7 @@ export class WriteClient< }) const { file, filename, notes, credits, alt, tags } = - migrationAsset.config + migrationAsset._config let resolvedFile: PostAssetParams["file"] | File if (typeof file === "string") { @@ -370,7 +370,7 @@ export class WriteClient< ...fetchParams, }) - migrationAsset.asset = asset + migrationAsset._asset = asset } reporter?.({ @@ -437,7 +437,8 @@ export class WriteClient< const masterLanguageDocument = await resolveMigrationContentRelationship(doc.masterLanguageDocument) - masterLanguageDocumentID = masterLanguageDocument?.id + masterLanguageDocumentID = + "id" in masterLanguageDocument ? masterLanguageDocument.id : undefined } else if (doc.originalPrismicDocument) { masterLanguageDocumentID = doc.originalPrismicDocument.alternate_languages.find( diff --git a/src/index.ts b/src/index.ts index 6750f34a..81ebb0ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -326,13 +326,14 @@ export type { } from "./types/model/types" // Migrations - Types representing Prismic Migration API content values. +export { PrismicMigrationDocument } from "./types/migration/Document" export type { - PrismicMigrationDocument, PendingPrismicDocument, ExistingPrismicDocument, InjectMigrationSpecificTypes, } from "./types/migration/Document" +export { PrismicMigrationAsset } from "./types/migration/Asset" export type { MigrationImage, MigrationLinkToMedia, diff --git a/src/lib/isMigrationValue.ts b/src/lib/isMigrationValue.ts new file mode 100644 index 00000000..d95907be --- /dev/null +++ b/src/lib/isMigrationValue.ts @@ -0,0 +1,118 @@ +import type { + MigrationImage, + MigrationLinkToMedia, + MigrationRTImageNode, +} from "../types/migration/Asset" +import { PrismicMigrationAsset } from "../types/migration/Asset" +import type { MigrationContentRelationship } from "../types/migration/ContentRelationship" +import { + type InjectMigrationSpecificTypes, + PrismicMigrationDocument, +} from "../types/migration/Document" +import type { PrismicDocument } from "../types/value/document" +import type { GroupField } from "../types/value/group" +import { LinkType } from "../types/value/link" +import { RichTextNodeType } from "../types/value/richText" +import type { SliceZone } from "../types/value/sliceZone" +import type { AnyRegularField } from "../types/value/types" + +import * as is from "./isValue" + +/** + * Unknown value to check if it's a specific field type. + * + * @remarks + * Explicit types are added to help ensure narrowing is done effectively. + */ +type UnknownValue = + | PrismicDocument + | InjectMigrationSpecificTypes + | unknown + +/** + * Checks if a value is a migration content relationship field. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a migration content relationship field, `false` + * otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const contentRelationship = ( + value: UnknownValue, +): value is MigrationContentRelationship => { + return ( + value instanceof PrismicMigrationDocument || + is.prismicDocument(value) || + (typeof value === "object" && + value !== null && + "link_type" in value && + value.link_type === LinkType.Document && + "id" in value && + (contentRelationship(value.id) || typeof value.id === "function")) + ) +} + +/** + * Checks if a value is a migration image field. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a migration image field, `false` otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const image = (value: UnknownValue): value is MigrationImage => { + return value instanceof PrismicMigrationAsset +} + +/** + * Checks if a value is a migration link to media field. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a migration link to media field, `false` + * otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const linkToMedia = ( + value: UnknownValue, +): value is MigrationLinkToMedia => { + return ( + typeof value === "object" && + value !== null && + "id" in value && + value.id instanceof PrismicMigrationAsset && + "link_type" in value && + value.link_type === LinkType.Media + ) +} + +/** + * Checks if a value is a migration rich text image node. + * + * @param value - Value to check. + * + * @returns `true` if `value` is a migration rich text image node, `false` + * otherwise. + * + * @internal + * This is not an official helper function and it's only designed to work with internal processes. + */ +export const rtImageNode = ( + value: UnknownValue, +): value is MigrationRTImageNode => { + return ( + typeof value === "object" && + value !== null && + "id" in value && + value.id instanceof PrismicMigrationAsset && + "type" in value && + value.type === RichTextNodeType.image + ) +} diff --git a/src/lib/resolveMigrationDocumentData.ts b/src/lib/resolveMigrationDocumentData.ts index 7df8b8f0..7bba2ea8 100644 --- a/src/lib/resolveMigrationDocumentData.ts +++ b/src/lib/resolveMigrationDocumentData.ts @@ -1,14 +1,32 @@ +import type { + MigrationImage, + MigrationLinkToMedia, + MigrationRTImageNode, +} from "../types/migration/Asset" import type { MigrationContentRelationship, MigrationContentRelationshipField, } from "../types/migration/ContentRelationship" import { PrismicMigrationDocument } from "../types/migration/Document" -import { MigrationField } from "../types/migration/Field" +import type { FilledImageFieldImage } from "../types/value/image" +import type { LinkField } from "../types/value/link" +import { LinkType } from "../types/value/link" +import type { LinkToMediaField } from "../types/value/linkToMedia" +import type { RTImageNode } from "../types/value/richText" +import { RichTextNodeType } from "../types/value/richText" +import * as isFilled from "../helpers/isFilled" import type { Migration } from "../Migration" -import * as is from "./isValue" +import * as isMigration from "./isMigrationValue" +/** + * Resolves a migration content relationship to a content relationship field. + * + * @param relation - Content relationship to resolve. + * + * @returns Resolved content relationship field. + */ export async function resolveMigrationContentRelationship( relation: MigrationContentRelationship, ): Promise { @@ -19,30 +37,180 @@ export async function resolveMigrationContentRelationship( if (relation instanceof PrismicMigrationDocument) { return relation.document.id ? { link_type: "Document", id: relation.document.id } - : undefined + : { link_type: "Document" } } if (relation) { + if ( + isMigration.contentRelationship(relation.id) || + typeof relation.id === "function" + ) { + return { + ...(await resolveMigrationContentRelationship(relation.id)), + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + text: relation.text, + } + } + return { link_type: "Document", id: relation.id } } + + return { link_type: "Document" } +} + +/** + * Resolves a migration image to an image field. + * + * @param migrationAsset - Asset to resolve. + * @param migration - Migration instance. + * @param withThumbnails - Whether to include thumbnails. + * + * @returns Resolved image field. + */ +export const resolveMigrationImage = ( + image: MigrationImage, + migration: Migration, + withThumbnails?: boolean, +): FilledImageFieldImage | undefined => { + const asset = migration._assets.get(image._config.id)?._asset + const maybeInitialField = image._initialField + + if (asset) { + const parameters = (maybeInitialField?.url || asset.url).split("?")[1] + const url = `${asset.url.split("?")[0]}${parameters ? `?${parameters}` : ""}` + const dimensions: FilledImageFieldImage["dimensions"] = { + width: asset.width!, + height: asset.height!, + } + const edit: FilledImageFieldImage["edit"] = + maybeInitialField && "edit" in maybeInitialField + ? maybeInitialField?.edit + : { x: 0, y: 0, zoom: 1, background: "transparent" } + + const alt = + (maybeInitialField && "alt" in maybeInitialField + ? maybeInitialField.alt + : undefined) || + asset.alt || + null + + const thumbnails: Record = {} + if (withThumbnails) { + for (const [name, thumbnail] of Object.entries(image._thumbnails)) { + const resolvedThumbnail = resolveMigrationImage(thumbnail, migration) + if (resolvedThumbnail) { + thumbnails[name] = resolvedThumbnail + } + } + } + + return { + id: asset.id, + url, + dimensions, + edit, + alt: alt, + copyright: asset.credits || null, + ...thumbnails, + } + } } +/** + * Resolves a migration rich text image node to a regular rich text image node. + * + * @param rtImageNode - Migration rich text image node to resolve. + * @param migration - Migration instance. + * + * @returns Resolved rich text image node. + */ +export const resolveMigrationRTImageNode = async ( + rtImageNode: MigrationRTImageNode, + migration: Migration, +): Promise => { + const image = resolveMigrationImage(rtImageNode.id, migration) + + if (image) { + const linkTo = (await resolveMigrationDocumentData( + rtImageNode.linkTo, + migration, + )) as LinkField + + return { + ...image, + type: RichTextNodeType.image, + linkTo: isFilled.link(linkTo) ? linkTo : undefined, + } + } +} + +/** + * Resolves a migration link to media to a regular link to media field. + * + * @param linkToMedia - Migration link to media to resolve. + * @param migration - Migration instance. + * + * @returns Resolved link to media field. + */ +export const resolveMigrationLinkToMedia = ( + linkToMedia: MigrationLinkToMedia, + migration: Migration, +): LinkToMediaField<"filled"> | undefined => { + const asset = migration._assets.get(linkToMedia.id._config.id)?._asset + + if (asset) { + return { + id: asset.id, + link_type: LinkType.Media, + name: asset.filename, + kind: asset.kind, + url: asset.url, + size: `${asset.size}`, + height: typeof asset.height === "number" ? `${asset.height}` : undefined, + width: typeof asset.width === "number" ? `${asset.width}` : undefined, + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + text: linkToMedia.text, + } + } +} + +/** + * Resolves a migration document data to actual data ready to be sent to the + * Migration API. + * + * @param input - Migration link to media to resolve. + * @param migration - Migration instance. + * + * @returns Resolved data. + */ export async function resolveMigrationDocumentData( input: unknown, migration: Migration, ): Promise { - if (input instanceof MigrationField) { - return input._resolve(migration) + // Migration fields + if (isMigration.contentRelationship(input)) { + return resolveMigrationContentRelationship(input) } - if (input instanceof PrismicMigrationDocument || is.prismicDocument(input)) { - return resolveMigrationContentRelationship(input) + if (isMigration.image(input)) { + return resolveMigrationImage(input, migration, true) + } + + if (isMigration.linkToMedia(input)) { + return resolveMigrationLinkToMedia(input, migration) + } + + if (isMigration.rtImageNode(input)) { + return resolveMigrationRTImageNode(input, migration) } if (typeof input === "function") { return await resolveMigrationDocumentData(await input(), migration) } + // Object traversing if (Array.isArray(input)) { const res = [] @@ -50,7 +218,7 @@ export async function resolveMigrationDocumentData( res.push(await resolveMigrationDocumentData(element, migration)) } - return res + return res.filter(Boolean) } if (input && typeof input === "object") { @@ -66,5 +234,6 @@ export async function resolveMigrationDocumentData( return res } + // Primitives return input } diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index e6a1e069..225dc4f9 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -1,96 +1,18 @@ -import { resolveMigrationDocumentData } from "../../lib/resolveMigrationDocumentData" - -import type { Migration } from "../../Migration" - import type { Asset } from "../api/asset/asset" -import type { FilledContentRelationshipField } from "../value/contentRelationship" -import type { FilledImageFieldImage, ImageField } from "../value/image" -import { type FilledLinkToWebField, LinkType } from "../value/link" -import type { - FilledLinkToMediaField, - LinkToMediaField, -} from "../value/linkToMedia" -import { type RTImageNode, RichTextNodeType } from "../value/richText" +import type { FilledImageFieldImage } from "../value/image" +import type { LinkToMediaField } from "../value/linkToMedia" +import { type RTImageNode } from "../value/richText" -import type { MigrationContentRelationship } from "./ContentRelationship" -import { MigrationField } from "./Field" +import type { InjectMigrationSpecificTypes } from "./Document" /** - * Any type of image field handled by {@link MigrationAsset} + * Any type of image field handled by {@link PrismicMigrationAsset} */ type ImageLike = | FilledImageFieldImage | LinkToMediaField<"filled"> | RTImageNode -/** - * Converts an asset to an image field. - * - * @param asset - Asset to convert. - * @param maybeInitialField - Initial image field if available, used to preserve - * edits. - * - * @returns Equivalent image field. - */ -export const assetToImage = ( - asset: Asset, - maybeInitialField?: ImageLike, -): FilledImageFieldImage => { - const parameters = (maybeInitialField?.url || asset.url).split("?")[1] - const url = `${asset.url.split("?")[0]}${parameters ? `?${parameters}` : ""}` - const dimensions: FilledImageFieldImage["dimensions"] = { - width: asset.width!, - height: asset.height!, - } - const edit: FilledImageFieldImage["edit"] = - maybeInitialField && "edit" in maybeInitialField - ? maybeInitialField?.edit - : { x: 0, y: 0, zoom: 1, background: "transparent" } - - const alt = - (maybeInitialField && "alt" in maybeInitialField - ? maybeInitialField.alt - : undefined) || - asset.alt || - null - - return { - id: asset.id, - url, - dimensions, - edit, - alt: alt, - copyright: asset.credits || null, - } -} - -/** - * Converts an asset to a link to media field. - * - * @param asset - Asset to convert. - * @param text - Link text for the link to media field if any. - * - * @returns Equivalent link to media field. - */ -export const assetToLinkToMedia = ( - asset: Asset, - text?: string, -): LinkToMediaField<"filled"> => { - return { - id: asset.id, - link_type: LinkType.Media, - name: asset.filename, - kind: asset.kind, - url: asset.url, - size: `${asset.size}`, - height: typeof asset.height === "number" ? `${asset.height}` : undefined, - width: typeof asset.width === "number" ? `${asset.width}` : undefined, - // TODO: Remove when link text PR is merged - // @ts-expect-error - Future-proofing for link text - text, - } -} - /** * An asset to be uploaded to Prismic media library. */ @@ -136,209 +58,107 @@ export type MigrationAssetConfig = { } /** - * A migration asset used with the Prismic Migration API. - * - * @typeParam TImageLike - Type of the image-like value. + * An image field in a migration. */ -export abstract class MigrationAsset< - TImageLike extends ImageLike = ImageLike, -> extends MigrationField { - /** - * Configuration of the asset. - * - * @internal - */ - config: MigrationAssetConfig +export type MigrationImage = PrismicMigrationAsset - /** - * Asset object from Prismic available once created. - */ - asset?: Asset +/** + * A link to media field in a migration. + */ +export type MigrationLinkToMedia = Pick< + LinkToMediaField<"filled">, + "link_type" +> & + Partial< + Pick< + LinkToMediaField<"filled">, + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + "text" + > + > & { + /** + * A reference to the migration asset used to resolve the link to media + * field's value. + */ + id: PrismicMigrationAsset + } +/** + * A rich text image node in a migration. + */ +export type MigrationRTImageNode = InjectMigrationSpecificTypes< + Pick +> & { /** - * Creates a migration asset used with the Prismic Migration API. - * - * @param config - Configuration of the asset. - * @param initialField - The initial field value if any. - * - * @returns A migration asset instance. + * A reference to the migration asset used to resolve the rich text image + * node's value. */ - constructor(config: MigrationAssetConfig, initialField?: ImageLike) { - super(initialField) - - this.config = config - } + id: PrismicMigrationAsset +} +/** + * A migration asset used with the Prismic Migration API. + * + * @typeParam TImageLike - Type of the image-like value. + */ +export class PrismicMigrationAsset { /** - * Marks the migration asset instance to be serialized as an image field. + * The initial field value this migration field was created with. * - * @returns A migration image instance. + * @internal */ - asImage(): MigrationImage { - return new MigrationImage(this.config, this._initialField) - } + _initialField?: ImageLike /** - * Marks the migration asset instance to be serialized as a link to media - * field. - * - * @param text - Link text for the link to media field if any. + * Configuration of the asset. * - * @returns A migration link to media instance. + * @internal */ - asLinkToMedia(text?: string): MigrationLinkToMedia { - return new MigrationLinkToMedia(this.config, text, this._initialField) - } + _config: MigrationAssetConfig /** - * Marks the migration asset instance to be serialized as a rich text image - * node. - * - * @param linkTo - Image node's link if any. + * Asset object from Prismic, available once created. * - * @returns A migration rich text image node instance. + * @internal */ - asRTImageNode( - linkTo?: - | MigrationLinkToMedia - | MigrationContentRelationship - | FilledLinkToWebField, - ): MigrationRTImageNode { - return new MigrationRTImageNode(this.config, linkTo, this._initialField) - } -} + _asset?: Asset -/** - * A migration image used with the Prismic Migration API. - */ -export class MigrationImage extends MigrationAsset { /** * Thumbnails of the image. - */ - #thumbnails: Record = {} - - /** - * Adds a thumbnail to the migration image instance. * - * @param name - Name of the thumbnail. - * @param thumbnail - Thumbnail to add as a migration image instance. - * - * @returns The current migration image instance, useful for chaining. - */ - addThumbnail(name: string, thumbnail: MigrationImage): this { - this.#thumbnails[name] = thumbnail - - return this - } - - async _resolve( - migration: Migration, - ): Promise { - const asset = migration._assets.get(this.config.id)?.asset - - if (asset) { - const field = assetToImage(asset, this._initialField) - - for (const name in this.#thumbnails) { - const thumbnail = await this.#thumbnails[name]._resolve(migration) - if (thumbnail) { - ;(field as ImageField)[name] = thumbnail - } - } - - return field - } - } -} - -/** - * A migration link to media used with the Prismic Migration API. - */ -export class MigrationLinkToMedia extends MigrationAsset< - LinkToMediaField<"filled"> -> { - /** - * Link text for the link to media field if any. + * @internal */ - text?: string + _thumbnails: Record = {} /** - * Creates a migration link to media instance used with the Prismic Migration - * API. + * Creates a migration asset used with the Prismic Migration API. * * @param config - Configuration of the asset. - * @param text - Link text for the link to media field if any. * @param initialField - The initial field value if any. * - * @returns A migration link to media instance. + * @returns A migration asset instance. */ - constructor( - config: MigrationAssetConfig, - text?: string, - initialField?: ImageLike, - ) { - super(config, initialField) - - this.text = text - } - - _resolve(migration: Migration): LinkToMediaField<"filled"> | undefined { - const asset = migration._assets.get(this.config.id)?.asset - - if (asset) { - return assetToLinkToMedia(asset, this.text) - } + constructor(config: MigrationAssetConfig, initialField?: ImageLike) { + this._config = config + this._initialField = initialField } -} -/** - * A migration rich text image node used with the Prismic Migration API. - */ -export class MigrationRTImageNode extends MigrationAsset { /** - * Image node's link if any. - */ - linkTo?: - | MigrationLinkToMedia - | MigrationContentRelationship - | FilledLinkToWebField - - /** - * Creates a migration rich text image node instance used with the Prismic - * Migration API. + * Adds a thumbnail to the migration asset instance. * - * @param config - Configuration of the asset. - * @param linkTo - Image node's link if any. - * @param initialField - The initial field value if any. + * @remarks + * This is only useful if the migration asset instance represents an image + * field. * - * @returns A migration rich text image node instance. + * @param name - Name of the thumbnail. + * @param thumbnail - Thumbnail to add as a migration image instance. + * + * @returns The current migration image instance, useful for chaining. */ - constructor( - config: MigrationAssetConfig, - linkTo?: - | MigrationLinkToMedia - | MigrationContentRelationship - | FilledLinkToWebField, - initialField?: ImageLike, - ) { - super(config, initialField) - - this.linkTo = linkTo - } - - async _resolve(migration: Migration): Promise { - const asset = migration._assets.get(this.config.id)?.asset + addThumbnail(name: string, thumbnail: PrismicMigrationAsset): this { + this._thumbnails[name] = thumbnail - if (asset) { - return { - ...assetToImage(asset, this._initialField), - type: RichTextNodeType.image, - linkTo: (await resolveMigrationDocumentData(this.linkTo, migration)) as - | FilledLinkToWebField - | FilledLinkToMediaField - | FilledContentRelationshipField - | undefined, - } - } + return this } } diff --git a/src/types/migration/ContentRelationship.ts b/src/types/migration/ContentRelationship.ts index 17177672..44255fc6 100644 --- a/src/types/migration/ContentRelationship.ts +++ b/src/types/migration/ContentRelationship.ts @@ -1,5 +1,6 @@ import type { FilledContentRelationshipField } from "../value/contentRelationship" import type { PrismicDocument } from "../value/document" +import type { EmptyLinkField } from "../value/link" import type { PrismicMigrationDocument } from "./Document" @@ -10,7 +11,21 @@ type ValueOrThunk = T | (() => Promise | T) */ export type MigrationContentRelationship< TDocuments extends PrismicDocument = PrismicDocument, -> = ValueOrThunk | undefined> +> = + | ValueOrThunk | undefined> + | (Pick & + Partial< + Pick< + FilledContentRelationshipField, + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + "text" + > + > & { + id: ValueOrThunk< + TDocuments | PrismicMigrationDocument | undefined + > + }) /** * The minimum amount of information needed to represent a content relationship @@ -18,4 +33,4 @@ export type MigrationContentRelationship< */ export type MigrationContentRelationshipField = | Pick - | undefined + | EmptyLinkField<"Document"> diff --git a/src/types/migration/Document.ts b/src/types/migration/Document.ts index 2bd1b5ea..65897c77 100644 --- a/src/types/migration/Document.ts +++ b/src/types/migration/Document.ts @@ -18,7 +18,11 @@ import type { MigrationContentRelationship } from "./ContentRelationship" * @typeParam T - Type of the record to extend. */ export type InjectMigrationSpecificTypes = T extends RTImageNode - ? T | MigrationRTImageNode | undefined + ? + | T + | (Omit & InjectMigrationSpecificTypes>) + | MigrationRTImageNode + | undefined : T extends FilledImageFieldImage ? T | MigrationImage | undefined : T extends FilledLinkToMediaField diff --git a/src/types/migration/Field.ts b/src/types/migration/Field.ts deleted file mode 100644 index a53984ec..00000000 --- a/src/types/migration/Field.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { AnyRegularField } from "../value/types" - -import type { Migration } from "../../Migration" - -import type { RTBlockNode, RTInlineNode } from "../value/richText" - -/** - * Interface for migration fields that can be resolved. - */ -interface Resolvable< - TField extends AnyRegularField | RTBlockNode | RTInlineNode = - | AnyRegularField - | RTBlockNode - | RTInlineNode, -> { - /** - * Resolves the field's value with the provided maps. - * - * @param migration - A migration instance with documents and assets to use - * for resolving the field's value - * - * @internal - */ - _resolve( - migration: Migration, - ): Promise | TField | undefined -} - -/** - * A migration field used with the Prismic Migration API. - * - * @typeParam TField - Type of the field value. - * @typeParam TInitialField - Type of the initial field value. - */ -export abstract class MigrationField< - TField extends AnyRegularField | RTBlockNode | RTInlineNode = - | AnyRegularField - | RTBlockNode - | RTInlineNode, - TInitialField = TField, -> implements Resolvable -{ - /** - * The initial field value this migration field was created with. - * - * @internal - */ - _initialField?: TInitialField - - /** - * Creates a migration field used with the Prismic Migration API. - * - * @param initialField - The initial field value if any. - * - * @returns A migration field instance. - */ - constructor(initialField?: TInitialField) { - this._initialField = initialField - } - - abstract _resolve( - migration: Migration, - ): Promise | TField | undefined -} diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index 3f59b0c2..74a141b0 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -141,14 +141,15 @@ const internalTestMigrationFieldPatching = ( }) } -type TestMigrationFieldPatchingFactoryCases = Record< - string, - (args: GetDataArgs) => prismic.ExistingPrismicDocument["data"][string] -> +type TestMigrationFieldPatchingFactoryCases< + TField extends prismic.ExistingPrismicDocument["data"][string], +> = Record TField> -export const testMigrationFieldPatching = ( +export const testMigrationFieldPatching = < + TField extends prismic.ExistingPrismicDocument["data"][string], +>( description: string, - cases: TestMigrationFieldPatchingFactoryCases, + cases: TestMigrationFieldPatchingFactoryCases, args: Omit = {}, ): void => { describe(description, () => { diff --git a/test/migration-createAsset.test.ts b/test/migration-createAsset.test.ts index b6c57773..317d6e66 100644 --- a/test/migration-createAsset.test.ts +++ b/test/migration-createAsset.test.ts @@ -17,7 +17,7 @@ it("creates an asset from a url", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)?.config).toEqual({ + expect(migration._assets.get(file)?._config).toEqual({ id: file, file, filename, @@ -32,7 +32,7 @@ it("creates an asset from a file", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)?.config).toEqual({ + expect(migration._assets.get(file)?._config).toEqual({ id: file, file, filename, @@ -69,7 +69,7 @@ it("creates an asset from an existing asset", () => { migration.createAsset(asset) - expect(migration._assets.get(asset.id)?.config).toStrictEqual({ + expect(migration._assets.get(asset.id)?._config).toStrictEqual({ id: asset.id, file: asset.url, filename: asset.filename, @@ -94,7 +94,7 @@ it("creates an asset from an image field", () => { migration.createAsset(image) - expect(migration._assets.get(image.id)?.config).toEqual({ + expect(migration._assets.get(image.id)?._config).toEqual({ id: image.id, file: image.url, filename: image.url.split("/").pop(), @@ -119,7 +119,7 @@ it("creates an asset from a link to media field", () => { migration.createAsset(link) - expect(migration._assets.get(link.id)?.config).toEqual({ + expect(migration._assets.get(link.id)?._config).toEqual({ id: link.id, file: link.url, filename: link.name, @@ -169,7 +169,7 @@ it("consolidates existing assets with additional metadata", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)?.config).toStrictEqual({ + expect(migration._assets.get(file)?._config).toStrictEqual({ id: file, file, filename, @@ -186,7 +186,7 @@ it("consolidates existing assets with additional metadata", () => { tags: ["tag"], }) - expect(migration._assets.get(file)?.config).toStrictEqual({ + expect(migration._assets.get(file)?._config).toStrictEqual({ id: file, file, filename, @@ -203,7 +203,7 @@ it("consolidates existing assets with additional metadata", () => { tags: ["tag", "tag 2"], }) - expect(migration._assets.get(file)?.config).toStrictEqual({ + expect(migration._assets.get(file)?._config).toStrictEqual({ id: file, file, filename, @@ -215,7 +215,7 @@ it("consolidates existing assets with additional metadata", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)?.config).toStrictEqual({ + expect(migration._assets.get(file)?._config).toStrictEqual({ id: file, file, filename, diff --git a/test/migration-createDocumentFromPrismic.test.ts b/test/migration-createDocumentFromPrismic.test.ts index d453f022..1c45b5f7 100644 --- a/test/migration-createDocumentFromPrismic.test.ts +++ b/test/migration-createDocumentFromPrismic.test.ts @@ -182,7 +182,7 @@ describe.each<{ migration.createDocumentFromPrismic(document, documentTitle) - expect(migration._assets.get(id)?.config).toStrictEqual(expected) + expect(migration._assets.get(id)?._config).toStrictEqual(expected) }) it("group fields", ({ mock }) => { @@ -198,7 +198,7 @@ describe.each<{ migration.createDocumentFromPrismic(document, documentTitle) - expect(migration._assets.get(id)?.config).toStrictEqual(expected) + expect(migration._assets.get(id)?._config).toStrictEqual(expected) }) it("slice's primary zone", ({ mock }) => { @@ -224,7 +224,7 @@ describe.each<{ migration.createDocumentFromPrismic(document, documentTitle) - expect(migration._assets.get(id)?.config).toStrictEqual(expected) + expect(migration._assets.get(id)?._config).toStrictEqual(expected) }) it("slice's repeatable zone", ({ mock }) => { @@ -250,6 +250,6 @@ describe.each<{ migration.createDocumentFromPrismic(document, documentTitle) - expect(migration._assets.get(id)?.config).toStrictEqual(expected) + expect(migration._assets.get(id)?._config).toStrictEqual(expected) }) }) diff --git a/test/types/migration.types.ts b/test/types/migration.types.ts index ca79adab..04c69e83 100644 --- a/test/types/migration.types.ts +++ b/test/types/migration.types.ts @@ -52,84 +52,23 @@ expectType>(false) const defaultCreateAsset = defaultMigration.createAsset("url", "name") expectType>(true) -expectType< - TypeEqual< - ReturnType, - prismic.MigrationImage - > ->(true) -expectType< - TypeOf> ->(true) - -expectType< - TypeEqual< - ReturnType, - prismic.MigrationLinkToMedia - > ->(true) -expectType< - TypeOf< - prismic.MigrationLinkToMedia, - ReturnType - > ->(true) - -expectType< - TypeEqual< - ReturnType, - prismic.MigrationRTImageNode - > ->(true) -expectType< - TypeOf< - prismic.MigrationRTImageNode, - ReturnType - > ->(true) +expectType>( + true, +) +expectType>( + true, +) // Documents const documentsCreateAsset = defaultMigration.createAsset("url", "name") expectType>(true) expectType< - TypeEqual< - ReturnType, - prismic.MigrationImage - > ->(true) -expectType< - TypeOf< - prismic.MigrationImage, - ReturnType - > ->(true) - -expectType< - TypeEqual< - ReturnType, - prismic.MigrationLinkToMedia - > ->(true) -expectType< - TypeOf< - prismic.MigrationLinkToMedia, - ReturnType - > ->(true) - -expectType< - TypeEqual< - ReturnType, - prismic.MigrationRTImageNode - > ->(true) -expectType< - TypeOf< - prismic.MigrationRTImageNode, - ReturnType - > + TypeEqual >(true) +expectType>( + true, +) /** * createDocument diff --git a/test/writeClient-migrate-patch-contentRelationship.test.ts b/test/writeClient-migrate-patch-contentRelationship.test.ts index 56a87278..bc0c2344 100644 --- a/test/writeClient-migrate-patch-contentRelationship.test.ts +++ b/test/writeClient-migrate-patch-contentRelationship.test.ts @@ -1,8 +1,16 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" -import { RichTextNodeType } from "../src" +import type { + ContentRelationshipField, + InjectMigrationSpecificTypes, + MigrationContentRelationship, + RichTextField, +} from "../src" +import { LinkType, RichTextNodeType } from "../src" -testMigrationFieldPatching("patches link fields", { +testMigrationFieldPatching< + MigrationContentRelationship | InjectMigrationSpecificTypes +>("patches link fields", { existing: ({ existingDocuments }) => existingDocuments[0], migration: ({ migrationDocuments }) => { delete migrationDocuments.other.document.id @@ -47,12 +55,12 @@ testMigrationFieldPatching("patches link fields", { ], }) -testMigrationFieldPatching( +testMigrationFieldPatching( "patches link fields", { brokenLink: () => { return { - link_type: "Document", + link_type: LinkType.Document, id: "id", type: "type", tags: [], @@ -61,7 +69,9 @@ testMigrationFieldPatching( } }, otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { - const contentRelationship = ctx.mock.value.link({ type: "Document" }) + const contentRelationship = ctx.mock.value.link({ + type: LinkType.Document, + }) // `migrationDocuments` contains documents from "another repository" contentRelationship.id = migrationDocuments.otherRepository.originalPrismicDocument!.id diff --git a/test/writeClient-migrate-patch-image.test.ts b/test/writeClient-migrate-patch-image.test.ts index e0bdc7fe..52c485e8 100644 --- a/test/writeClient-migrate-patch-image.test.ts +++ b/test/writeClient-migrate-patch-image.test.ts @@ -1,13 +1,27 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" -import { assetToImage } from "../src/types/migration/Asset" +import type { ImageField, MigrationImage } from "../src" -testMigrationFieldPatching("patches image fields", { - new: ({ migration }) => migration.createAsset("foo", "foo.png"), - existing: ({ existingAssets }) => assetToImage(existingAssets[0]), -}) +testMigrationFieldPatching( + "patches image fields", + { + new: ({ migration }) => migration.createAsset("foo", "foo.png"), + existing: ({ existingAssets }) => { + const asset = existingAssets[0] + + return { + id: asset.id, + url: asset.url, + dimensions: { width: asset.width!, height: asset.height! }, + edit: { x: 0, y: 0, zoom: 1, background: "transparent" }, + alt: asset.alt || null, + copyright: asset.credits || null, + } + }, + }, +) -testMigrationFieldPatching( +testMigrationFieldPatching( "patches image fields", { otherRepository: ({ ctx, mockedDomain }) => { diff --git a/test/writeClient-migrate-patch-linkToMedia.test.ts b/test/writeClient-migrate-patch-linkToMedia.test.ts index 5f01837a..90ad2b3e 100644 --- a/test/writeClient-migrate-patch-linkToMedia.test.ts +++ b/test/writeClient-migrate-patch-linkToMedia.test.ts @@ -1,12 +1,45 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" -import { RichTextNodeType } from "../src" +import type { + InjectMigrationSpecificTypes, + LinkToMediaField, + RichTextField, +} from "../src" +import { LinkType, RichTextNodeType } from "../src" +import type { Asset } from "../src/types/api/asset/asset" import { AssetType } from "../src/types/api/asset/asset" -import { assetToLinkToMedia } from "../src/types/migration/Asset" +import type { MigrationLinkToMedia } from "../src/types/migration/Asset" -testMigrationFieldPatching("patches link to media fields", { - new: ({ migration }) => - migration.createAsset("foo", "foo.png").asLinkToMedia(), +const assetToLinkToMedia = ( + asset: Asset, + text?: string, +): LinkToMediaField<"filled"> => { + return { + id: asset.id, + link_type: LinkType.Media, + name: asset.filename, + kind: asset.kind, + url: asset.url, + size: `${asset.size}`, + height: typeof asset.height === "number" ? `${asset.height}` : undefined, + width: typeof asset.width === "number" ? `${asset.width}` : undefined, + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + text, + } +} + +testMigrationFieldPatching< + | MigrationLinkToMedia + | LinkToMediaField + | InjectMigrationSpecificTypes +>("patches link to media fields", { + new: ({ migration }) => { + return { + link_type: LinkType.Media, + id: migration.createAsset("foo", "foo.png"), + } + }, existing: ({ existingAssets }) => assetToLinkToMedia(existingAssets[0]), existingNonImage: ({ existingAssets }) => { existingAssets[0].filename = "foo.pdf" @@ -27,7 +60,10 @@ testMigrationFieldPatching("patches link to media fields", { type: RichTextNodeType.hyperlink, start: 0, end: 5, - data: migration.createAsset("foo", "foo.png").asLinkToMedia(), + data: { + link_type: LinkType.Media, + id: migration.createAsset("foo", "foo.png"), + }, }, ], }, @@ -49,7 +85,7 @@ testMigrationFieldPatching("patches link to media fields", { ], }) -testMigrationFieldPatching( +testMigrationFieldPatching( "patches link to media fields", { otherRepository: ({ ctx, mockedDomain }) => { diff --git a/test/writeClient-migrate-patch-rtImageNode.test.ts b/test/writeClient-migrate-patch-rtImageNode.test.ts index 41d36393..d483e056 100644 --- a/test/writeClient-migrate-patch-rtImageNode.test.ts +++ b/test/writeClient-migrate-patch-rtImageNode.test.ts @@ -1,27 +1,45 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" +import type { InjectMigrationSpecificTypes, RichTextField } from "../src" import { RichTextNodeType } from "../src" -import { assetToImage } from "../src/types/migration/Asset" -testMigrationFieldPatching("patches rich text image nodes", { - new: ({ ctx, migration }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - migration.createAsset("foo", "foo.png").asRTImageNode(), - ], - existing: ({ ctx, existingAssets }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - { - type: RichTextNodeType.image, - ...assetToImage(existingAssets[0]), - }, - ], - newLinkTo: ({ ctx, migration, existingDocuments }) => [ - ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), - migration.createAsset("foo", "foo.png").asRTImageNode(existingDocuments[0]), - ], -}) +testMigrationFieldPatching>( + "patches rich text image nodes", + { + new: ({ ctx, migration }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + id: migration.createAsset("foo", "foo.png"), + }, + ], + existing: ({ ctx, existingAssets }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + id: existingAssets[0].id, + url: existingAssets[0].url, + dimensions: { + width: existingAssets[0].width!, + height: existingAssets[0].height!, + }, + edit: { x: 0, y: 0, zoom: 1, background: "transparent" }, + alt: existingAssets[0].alt || null, + copyright: existingAssets[0].credits || null, + }, + ], + newLinkTo: ({ ctx, migration, existingDocuments }) => [ + ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), + { + type: RichTextNodeType.image, + id: migration.createAsset("foo", "foo.png"), + linkTo: existingDocuments[0], + }, + ], + }, +) -testMigrationFieldPatching( +testMigrationFieldPatching>( "patches rich text image nodes", { otherRepository: ({ ctx, mockedDomain }) => [ diff --git a/test/writeClient-migrate-patch-simpleField.test.ts b/test/writeClient-migrate-patch-simpleField.test.ts index 0e6268cc..848fa844 100644 --- a/test/writeClient-migrate-patch-simpleField.test.ts +++ b/test/writeClient-migrate-patch-simpleField.test.ts @@ -1,8 +1,9 @@ import { testMigrationFieldPatching } from "./__testutils__/testMigrationFieldPatching" +import type { AnyRegularField, GroupField, RichTextField } from "../src" import { RichTextNodeType } from "../src" -testMigrationFieldPatching( +testMigrationFieldPatching( "does not patch simple fields", { embed: ({ ctx }) => ctx.mock.value.embed({ state: "filled" }), @@ -14,7 +15,9 @@ testMigrationFieldPatching( richTextSimple: ({ ctx }) => ctx.mock.value .richText({ state: "filled", pattern: "long" }) - .filter((node) => node.type !== RichTextNodeType.image), + .filter( + (node) => node.type !== RichTextNodeType.image, + ) as RichTextField, select: ({ ctx }) => ctx.mock.value.select({ model: ctx.mock.model.select({ options: ["foo", "bar"] }), From 51db286791288283ca37c7a599ccf97b21124287 Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 16 Sep 2024 17:55:23 +0200 Subject: [PATCH 56/61] refactor: remove `addThumbnail` method --- src/Migration.ts | 38 +++++----- src/WriteClient.ts | 4 +- src/lib/isMigrationValue.ts | 10 ++- src/lib/resolveMigrationDocumentData.ts | 34 ++++----- src/types/migration/Asset.ts | 76 +++++++------------ src/types/migration/Document.ts | 6 +- test/migration-createAsset.test.ts | 30 ++++---- ...igration-createDocumentFromPrismic.test.ts | 8 +- 8 files changed, 94 insertions(+), 112 deletions(-) diff --git a/src/Migration.ts b/src/Migration.ts index 968748ac..e138b16b 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -4,11 +4,11 @@ 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 { MigrationImage } from "./types/migration/Asset" import type { MigrationContentRelationship } from "./types/migration/ContentRelationship" import { PrismicMigrationDocument } from "./types/migration/Document" import type { @@ -68,10 +68,9 @@ export class Migration { * * @param asset - An asset object from Prismic Asset API. * - * @returns A migration asset field instance configured to be serialized as an - * image field by default. + * @returns A migration asset field instance. */ - createAsset(asset: Asset): MigrationImage + createAsset(asset: Asset): PrismicMigrationAsset /** * Registers an asset to be created in the migration from an image or link to @@ -85,12 +84,11 @@ export class Migration { * @param imageOrLinkToMediaField - An image or link to media field from * Prismic Document API. * - * @returns A migration asset field instance configured to be serialized as an - * image field by default. + * @returns A migration asset field instance. */ createAsset( imageOrLinkToMediaField: FilledImageFieldImage | FilledLinkToMediaField, - ): MigrationImage + ): PrismicMigrationAsset /** * Registers an asset to be created in the migration from a file. @@ -104,8 +102,7 @@ export class Migration { * @param filename - The filename of the asset. * @param params - Additional asset data. * - * @returns A migration asset field instance configured to be serialized as an - * image field by default. + * @returns A migration asset field instance. */ createAsset( file: MigrationAssetConfig["file"], @@ -116,7 +113,7 @@ export class Migration { alt?: string tags?: string[] }, - ): MigrationImage + ): PrismicMigrationAsset /** * Registers an asset to be created in the migration from a file, an asset @@ -127,8 +124,7 @@ export class Migration { * 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 configured to be serialized as an - * image field by default. + * @returns A migration asset field instance. */ createAsset( fileOrAssetOrField: @@ -148,7 +144,7 @@ export class Migration { alt?: string tags?: string[] } = {}, - ): MigrationImage { + ): PrismicMigrationAsset { let config: MigrationAssetConfig let maybeInitialField: FilledImageFieldImage | undefined if (typeof fileOrAssetOrField === "object" && "url" in fileOrAssetOrField) { @@ -214,11 +210,11 @@ export class Migration { const maybeAsset = this._assets.get(config.id) if (maybeAsset) { // Consolidate existing asset with new asset value if possible - maybeAsset._config.notes = config.notes || maybeAsset._config.notes - maybeAsset._config.credits = config.credits || maybeAsset._config.credits - maybeAsset._config.alt = config.alt || maybeAsset._config.alt - maybeAsset._config.tags = Array.from( - new Set([...(config.tags || []), ...(maybeAsset._config.tags || [])]), + 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) @@ -443,7 +439,9 @@ export class Migration { } if (is.filledImage(input)) { - const image = this.createAsset(input) + const image: MigrationImage = { + id: this.createAsset(input), + } const { id: _id, @@ -457,7 +455,7 @@ export class Migration { for (const name in thumbnails) { if (is.filledImage(thumbnails[name])) { - image.addThumbnail(name, this.createAsset(thumbnails[name])) + image[name] = this.createAsset(thumbnails[name]) } } diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 96609182..202f7ab8 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -334,7 +334,7 @@ export class WriteClient< }) const { file, filename, notes, credits, alt, tags } = - migrationAsset._config + migrationAsset.config let resolvedFile: PostAssetParams["file"] | File if (typeof file === "string") { @@ -370,7 +370,7 @@ export class WriteClient< ...fetchParams, }) - migrationAsset._asset = asset + migrationAsset.asset = asset } reporter?.({ diff --git a/src/lib/isMigrationValue.ts b/src/lib/isMigrationValue.ts index d95907be..28a442e1 100644 --- a/src/lib/isMigrationValue.ts +++ b/src/lib/isMigrationValue.ts @@ -66,7 +66,15 @@ export const contentRelationship = ( * This is not an official helper function and it's only designed to work with internal processes. */ export const image = (value: UnknownValue): value is MigrationImage => { - return value instanceof PrismicMigrationAsset + return ( + value instanceof PrismicMigrationAsset || + (typeof value === "object" && + value !== null && + "id" in value && + Object.values(value).every( + (maybeThumbnail) => maybeThumbnail instanceof PrismicMigrationAsset, + )) + ) } /** diff --git a/src/lib/resolveMigrationDocumentData.ts b/src/lib/resolveMigrationDocumentData.ts index 7bba2ea8..f25f95d4 100644 --- a/src/lib/resolveMigrationDocumentData.ts +++ b/src/lib/resolveMigrationDocumentData.ts @@ -1,7 +1,8 @@ -import type { - MigrationImage, - MigrationLinkToMedia, - MigrationRTImageNode, +import { + type MigrationImage, + type MigrationLinkToMedia, + type MigrationRTImageNode, + PrismicMigrationAsset, } from "../types/migration/Asset" import type { MigrationContentRelationship, @@ -73,8 +74,11 @@ export const resolveMigrationImage = ( migration: Migration, withThumbnails?: boolean, ): FilledImageFieldImage | undefined => { - const asset = migration._assets.get(image._config.id)?._asset - const maybeInitialField = image._initialField + const { id: master, ...thumbnails } = + image instanceof PrismicMigrationAsset ? { id: image } : image + + const asset = migration._assets.get(master.config.id)?.asset + const maybeInitialField = master.originalField if (asset) { const parameters = (maybeInitialField?.url || asset.url).split("?")[1] @@ -88,19 +92,15 @@ export const resolveMigrationImage = ( ? maybeInitialField?.edit : { x: 0, y: 0, zoom: 1, background: "transparent" } - const alt = - (maybeInitialField && "alt" in maybeInitialField - ? maybeInitialField.alt - : undefined) || - asset.alt || - null + // We give priority to the asset's specific alt text, then the image's general alt text + const alt = master.config.alt || asset.alt || null - const thumbnails: Record = {} + const resolvedThumbnails: Record = {} if (withThumbnails) { - for (const [name, thumbnail] of Object.entries(image._thumbnails)) { + for (const [name, thumbnail] of Object.entries(thumbnails)) { const resolvedThumbnail = resolveMigrationImage(thumbnail, migration) if (resolvedThumbnail) { - thumbnails[name] = resolvedThumbnail + resolvedThumbnails[name] = resolvedThumbnail } } } @@ -112,7 +112,7 @@ export const resolveMigrationImage = ( edit, alt: alt, copyright: asset.credits || null, - ...thumbnails, + ...resolvedThumbnails, } } } @@ -157,7 +157,7 @@ export const resolveMigrationLinkToMedia = ( linkToMedia: MigrationLinkToMedia, migration: Migration, ): LinkToMediaField<"filled"> | undefined => { - const asset = migration._assets.get(linkToMedia.id._config.id)?._asset + const asset = migration._assets.get(linkToMedia.id.config.id)?.asset if (asset) { return { diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index 225dc4f9..a6242e3a 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -5,14 +5,6 @@ import { type RTImageNode } from "../value/richText" import type { InjectMigrationSpecificTypes } from "./Document" -/** - * Any type of image field handled by {@link PrismicMigrationAsset} - */ -type ImageLike = - | FilledImageFieldImage - | LinkToMediaField<"filled"> - | RTImageNode - /** * An asset to be uploaded to Prismic media library. */ @@ -60,7 +52,15 @@ export type MigrationAssetConfig = { /** * An image field in a migration. */ -export type MigrationImage = PrismicMigrationAsset +export type MigrationImage = + | PrismicMigrationAsset + | ({ + /** + * A reference to the migration asset used to resolve the image field's + * value. + */ + id: PrismicMigrationAsset + } & Record) /** * A link to media field in a migration. @@ -99,37 +99,25 @@ export type MigrationRTImageNode = InjectMigrationSpecificTypes< /** * A migration asset used with the Prismic Migration API. - * - * @typeParam TImageLike - Type of the image-like value. */ export class PrismicMigrationAsset { /** - * The initial field value this migration field was created with. - * - * @internal + * Asset object from Prismic, available once created. */ - _initialField?: ImageLike + asset?: Asset /** * Configuration of the asset. - * - * @internal - */ - _config: MigrationAssetConfig - - /** - * Asset object from Prismic, available once created. - * - * @internal */ - _asset?: Asset + config: MigrationAssetConfig /** - * Thumbnails of the image. - * - * @internal + * The initial field value this migration field was created with. */ - _thumbnails: Record = {} + originalField?: + | FilledImageFieldImage + | LinkToMediaField<"filled"> + | RTImageNode /** * Creates a migration asset used with the Prismic Migration API. @@ -139,26 +127,14 @@ export class PrismicMigrationAsset { * * @returns A migration asset instance. */ - constructor(config: MigrationAssetConfig, initialField?: ImageLike) { - this._config = config - this._initialField = initialField - } - - /** - * Adds a thumbnail to the migration asset instance. - * - * @remarks - * This is only useful if the migration asset instance represents an image - * field. - * - * @param name - Name of the thumbnail. - * @param thumbnail - Thumbnail to add as a migration image instance. - * - * @returns The current migration image instance, useful for chaining. - */ - addThumbnail(name: string, thumbnail: PrismicMigrationAsset): this { - this._thumbnails[name] = thumbnail - - return this + constructor( + config: MigrationAssetConfig, + initialField?: + | FilledImageFieldImage + | LinkToMediaField<"filled"> + | RTImageNode, + ) { + this.config = config + this.originalField = initialField } } diff --git a/src/types/migration/Document.ts b/src/types/migration/Document.ts index 65897c77..45d0a007 100644 --- a/src/types/migration/Document.ts +++ b/src/types/migration/Document.ts @@ -105,9 +105,9 @@ export class PrismicMigrationDocument< */ title: string - // We're forced to inline `ContentRelationshipMigrationField` here, otherwise - // it creates a circular reference to itself which makes TypeScript unhappy. - // (but I think it's weird and it doesn't make sense :thinking:) + /** + * The link to the master language document to relate the document to if any. + */ masterLanguageDocument?: MigrationContentRelationship /** diff --git a/test/migration-createAsset.test.ts b/test/migration-createAsset.test.ts index 317d6e66..9d8e205a 100644 --- a/test/migration-createAsset.test.ts +++ b/test/migration-createAsset.test.ts @@ -17,7 +17,7 @@ it("creates an asset from a url", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)?._config).toEqual({ + expect(migration._assets.get(file)?.config).toEqual({ id: file, file, filename, @@ -32,7 +32,7 @@ it("creates an asset from a file", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)?._config).toEqual({ + expect(migration._assets.get(file)?.config).toEqual({ id: file, file, filename, @@ -69,7 +69,7 @@ it("creates an asset from an existing asset", () => { migration.createAsset(asset) - expect(migration._assets.get(asset.id)?._config).toStrictEqual({ + expect(migration._assets.get(asset.id)?.config).toStrictEqual({ id: asset.id, file: asset.url, filename: asset.filename, @@ -94,7 +94,7 @@ it("creates an asset from an image field", () => { migration.createAsset(image) - expect(migration._assets.get(image.id)?._config).toEqual({ + expect(migration._assets.get(image.id)?.config).toEqual({ id: image.id, file: image.url, filename: image.url.split("/").pop(), @@ -119,7 +119,7 @@ it("creates an asset from a link to media field", () => { migration.createAsset(link) - expect(migration._assets.get(link.id)?._config).toEqual({ + expect(migration._assets.get(link.id)?.config).toEqual({ id: link.id, file: link.url, filename: link.name, @@ -169,7 +169,7 @@ it("consolidates existing assets with additional metadata", () => { migration.createAsset(file, filename) - expect(migration._assets.get(file)?._config).toStrictEqual({ + expect(migration._assets.get(file)?.config).toStrictEqual({ id: file, file, filename, @@ -186,7 +186,7 @@ it("consolidates existing assets with additional metadata", () => { tags: ["tag"], }) - expect(migration._assets.get(file)?._config).toStrictEqual({ + expect(migration._assets.get(file)?.config).toStrictEqual({ id: file, file, filename, @@ -203,25 +203,25 @@ it("consolidates existing assets with additional metadata", () => { tags: ["tag", "tag 2"], }) - expect(migration._assets.get(file)?._config).toStrictEqual({ + expect(migration._assets.get(file)?.config).toStrictEqual({ id: file, file, filename, - notes: "notes 2", - alt: "alt 2", - credits: "credits 2", + notes: "notes", + alt: "alt", + credits: "credits", tags: ["tag", "tag 2"], }) migration.createAsset(file, filename) - expect(migration._assets.get(file)?._config).toStrictEqual({ + expect(migration._assets.get(file)?.config).toStrictEqual({ id: file, file, filename, - notes: "notes 2", - alt: "alt 2", - credits: "credits 2", + notes: "notes", + alt: "alt", + credits: "credits", tags: ["tag", "tag 2"], }) }) diff --git a/test/migration-createDocumentFromPrismic.test.ts b/test/migration-createDocumentFromPrismic.test.ts index 1c45b5f7..d453f022 100644 --- a/test/migration-createDocumentFromPrismic.test.ts +++ b/test/migration-createDocumentFromPrismic.test.ts @@ -182,7 +182,7 @@ describe.each<{ migration.createDocumentFromPrismic(document, documentTitle) - expect(migration._assets.get(id)?._config).toStrictEqual(expected) + expect(migration._assets.get(id)?.config).toStrictEqual(expected) }) it("group fields", ({ mock }) => { @@ -198,7 +198,7 @@ describe.each<{ migration.createDocumentFromPrismic(document, documentTitle) - expect(migration._assets.get(id)?._config).toStrictEqual(expected) + expect(migration._assets.get(id)?.config).toStrictEqual(expected) }) it("slice's primary zone", ({ mock }) => { @@ -224,7 +224,7 @@ describe.each<{ migration.createDocumentFromPrismic(document, documentTitle) - expect(migration._assets.get(id)?._config).toStrictEqual(expected) + expect(migration._assets.get(id)?.config).toStrictEqual(expected) }) it("slice's repeatable zone", ({ mock }) => { @@ -250,6 +250,6 @@ describe.each<{ migration.createDocumentFromPrismic(document, documentTitle) - expect(migration._assets.get(id)?._config).toStrictEqual(expected) + expect(migration._assets.get(id)?.config).toStrictEqual(expected) }) }) From 397b04c75ba98a4353e37b8af71290129d52ac4c Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 16 Sep 2024 21:09:01 +0200 Subject: [PATCH 57/61] refactor: tests naming --- src/Migration.ts | 8 + src/lib/resolveMigrationDocumentData.ts | 8 +- ...ate-patch-contentRelationship.test.ts.snap | 533 +++-- ...iteClient-migrate-patch-image.test.ts.snap | 1800 ++++++++++------- ...ent-migrate-patch-linkToMedia.test.ts.snap | 961 +++++---- ...ent-migrate-patch-rtImageNode.test.ts.snap | 766 ++++--- test/__testutils__/mockPrismicAssetAPI.ts | 8 + .../testMigrationFieldPatching.ts | 80 +- test/writeClient-migrate-documents.test.ts | 32 + ...-migrate-patch-contentRelationship.test.ts | 82 +- test/writeClient-migrate-patch-image.test.ts | 23 +- ...teClient-migrate-patch-linkToMedia.test.ts | 31 +- ...teClient-migrate-patch-rtImageNode.test.ts | 6 +- ...teClient-migrate-patch-simpleField.test.ts | 6 + 14 files changed, 2632 insertions(+), 1712 deletions(-) diff --git a/src/Migration.ts b/src/Migration.ts index e138b16b..d821641f 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -389,6 +389,14 @@ export class Migration { ) } + /** + * 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)) { if (input.isBroken) { diff --git a/src/lib/resolveMigrationDocumentData.ts b/src/lib/resolveMigrationDocumentData.ts index f25f95d4..8bfd9fbf 100644 --- a/src/lib/resolveMigrationDocumentData.ts +++ b/src/lib/resolveMigrationDocumentData.ts @@ -37,8 +37,8 @@ export async function resolveMigrationContentRelationship( if (relation instanceof PrismicMigrationDocument) { return relation.document.id - ? { link_type: "Document", id: relation.document.id } - : { link_type: "Document" } + ? { link_type: LinkType.Document, id: relation.document.id } + : { link_type: LinkType.Document } } if (relation) { @@ -54,10 +54,10 @@ export async function resolveMigrationContentRelationship( } } - return { link_type: "Document", id: relation.id } + return { link_type: LinkType.Document, id: relation.id } } - return { link_type: "Document" } + return { link_type: LinkType.Document } } /** diff --git a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap index ff7b4689..ae564060 100644 --- a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`patches link fields > brokenLink > group 1`] = ` +exports[`patches content relationship fields (from Prismic) > broken > group 1`] = ` { "group": [ { @@ -14,11 +14,11 @@ exports[`patches link fields > brokenLink > group 1`] = ` } `; -exports[`patches link fields > brokenLink > shared slice 1`] = ` +exports[`patches content relationship fields (from Prismic) > broken > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { @@ -45,19 +45,19 @@ exports[`patches link fields > brokenLink > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link fields > brokenLink > slice 1`] = ` +exports[`patches content relationship fields (from Prismic) > broken > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { @@ -74,14 +74,14 @@ exports[`patches link fields > brokenLink > slice 1`] = ` "link_type": "Document", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link fields > brokenLink > static zone 1`] = ` +exports[`patches content relationship fields (from Prismic) > broken > static zone 1`] = ` { "field": { "id": "__broken__", @@ -91,12 +91,12 @@ exports[`patches link fields > brokenLink > static zone 1`] = ` } `; -exports[`patches link fields > existing > group 1`] = ` +exports[`patches content relationship fields (from Prismic) > simple > group 1`] = ` { "group": [ { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", }, }, @@ -104,167 +104,174 @@ exports[`patches link fields > existing > group 1`] = ` } `; -exports[`patches link fields > existing > shared slice 1`] = ` +exports[`patches content relationship fields (from Prismic) > simple > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", }, }, ], "primary": { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", }, "group": [ { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link fields > existing > slice 1`] = ` +exports[`patches content relationship fields (from Prismic) > simple > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", }, }, ], "primary": { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link fields > existing > static zone 1`] = ` +exports[`patches content relationship fields (from Prismic) > simple > static zone 1`] = ` { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", }, } `; -exports[`patches link fields > lazyExisting > group 1`] = ` +exports[`patches content relationship fields (from Prismic) > withText > group 1`] = ` { "group": [ { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", + "text": "foo", }, }, ], } `; -exports[`patches link fields > lazyExisting > shared slice 1`] = ` +exports[`patches content relationship fields (from Prismic) > withText > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", + "text": "foo", }, }, ], "primary": { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", + "text": "foo", }, "group": [ { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", + "text": "foo", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link fields > lazyExisting > slice 1`] = ` +exports[`patches content relationship fields (from Prismic) > withText > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", + "text": "foo", }, }, ], "primary": { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", + "text": "foo", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link fields > lazyExisting > static zone 1`] = ` +exports[`patches content relationship fields (from Prismic) > withText > static zone 1`] = ` { "field": { - "id": "id-existing", + "id": "other.id-fromPrismic", "link_type": "Document", + "text": "foo", }, } `; -exports[`patches link fields > lazyMigration > group 1`] = ` +exports[`patches content relationship fields > existing > group 1`] = ` { "group": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, @@ -272,83 +279,83 @@ exports[`patches link fields > lazyMigration > group 1`] = ` } `; -exports[`patches link fields > lazyMigration > shared slice 1`] = ` +exports[`patches content relationship fields > existing > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, ], "primary": { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, "group": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link fields > lazyMigration > slice 1`] = ` +exports[`patches content relationship fields > existing > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, ], "primary": { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link fields > lazyMigration > static zone 1`] = ` +exports[`patches content relationship fields > existing > static zone 1`] = ` { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, } `; -exports[`patches link fields > migration > group 1`] = ` +exports[`patches content relationship fields > existingLongForm > group 1`] = ` { "group": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, @@ -356,167 +363,174 @@ exports[`patches link fields > migration > group 1`] = ` } `; -exports[`patches link fields > migration > shared slice 1`] = ` +exports[`patches content relationship fields > existingLongForm > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, ], "primary": { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, "group": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link fields > migration > slice 1`] = ` +exports[`patches content relationship fields > existingLongForm > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, ], "primary": { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link fields > migration > static zone 1`] = ` +exports[`patches content relationship fields > existingLongForm > static zone 1`] = ` { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", }, } `; -exports[`patches link fields > migrationNoTags > group 1`] = ` +exports[`patches content relationship fields > existingLongFormWithText > group 1`] = ` { "group": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", + "text": "foo", }, }, ], } `; -exports[`patches link fields > migrationNoTags > shared slice 1`] = ` +exports[`patches content relationship fields > existingLongFormWithText > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", + "text": "foo", }, }, ], "primary": { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", + "text": "foo", }, "group": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", + "text": "foo", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link fields > migrationNoTags > slice 1`] = ` +exports[`patches content relationship fields > existingLongFormWithText > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", + "text": "foo", }, }, ], "primary": { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", + "text": "foo", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link fields > migrationNoTags > static zone 1`] = ` +exports[`patches content relationship fields > existingLongFormWithText > static zone 1`] = ` { "field": { - "id": "id-other", + "id": "other.id-existing", "link_type": "Document", + "text": "foo", }, } `; -exports[`patches link fields > otherRepositoryContentRelationship > group 1`] = ` +exports[`patches content relationship fields > lazyExisting > group 1`] = ` { "group": [ { "field": { - "id": "id-other-repository", + "id": "other.id-existing", "link_type": "Document", }, }, @@ -524,78 +538,323 @@ exports[`patches link fields > otherRepositoryContentRelationship > group 1`] = } `; -exports[`patches link fields > otherRepositoryContentRelationship > shared slice 1`] = ` +exports[`patches content relationship fields > lazyExisting > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "id": "id-other-repository", + "id": "other.id-existing", "link_type": "Document", }, }, ], "primary": { "field": { - "id": "id-other-repository", + "id": "other.id-existing", "link_type": "Document", }, "group": [ { "field": { - "id": "id-other-repository", + "id": "other.id-existing", "link_type": "Document", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link fields > otherRepositoryContentRelationship > slice 1`] = ` +exports[`patches content relationship fields > lazyExisting > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "id": "id-other-repository", + "id": "other.id-existing", "link_type": "Document", }, }, ], "primary": { "field": { - "id": "id-other-repository", + "id": "other.id-existing", "link_type": "Document", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link fields > otherRepositoryContentRelationship > static zone 1`] = ` +exports[`patches content relationship fields > lazyExisting > static zone 1`] = ` { "field": { - "id": "id-other-repository", + "id": "other.id-existing", "link_type": "Document", }, } `; -exports[`patches link fields > richTextLinkNode > group 1`] = ` +exports[`patches content relationship fields > lazyOtherCreate > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + ], +} +`; + +exports[`patches content relationship fields > lazyOtherCreate > shared slice 1`] = ` +{ + "slices": [ + { + "id": "9b9dd0f2c3f", + "items": [ + { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + ], + "primary": { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + "group": [ + { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", + }, + ], +} +`; + +exports[`patches content relationship fields > lazyOtherCreate > slice 1`] = ` +{ + "slices": [ + { + "id": "5306297c5ed", + "items": [ + { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + ], + "primary": { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + "slice_label": "Vel", + "slice_type": "hac_habitasse", + }, + ], +} +`; + +exports[`patches content relationship fields > lazyOtherCreate > static zone 1`] = ` +{ + "field": { + "id": "other.id-create", + "link_type": "Document", + }, +} +`; + +exports[`patches content relationship fields > lazyOtherCreateMissingID > group 1`] = ` +{ + "group": [ + { + "field": { + "link_type": "Document", + }, + }, + ], +} +`; + +exports[`patches content relationship fields > lazyOtherCreateMissingID > shared slice 1`] = ` +{ + "slices": [ + { + "id": "9b9dd0f2c3f", + "items": [ + { + "field": { + "link_type": "Document", + }, + }, + ], + "primary": { + "field": { + "link_type": "Document", + }, + "group": [ + { + "field": { + "link_type": "Document", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", + }, + ], +} +`; + +exports[`patches content relationship fields > lazyOtherCreateMissingID > slice 1`] = ` +{ + "slices": [ + { + "id": "5306297c5ed", + "items": [ + { + "field": { + "link_type": "Document", + }, + }, + ], + "primary": { + "field": { + "link_type": "Document", + }, + }, + "slice_label": "Vel", + "slice_type": "hac_habitasse", + }, + ], +} +`; + +exports[`patches content relationship fields > lazyOtherCreateMissingID > static zone 1`] = ` +{ + "field": { + "link_type": "Document", + }, +} +`; + +exports[`patches content relationship fields > otherCreate > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + ], +} +`; + +exports[`patches content relationship fields > otherCreate > shared slice 1`] = ` +{ + "slices": [ + { + "id": "9b9dd0f2c3f", + "items": [ + { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + ], + "primary": { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + "group": [ + { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", + }, + ], +} +`; + +exports[`patches content relationship fields > otherCreate > slice 1`] = ` +{ + "slices": [ + { + "id": "5306297c5ed", + "items": [ + { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + ], + "primary": { + "field": { + "id": "other.id-create", + "link_type": "Document", + }, + }, + "slice_label": "Vel", + "slice_type": "hac_habitasse", + }, + ], +} +`; + +exports[`patches content relationship fields > otherCreate > static zone 1`] = ` +{ + "field": { + "id": "other.id-create", + "link_type": "Document", + }, +} +`; + +exports[`patches content relationship fields > richTextLinkNode > group 1`] = ` { "group": [ { @@ -609,7 +868,7 @@ exports[`patches link fields > richTextLinkNode > group 1`] = ` }, { "data": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "end": 5, @@ -626,11 +885,11 @@ exports[`patches link fields > richTextLinkNode > group 1`] = ` } `; -exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` +exports[`patches content relationship fields > richTextLinkNode > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": [ @@ -643,7 +902,7 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` }, { "data": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "end": 5, @@ -668,7 +927,7 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` }, { "data": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "end": 5, @@ -692,7 +951,7 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` }, { "data": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "end": 5, @@ -708,19 +967,19 @@ exports[`patches link fields > richTextLinkNode > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link fields > richTextLinkNode > slice 1`] = ` +exports[`patches content relationship fields > richTextLinkNode > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": [ @@ -733,7 +992,7 @@ exports[`patches link fields > richTextLinkNode > slice 1`] = ` }, { "data": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "end": 5, @@ -758,7 +1017,7 @@ exports[`patches link fields > richTextLinkNode > slice 1`] = ` }, { "data": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "end": 5, @@ -771,14 +1030,14 @@ exports[`patches link fields > richTextLinkNode > slice 1`] = ` }, ], }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link fields > richTextLinkNode > static zone 1`] = ` +exports[`patches content relationship fields > richTextLinkNode > static zone 1`] = ` { "field": [ { @@ -790,7 +1049,7 @@ exports[`patches link fields > richTextLinkNode > static zone 1`] = ` }, { "data": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "end": 5, diff --git a/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap index 98f4ebb3..3662a201 100644 --- a/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-image.test.ts.snap @@ -1,52 +1,34 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`patches image fields > existing > group 1`] = ` +exports[`patches image fields (from Prismic) > empty > group 1`] = ` { "group": [ { "field": { "alt": null, "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "dimensions": null, + "id": null, + "url": null, }, }, ], } `; -exports[`patches image fields > existing > shared slice 1`] = ` +exports[`patches image fields (from Prismic) > empty > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { "alt": null, "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "dimensions": null, + "id": null, + "url": null, }, }, ], @@ -54,71 +36,44 @@ exports[`patches image fields > existing > shared slice 1`] = ` "field": { "alt": null, "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "dimensions": null, + "id": null, + "url": null, }, "group": [ { "field": { "alt": null, "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "dimensions": null, + "id": null, + "url": null, }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches image fields > existing > slice 1`] = ` +exports[`patches image fields (from Prismic) > empty > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { "alt": null, "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "dimensions": null, + "id": null, + "url": null, }, }, ], @@ -126,565 +81,834 @@ exports[`patches image fields > existing > slice 1`] = ` "field": { "alt": null, "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "dimensions": null, + "id": null, + "url": null, }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches image fields > existing > static zone 1`] = ` +exports[`patches image fields (from Prismic) > empty > static zone 1`] = ` { "field": { "alt": null, "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, - }, - "id": "id-existing", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + "dimensions": null, + "id": null, + "url": null, }, } `; -exports[`patches image fields > new > group 1`] = ` +exports[`patches image fields (from Prismic) > simple > group 1`] = ` { "group": [ { "field": { - "alt": null, + "alt": "Morbi tincidunt ornare massa eget egestas purus viverra accumsan in nisl nisi scelerisque eu", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#fc26fe", + "x": 1643, + "y": -110, + "zoom": 1.3701173345023108, }, - "id": "dfdd322ca9d", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "id": "d0985c09900", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724", }, }, ], } `; -exports[`patches image fields > new > shared slice 1`] = ` +exports[`patches image fields (from Prismic) > simple > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "alt": null, + "alt": "Nam libero justo laoreet sit amet cursus sit amet dictum sit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#d9ed25", + "x": 1472, + "y": -1646, + "zoom": 1.0945668828428181, }, - "id": "0eddbd0255b", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", }, }, ], "primary": { "field": { - "alt": null, + "alt": "Massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#04f4f5", + "x": 27, + "y": -816, + "zoom": 1.9856937117841313, }, - "id": "0eddbd0255b", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", }, "group": [ { "field": { - "alt": null, + "alt": "Egestas maecenas pharetra convallis posuere morbi leo urna", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#d42f5c", + "x": 1778, + "y": -875, + "zoom": 1.6943119181539659, }, - "id": "0eddbd0255b", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches image fields > new > slice 1`] = ` +exports[`patches image fields (from Prismic) > simple > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "alt": null, + "alt": "Molestie lorem ipsum dolor sit amet consectetur adipiscing elit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#ff72de", + "x": 329, + "y": -450, + "zoom": 1.9747006336633817, }, - "id": "2beb55ec2f4", - "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", + "id": "07a35bc5aa2", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg", }, }, ], "primary": { "field": { - "alt": null, + "alt": "Tincidunt vitae semper quis lectus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#860cba", + "x": -1925, + "y": -2408, + "zoom": 1.4001176423393464, }, - "id": "2beb55ec2f4", - "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", + "id": "07a35bc5aa2", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches image fields > new > static zone 1`] = ` +exports[`patches image fields (from Prismic) > simple > static zone 1`] = ` { "field": { - "alt": null, + "alt": "Velit laoreet id donec ultrices tincidunt arcu non sodales neque sodales ut etiam sit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#1d17b0", + "x": -1859, + "y": 1185, + "zoom": 1.2989647419422317, }, - "id": "bbad670dad7", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + "id": "a95cc61c373", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg", }, } `; -exports[`patches image fields > otherRepository > group 1`] = ` +exports[`patches image fields (from Prismic) > withSpecialTypeThumbnail > group 1`] = ` { "group": [ { "field": { - "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", + "alt": "Morbi tincidunt ornare massa eget egestas purus viverra accumsan in nisl nisi scelerisque eu", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#cfc26f", - "x": -2885, - "y": -2419, - "zoom": 1.7509753524211642, + "background": "#fc26fe", + "x": 1643, + "y": -110, + "zoom": 1.3701173345023108, + }, + "id": "d0985c09900", + "type": { + "alt": "Morbi tincidunt ornare massa eget egestas purus viverra accumsan in nisl nisi scelerisque eu", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#fc26fe", + "x": 1643, + "y": -110, + "zoom": 1.3701173345023108, + }, + "id": "d0985c09900", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?some=other&query=params", }, - "id": "8d0985c0990", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?some=query", }, }, ], } `; -exports[`patches image fields > otherRepository > shared slice 1`] = ` +exports[`patches image fields (from Prismic) > withSpecialTypeThumbnail > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", + "alt": "Nam libero justo laoreet sit amet cursus sit amet dictum sit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#9ed252", - "x": -1720, - "y": -191, - "zoom": 1.6165685319845957, + "background": "#d9ed25", + "x": 1472, + "y": -1646, + "zoom": 1.0945668828428181, }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + "id": "b35eaa8ebee", + "type": { + "alt": "Nam libero justo laoreet sit amet cursus sit amet dictum sit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#d9ed25", + "x": 1472, + "y": -1646, + "zoom": 1.0945668828428181, + }, + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], "primary": { "field": { - "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", + "alt": "Massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#4f4f59", - "x": 3094, - "y": -2976, - "zoom": 1.0855120641844143, + "background": "#04f4f5", + "x": 27, + "y": -816, + "zoom": 1.9856937117841313, }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + "id": "b35eaa8ebee", + "type": { + "alt": "Massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#04f4f5", + "x": 27, + "y": -816, + "zoom": 1.9856937117841313, + }, + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, "group": [ { "field": { - "alt": "Id aliquet risus feugiat in", + "alt": "Egestas maecenas pharetra convallis posuere morbi leo urna", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#42f5cf", - "x": 427, - "y": 731, - "zoom": 1.6417661415510265, + "background": "#d42f5c", + "x": 1778, + "y": -875, + "zoom": 1.6943119181539659, }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5", + "id": "b35eaa8ebee", + "type": { + "alt": "Egestas maecenas pharetra convallis posuere morbi leo urna", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#d42f5c", + "x": 1778, + "y": -875, + "zoom": 1.6943119181539659, + }, + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches image fields > otherRepository > slice 1`] = ` +exports[`patches image fields (from Prismic) > withSpecialTypeThumbnail > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "alt": "Pellentesque nec nam aliquam sem", + "alt": "Molestie lorem ipsum dolor sit amet consectetur adipiscing elit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#f72dec", - "x": 2027, - "y": 7, - "zoom": 1.9216652845315787, + "background": "#ff72de", + "x": 329, + "y": -450, + "zoom": 1.9747006336633817, + }, + "id": "07a35bc5aa2", + "type": { + "alt": "Molestie lorem ipsum dolor sit amet consectetur adipiscing elit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#ff72de", + "x": 329, + "y": -450, + "zoom": 1.9747006336633817, + }, + "id": "07a35bc5aa2", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", }, - "id": "7a35bc5aa2f", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", }, }, ], "primary": { "field": { - "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", + "alt": "Tincidunt vitae semper quis lectus", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#60cbac", - "x": -484, - "y": -2038, - "zoom": 1.8400009569805118, + "background": "#860cba", + "x": -1925, + "y": -2408, + "zoom": 1.4001176423393464, + }, + "id": "07a35bc5aa2", + "type": { + "alt": "Tincidunt vitae semper quis lectus", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#860cba", + "x": -1925, + "y": -2408, + "zoom": 1.4001176423393464, + }, + "id": "07a35bc5aa2", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", }, - "id": "7a35bc5aa2f", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches image fields > otherRepository > static zone 1`] = ` +exports[`patches image fields (from Prismic) > withSpecialTypeThumbnail > static zone 1`] = ` { "field": { - "alt": "Gravida rutrum quisque non tellus orci", + "alt": "Velit laoreet id donec ultrices tincidunt arcu non sodales neque sodales ut etiam sit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#b1d17b", - "x": 3291, - "y": -730, - "zoom": 1.061566393836766, + "background": "#1d17b0", + "x": -1859, + "y": 1185, + "zoom": 1.2989647419422317, + }, + "id": "a95cc61c373", + "type": { + "alt": "Velit laoreet id donec ultrices tincidunt arcu non sodales neque sodales ut etiam sit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#1d17b0", + "x": -1859, + "y": 1185, + "zoom": 1.2989647419422317, + }, + "id": "a95cc61c373", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", }, - "id": "4a95cc61c37", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", }, } `; -exports[`patches image fields > otherRepositoryEmpty > group 1`] = ` +exports[`patches image fields (from Prismic) > withThumbnails > group 1`] = ` { "group": [ { "field": { - "alt": null, + "alt": "Morbi tincidunt ornare massa eget egestas purus viverra accumsan in nisl nisi scelerisque eu", "copyright": null, - "dimensions": null, - "id": null, - "url": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#fc26fe", + "x": 1643, + "y": -110, + "zoom": 1.3701173345023108, + }, + "id": "d0985c09900", + "square": { + "alt": "Morbi tincidunt ornare massa eget egestas purus viverra accumsan in nisl nisi scelerisque eu", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#fc26fe", + "x": 1643, + "y": -110, + "zoom": 1.3701173345023108, + }, + "id": "d0985c09900", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?some=query", }, }, ], } `; -exports[`patches image fields > otherRepositoryEmpty > shared slice 1`] = ` +exports[`patches image fields (from Prismic) > withThumbnails > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "alt": null, + "alt": "Nam libero justo laoreet sit amet cursus sit amet dictum sit", "copyright": null, - "dimensions": null, - "id": null, - "url": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#d9ed25", + "x": 1472, + "y": -1646, + "zoom": 1.0945668828428181, + }, + "id": "b35eaa8ebee", + "square": { + "alt": "Nam libero justo laoreet sit amet cursus sit amet dictum sit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#d9ed25", + "x": 1472, + "y": -1646, + "zoom": 1.0945668828428181, + }, + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], "primary": { "field": { - "alt": null, + "alt": "Massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit", "copyright": null, - "dimensions": null, - "id": null, - "url": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#04f4f5", + "x": 27, + "y": -816, + "zoom": 1.9856937117841313, + }, + "id": "b35eaa8ebee", + "square": { + "alt": "Massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#04f4f5", + "x": 27, + "y": -816, + "zoom": 1.9856937117841313, + }, + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, "group": [ { "field": { - "alt": null, + "alt": "Egestas maecenas pharetra convallis posuere morbi leo urna", "copyright": null, - "dimensions": null, - "id": null, - "url": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#d42f5c", + "x": 1778, + "y": -875, + "zoom": 1.6943119181539659, + }, + "id": "b35eaa8ebee", + "square": { + "alt": "Egestas maecenas pharetra convallis posuere morbi leo urna", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#d42f5c", + "x": 1778, + "y": -875, + "zoom": 1.6943119181539659, + }, + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches image fields > otherRepositoryEmpty > slice 1`] = ` +exports[`patches image fields (from Prismic) > withThumbnails > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "alt": null, + "alt": "Molestie lorem ipsum dolor sit amet consectetur adipiscing elit", "copyright": null, - "dimensions": null, - "id": null, - "url": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#ff72de", + "x": 329, + "y": -450, + "zoom": 1.9747006336633817, + }, + "id": "07a35bc5aa2", + "square": { + "alt": "Molestie lorem ipsum dolor sit amet consectetur adipiscing elit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#ff72de", + "x": 329, + "y": -450, + "zoom": 1.9747006336633817, + }, + "id": "07a35bc5aa2", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", + }, + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", }, }, ], "primary": { "field": { - "alt": null, + "alt": "Tincidunt vitae semper quis lectus", "copyright": null, - "dimensions": null, - "id": null, - "url": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#860cba", + "x": -1925, + "y": -2408, + "zoom": 1.4001176423393464, + }, + "id": "07a35bc5aa2", + "square": { + "alt": "Tincidunt vitae semper quis lectus", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#860cba", + "x": -1925, + "y": -2408, + "zoom": 1.4001176423393464, + }, + "id": "07a35bc5aa2", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", + }, + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches image fields > otherRepositoryEmpty > static zone 1`] = ` +exports[`patches image fields (from Prismic) > withThumbnails > static zone 1`] = ` { "field": { - "alt": null, + "alt": "Velit laoreet id donec ultrices tincidunt arcu non sodales neque sodales ut etiam sit", "copyright": null, - "dimensions": null, - "id": null, - "url": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#1d17b0", + "x": -1859, + "y": 1185, + "zoom": 1.2989647419422317, + }, + "id": "a95cc61c373", + "square": { + "alt": "Velit laoreet id donec ultrices tincidunt arcu non sodales neque sodales ut etiam sit", + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#1d17b0", + "x": -1859, + "y": 1185, + "zoom": 1.2989647419422317, + }, + "id": "a95cc61c373", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", + }, + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", }, } `; -exports[`patches image fields > otherRepositoryWithThumbnails > group 1`] = ` +exports[`patches image fields (from Prismic) > withThumbnailsNoAlt > group 1`] = ` { "group": [ { "field": { - "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#cfc26f", - "x": -2885, - "y": -2419, - "zoom": 1.7509753524211642, + "background": "#fc26fe", + "x": 1643, + "y": -110, + "zoom": 1.3701173345023108, }, - "id": "8d0985c0990", + "id": "d0985c09900", "square": { - "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#cfc26f", - "x": -2885, - "y": -2419, - "zoom": 1.7509753524211642, + "background": "#fc26fe", + "x": 1643, + "y": -110, + "zoom": 1.3701173345023108, }, - "id": "8d0985c0990", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + "id": "d0985c09900", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?some=other&query=params", }, - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?some=query", }, }, ], } `; -exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] = ` +exports[`patches image fields (from Prismic) > withThumbnailsNoAlt > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#9ed252", - "x": -1720, - "y": -191, - "zoom": 1.6165685319845957, + "background": "#d9ed25", + "x": 1472, + "y": -1646, + "zoom": 1.0945668828428181, }, - "id": "35eaa8ebee4", + "id": "b35eaa8ebee", "square": { - "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#9ed252", - "x": -1720, - "y": -191, - "zoom": 1.6165685319845957, + "background": "#d9ed25", + "x": 1472, + "y": -1646, + "zoom": 1.0945668828428181, }, - "id": "35eaa8ebee4", + "id": "b35eaa8ebee", "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", @@ -693,33 +917,33 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] ], "primary": { "field": { - "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#4f4f59", - "x": 3094, - "y": -2976, - "zoom": 1.0855120641844143, + "background": "#04f4f5", + "x": 27, + "y": -816, + "zoom": 1.9856937117841313, }, - "id": "35eaa8ebee4", + "id": "b35eaa8ebee", "square": { - "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#4f4f59", - "x": 3094, - "y": -2976, - "zoom": 1.0855120641844143, + "background": "#04f4f5", + "x": 27, + "y": -816, + "zoom": 1.9856937117841313, }, - "id": "35eaa8ebee4", + "id": "b35eaa8ebee", "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", }, "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", @@ -727,169 +951,505 @@ exports[`patches image fields > otherRepositoryWithThumbnails > shared slice 1`] "group": [ { "field": { - "alt": "Id aliquet risus feugiat in", + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#d42f5c", + "x": 1778, + "y": -875, + "zoom": 1.6943119181539659, + }, + "id": "b35eaa8ebee", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#d42f5c", + "x": 1778, + "y": -875, + "zoom": 1.6943119181539659, + }, + "id": "b35eaa8ebee", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + }, + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", + }, + ], +} +`; + +exports[`patches image fields (from Prismic) > withThumbnailsNoAlt > slice 1`] = ` +{ + "slices": [ + { + "id": "5306297c5ed", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#ff72de", + "x": 329, + "y": -450, + "zoom": 1.9747006336633817, + }, + "id": "07a35bc5aa2", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#ff72de", + "x": 329, + "y": -450, + "zoom": 1.9747006336633817, + }, + "id": "07a35bc5aa2", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", + }, + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#860cba", + "x": -1925, + "y": -2408, + "zoom": 1.4001176423393464, + }, + "id": "07a35bc5aa2", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#860cba", + "x": -1925, + "y": -2408, + "zoom": 1.4001176423393464, + }, + "id": "07a35bc5aa2", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", + }, + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", + }, + }, + "slice_label": "Vel", + "slice_type": "hac_habitasse", + }, + ], +} +`; + +exports[`patches image fields (from Prismic) > withThumbnailsNoAlt > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#1d17b0", + "x": -1859, + "y": 1185, + "zoom": 1.2989647419422317, + }, + "id": "a95cc61c373", + "square": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "#1d17b0", + "x": -1859, + "y": 1185, + "zoom": 1.2989647419422317, + }, + "id": "a95cc61c373", + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=other&query=params", + }, + "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?some=query", + }, +} +`; + +exports[`patches image fields > existing > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "asset.id-existing", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + }, + }, + ], +} +`; + +exports[`patches image fields > existing > shared slice 1`] = ` +{ + "slices": [ + { + "id": "9b9dd0f2c3f", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "asset.id-existing", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "asset.id-existing", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", + }, + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "asset.id-existing", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", + }, + ], +} +`; + +exports[`patches image fields > existing > slice 1`] = ` +{ + "slices": [ + { + "id": "5306297c5ed", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "asset.id-existing", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "asset.id-existing", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + }, + }, + "slice_label": "Vel", + "slice_type": "hac_habitasse", + }, + ], +} +`; + +exports[`patches image fields > existing > static zone 1`] = ` +{ + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "asset.id-existing", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + }, +} +`; + +exports[`patches image fields > new > group 1`] = ` +{ + "group": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "fdd322ca9d5", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + }, + }, + ], +} +`; + +exports[`patches image fields > new > shared slice 1`] = ` +{ + "slices": [ + { + "id": "9b9dd0f2c3f", + "items": [ + { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + }, + }, + ], + "primary": { + "field": { + "alt": null, + "copyright": null, + "dimensions": { + "height": 1, + "width": 1, + }, + "edit": { + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, + }, + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + }, + "group": [ + { + "field": { + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#42f5cf", - "x": 427, - "y": 731, - "zoom": 1.6417661415510265, - }, - "id": "35eaa8ebee4", - "square": { - "alt": "Id aliquet risus feugiat in", - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#42f5cf", - "x": 427, - "y": 731, - "zoom": 1.6417661415510265, - }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches image fields > otherRepositoryWithThumbnails > slice 1`] = ` +exports[`patches image fields > new > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "alt": "Pellentesque nec nam aliquam sem", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#f72dec", - "x": 2027, - "y": 7, - "zoom": 1.9216652845315787, - }, - "id": "7a35bc5aa2f", - "square": { - "alt": "Pellentesque nec nam aliquam sem", - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#f72dec", - "x": 2027, - "y": 7, - "zoom": 1.9216652845315787, - }, - "id": "7a35bc5aa2f", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", + "id": "82beb55ec2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, ], "primary": { "field": { - "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#60cbac", - "x": -484, - "y": -2038, - "zoom": 1.8400009569805118, - }, - "id": "7a35bc5aa2f", - "square": { - "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#60cbac", - "x": -484, - "y": -2038, - "zoom": 1.8400009569805118, - }, - "id": "7a35bc5aa2f", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", + "id": "82beb55ec2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches image fields > otherRepositoryWithThumbnails > static zone 1`] = ` +exports[`patches image fields > new > static zone 1`] = ` { "field": { - "alt": "Gravida rutrum quisque non tellus orci", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#b1d17b", - "x": 3291, - "y": -730, - "zoom": 1.061566393836766, - }, - "id": "4a95cc61c37", - "square": { - "alt": "Gravida rutrum quisque non tellus orci", - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#b1d17b", - "x": 3291, - "y": -730, - "zoom": 1.061566393836766, - }, - "id": "4a95cc61c37", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=query", + "id": "bad670dad77", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, } `; -exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = ` +exports[`patches image fields > newLongForm > group 1`] = ` { "group": [ { @@ -901,40 +1461,24 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > group 1`] = "width": 1, }, "edit": { - "background": "#cfc26f", - "x": -2885, - "y": -2419, - "zoom": 1.7509753524211642, - }, - "id": "8d0985c0990", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#cfc26f", - "x": -2885, - "y": -2419, - "zoom": 1.7509753524211642, - }, - "id": "8d0985c0990", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + "id": "fdd322ca9d5", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, }, ], } `; -exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slice 1`] = ` +exports[`patches image fields > newLongForm > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { @@ -945,29 +1489,13 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#9ed252", - "x": -1720, - "y": -191, - "zoom": 1.6165685319845957, - }, - "id": "35eaa8ebee4", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#9ed252", - "x": -1720, - "y": -191, - "zoom": 1.6165685319845957, - }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], @@ -980,29 +1508,13 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#4f4f59", - "x": 3094, - "y": -2976, - "zoom": 1.0855120641844143, - }, - "id": "35eaa8ebee4", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#4f4f59", - "x": 3094, - "y": -2976, - "zoom": 1.0855120641844143, - }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, "group": [ { @@ -1014,47 +1526,31 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > shared slic "width": 1, }, "edit": { - "background": "#42f5cf", - "x": 427, - "y": 731, - "zoom": 1.6417661415510265, - }, - "id": "35eaa8ebee4", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#42f5cf", - "x": 427, - "y": 731, - "zoom": 1.6417661415510265, - }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = ` +exports[`patches image fields > newLongForm > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { @@ -1065,29 +1561,13 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "width": 1, }, "edit": { - "background": "#f72dec", - "x": 2027, - "y": 7, - "zoom": 1.9216652845315787, - }, - "id": "7a35bc5aa2f", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#f72dec", - "x": 2027, - "y": 7, - "zoom": 1.9216652845315787, - }, - "id": "7a35bc5aa2f", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", + "id": "82beb55ec2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, ], @@ -1100,39 +1580,23 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > slice 1`] = "width": 1, }, "edit": { - "background": "#60cbac", - "x": -484, - "y": -2038, - "zoom": 1.8400009569805118, - }, - "id": "7a35bc5aa2f", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#60cbac", - "x": -484, - "y": -2038, - "zoom": 1.8400009569805118, - }, - "id": "7a35bc5aa2f", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", + "id": "82beb55ec2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone 1`] = ` +exports[`patches image fields > newLongForm > static zone 1`] = ` { "field": { "alt": null, @@ -1142,309 +1606,293 @@ exports[`patches image fields > otherRepositoryWithThumbnailsNoAlt > static zone "width": 1, }, "edit": { - "background": "#b1d17b", - "x": 3291, - "y": -730, - "zoom": 1.061566393836766, - }, - "id": "4a95cc61c37", - "square": { - "alt": null, - "copyright": null, - "dimensions": { - "height": 1, - "width": 1, - }, - "edit": { - "background": "#b1d17b", - "x": 3291, - "y": -730, - "zoom": 1.061566393836766, - }, - "id": "4a95cc61c37", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=other&query=params", + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=query", + "id": "bad670dad77", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, } `; -exports[`patches image fields > otherRepositoryWithTypeThumbnail > group 1`] = ` +exports[`patches image fields > newThumbnails > group 1`] = ` { "group": [ { "field": { - "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#cfc26f", - "x": -2885, - "y": -2419, - "zoom": 1.7509753524211642, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "8d0985c0990", - "type": { - "alt": "Massa id neque aliquam vestibulum morbi blandit cursus risus", + "id": "fdd322ca9d5", + "square": { + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#cfc26f", - "x": -2885, - "y": -2419, - "zoom": 1.7509753524211642, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "8d0985c0990", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=other&query=params", + "id": "fdd322ca9d5", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?some=query", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, }, ], } `; -exports[`patches image fields > otherRepositoryWithTypeThumbnail > shared slice 1`] = ` +exports[`patches image fields > newThumbnails > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#9ed252", - "x": -1720, - "y": -191, - "zoom": 1.6165685319845957, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "35eaa8ebee4", - "type": { - "alt": "Fermentum odio eu feugiat pretium nibh ipsum consequat", + "id": "c0eddbd0255", + "square": { + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#9ed252", - "x": -1720, - "y": -191, - "zoom": 1.6165685319845957, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], "primary": { "field": { - "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#4f4f59", - "x": 3094, - "y": -2976, - "zoom": 1.0855120641844143, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "35eaa8ebee4", - "type": { - "alt": "Pellentesque pulvinar pellentesque habitant morbi tristique senectus et netus et malesuada fames ac", + "id": "c0eddbd0255", + "square": { + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#4f4f59", - "x": 3094, - "y": -2976, - "zoom": 1.0855120641844143, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, "group": [ { "field": { - "alt": "Id aliquet risus feugiat in", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#42f5cf", - "x": 427, - "y": 731, - "zoom": 1.6417661415510265, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "35eaa8ebee4", - "type": { - "alt": "Id aliquet risus feugiat in", + "id": "c0eddbd0255", + "square": { + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#42f5cf", - "x": 427, - "y": 731, - "zoom": 1.6417661415510265, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "35eaa8ebee4", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=other&query=params", + "id": "c0eddbd0255", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?some=query", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches image fields > otherRepositoryWithTypeThumbnail > slice 1`] = ` +exports[`patches image fields > newThumbnails > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "alt": "Pellentesque nec nam aliquam sem", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#f72dec", - "x": 2027, - "y": 7, - "zoom": 1.9216652845315787, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "7a35bc5aa2f", - "type": { - "alt": "Pellentesque nec nam aliquam sem", + "id": "82beb55ec2f", + "square": { + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#f72dec", - "x": 2027, - "y": 7, - "zoom": 1.9216652845315787, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "7a35bc5aa2f", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", + "id": "82beb55ec2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, ], "primary": { "field": { - "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#60cbac", - "x": -484, - "y": -2038, - "zoom": 1.8400009569805118, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "7a35bc5aa2f", - "type": { - "alt": "Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit", + "id": "82beb55ec2f", + "square": { + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#60cbac", - "x": -484, - "y": -2038, - "zoom": 1.8400009569805118, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "7a35bc5aa2f", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=other&query=params", + "id": "82beb55ec2f", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?some=query", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches image fields > otherRepositoryWithTypeThumbnail > static zone 1`] = ` +exports[`patches image fields > newThumbnails > static zone 1`] = ` { "field": { - "alt": "Gravida rutrum quisque non tellus orci", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#b1d17b", - "x": 3291, - "y": -730, - "zoom": 1.061566393836766, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "4a95cc61c37", - "type": { - "alt": "Gravida rutrum quisque non tellus orci", + "id": "bad670dad77", + "square": { + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#b1d17b", - "x": 3291, - "y": -730, - "zoom": 1.061566393836766, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "4a95cc61c37", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=other&query=params", + "id": "bad670dad77", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?some=query", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, } `; diff --git a/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap index a6d213fd..ceb749b4 100644 --- a/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap @@ -1,17 +1,269 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`patches link to media fields > existing > group 1`] = ` +exports[`patches link to media fields (from Prismic) > inRichText > group 1`] = ` +{ + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "fc26fe8d098", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], +} +`; + +exports[`patches link to media fields (from Prismic) > inRichText > shared slice 1`] = ` +{ + "slices": [ + { + "id": "9b9dd0f2c3f", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "961adcf1f5d", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "961adcf1f5d", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + "group": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "961adcf1f5d", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + }, + "slice_label": null, + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", + }, + ], +} +`; + +exports[`patches link to media fields (from Prismic) > inRichText > slice 1`] = ` +{ + "slices": [ + { + "id": "5306297c5ed", + "items": [ + { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "54f4a69ff72", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + ], + "primary": { + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "54f4a69ff72", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], + }, + "slice_label": "Vel", + "slice_type": "hac_habitasse", + }, + ], +} +`; + +exports[`patches link to media fields (from Prismic) > inRichText > static zone 1`] = ` +{ + "field": [ + { + "spans": [ + { + "end": 5, + "start": 0, + "type": "strong", + }, + { + "data": { + "height": "1", + "id": "1d17b04a95c", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "width": "1", + }, + "end": 5, + "start": 0, + "type": "hyperlink", + }, + ], + "text": "lorem", + "type": "paragraph", + }, + ], +} +`; + +exports[`patches link to media fields (from Prismic) > simple > group 1`] = ` { "group": [ { "field": { "height": "1", - "id": "id-existing", + "id": "fc26fe8d098", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", "width": "1", }, }, @@ -19,21 +271,21 @@ exports[`patches link to media fields > existing > group 1`] = ` } `; -exports[`patches link to media fields > existing > shared slice 1`] = ` +exports[`patches link to media fields (from Prismic) > simple > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { "height": "1", - "id": "id-existing", + "id": "961adcf1f5d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", "width": "1", }, }, @@ -41,53 +293,53 @@ exports[`patches link to media fields > existing > shared slice 1`] = ` "primary": { "field": { "height": "1", - "id": "id-existing", + "id": "961adcf1f5d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", "width": "1", }, "group": [ { "field": { "height": "1", - "id": "id-existing", + "id": "961adcf1f5d", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", "width": "1", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link to media fields > existing > slice 1`] = ` +exports[`patches link to media fields (from Prismic) > simple > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { "height": "1", - "id": "id-existing", + "id": "54f4a69ff72", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", "width": "1", }, }, @@ -95,145 +347,159 @@ exports[`patches link to media fields > existing > slice 1`] = ` "primary": { "field": { "height": "1", - "id": "id-existing", + "id": "54f4a69ff72", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", "width": "1", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link to media fields > existing > static zone 1`] = ` +exports[`patches link to media fields (from Prismic) > simple > static zone 1`] = ` { "field": { "height": "1", - "id": "id-existing", + "id": "1d17b04a95c", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, } `; -exports[`patches link to media fields > existingNonImage > group 1`] = ` +exports[`patches link to media fields > existing > group 1`] = ` { "group": [ { "field": { - "id": "id-existing", - "kind": "document", + "height": "1", + "id": "asset.id-existing", + "kind": "image", "link_type": "Media", - "name": "foo.pdf", + "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "width": "1", }, }, ], } `; -exports[`patches link to media fields > existingNonImage > shared slice 1`] = ` +exports[`patches link to media fields > existing > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { - "id": "id-existing", - "kind": "document", + "height": "1", + "id": "asset.id-existing", + "kind": "image", "link_type": "Media", - "name": "foo.pdf", + "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", + "width": "1", }, }, ], "primary": { "field": { - "id": "id-existing", - "kind": "document", + "height": "1", + "id": "asset.id-existing", + "kind": "image", "link_type": "Media", - "name": "foo.pdf", + "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", + "width": "1", }, "group": [ { "field": { - "id": "id-existing", - "kind": "document", + "height": "1", + "id": "asset.id-existing", + "kind": "image", "link_type": "Media", - "name": "foo.pdf", + "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", + "width": "1", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link to media fields > existingNonImage > slice 1`] = ` +exports[`patches link to media fields > existing > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { - "id": "id-existing", - "kind": "document", + "height": "1", + "id": "asset.id-existing", + "kind": "image", "link_type": "Media", - "name": "foo.pdf", + "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + "width": "1", }, }, ], "primary": { "field": { - "id": "id-existing", - "kind": "document", + "height": "1", + "id": "asset.id-existing", + "kind": "image", "link_type": "Media", - "name": "foo.pdf", + "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + "width": "1", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link to media fields > existingNonImage > static zone 1`] = ` +exports[`patches link to media fields > existing > static zone 1`] = ` { "field": { - "id": "id-existing", - "kind": "document", + "height": "1", + "id": "asset.id-existing", + "kind": "image", "link_type": "Media", - "name": "foo.pdf", + "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "width": "1", }, } `; @@ -244,12 +510,12 @@ exports[`patches link to media fields > new > group 1`] = ` { "field": { "height": "1", - "id": "dfdd322ca9d", + "id": "fdd322ca9d5", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", "width": "1", }, }, @@ -261,17 +527,17 @@ exports[`patches link to media fields > new > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { "height": "1", - "id": "0eddbd0255b", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, }, @@ -279,33 +545,33 @@ exports[`patches link to media fields > new > shared slice 1`] = ` "primary": { "field": { "height": "1", - "id": "0eddbd0255b", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "group": [ { "field": { "height": "1", - "id": "0eddbd0255b", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } @@ -315,67 +581,180 @@ exports[`patches link to media fields > new > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", + "items": [ + { + "field": { + "height": "1", + "id": "82beb55ec2f", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + "width": "1", + }, + }, + ], + "primary": { + "field": { + "height": "1", + "id": "82beb55ec2f", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", + "width": "1", + }, + }, + "slice_label": "Vel", + "slice_type": "hac_habitasse", + }, + ], +} +`; + +exports[`patches link to media fields > new > static zone 1`] = ` +{ + "field": { + "height": "1", + "id": "bad670dad77", + "kind": "image", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "width": "1", + }, +} +`; + +exports[`patches link to media fields > newNonImage > group 1`] = ` +{ + "group": [ + { + "field": { + "id": "fdd322ca9d5", + "kind": "document", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + }, + }, + ], +} +`; + +exports[`patches link to media fields > newNonImage > shared slice 1`] = ` +{ + "slices": [ + { + "id": "9b9dd0f2c3f", + "items": [ + { + "field": { + "id": "c0eddbd0255", + "kind": "document", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + }, + }, + ], + "primary": { + "field": { + "id": "c0eddbd0255", + "kind": "document", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + }, + "group": [ + { + "field": { + "id": "c0eddbd0255", + "kind": "document", + "link_type": "Media", + "name": "default.jpg", + "size": "1", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", + }, + ], +} +`; + +exports[`patches link to media fields > newNonImage > slice 1`] = ` +{ + "slices": [ + { + "id": "5306297c5ed", "items": [ { "field": { - "height": "1", - "id": "2beb55ec2f4", - "kind": "image", + "id": "82beb55ec2f", + "kind": "document", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", - "width": "1", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, ], "primary": { "field": { - "height": "1", - "id": "2beb55ec2f4", - "kind": "image", + "id": "82beb55ec2f", + "kind": "document", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", - "width": "1", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link to media fields > new > static zone 1`] = ` +exports[`patches link to media fields > newNonImage > static zone 1`] = ` { "field": { - "height": "1", - "id": "bbad670dad7", - "kind": "image", + "id": "bad670dad77", + "kind": "document", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - "width": "1", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, } `; -exports[`patches link to media fields > otherRepository > group 1`] = ` +exports[`patches link to media fields > newWithText > group 1`] = ` { "group": [ { "field": { "height": "1", - "id": "cfc26fe8d09", + "id": "fdd322ca9d5", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", + "text": "foo", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", "width": "1", }, }, @@ -383,21 +762,22 @@ exports[`patches link to media fields > otherRepository > group 1`] = ` } `; -exports[`patches link to media fields > otherRepository > shared slice 1`] = ` +exports[`patches link to media fields > newWithText > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": { "height": "1", - "id": "61adcf1f5db", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "text": "foo", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, }, @@ -405,53 +785,56 @@ exports[`patches link to media fields > otherRepository > shared slice 1`] = ` "primary": { "field": { "height": "1", - "id": "61adcf1f5db", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "text": "foo", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "group": [ { "field": { "height": "1", - "id": "61adcf1f5db", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", + "text": "foo", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches link to media fields > otherRepository > slice 1`] = ` +exports[`patches link to media fields > newWithText > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": { "height": "1", - "id": "4f4a69ff72d", + "id": "82beb55ec2f", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "text": "foo", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", "width": "1", }, }, @@ -459,32 +842,34 @@ exports[`patches link to media fields > otherRepository > slice 1`] = ` "primary": { "field": { "height": "1", - "id": "4f4a69ff72d", + "id": "82beb55ec2f", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", + "text": "foo", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", "width": "1", }, }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches link to media fields > otherRepository > static zone 1`] = ` +exports[`patches link to media fields > newWithText > static zone 1`] = ` { "field": { "height": "1", - "id": "b1d17b04a95", + "id": "bad670dad77", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "text": "foo", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", "width": "1", }, } @@ -505,12 +890,12 @@ exports[`patches link to media fields > richTextExisting > group 1`] = ` { "data": { "height": "1", - "id": "id-existing", + "id": "asset.id-existing", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", "width": "1", }, "end": 5, @@ -531,7 +916,7 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": [ @@ -545,12 +930,12 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` { "data": { "height": "1", - "id": "id-existing", + "id": "asset.id-existing", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", "width": "1", }, "end": 5, @@ -576,12 +961,12 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` { "data": { "height": "1", - "id": "id-existing", + "id": "asset.id-existing", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", "width": "1", }, "end": 5, @@ -606,12 +991,12 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` { "data": { "height": "1", - "id": "id-existing", + "id": "asset.id-existing", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", "width": "1", }, "end": 5, @@ -627,9 +1012,9 @@ exports[`patches link to media fields > richTextExisting > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } @@ -639,7 +1024,7 @@ exports[`patches link to media fields > richTextExisting > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": [ @@ -653,12 +1038,12 @@ exports[`patches link to media fields > richTextExisting > slice 1`] = ` { "data": { "height": "1", - "id": "id-existing", + "id": "asset.id-existing", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", "width": "1", }, "end": 5, @@ -684,12 +1069,12 @@ exports[`patches link to media fields > richTextExisting > slice 1`] = ` { "data": { "height": "1", - "id": "id-existing", + "id": "asset.id-existing", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", "width": "1", }, "end": 5, @@ -702,8 +1087,8 @@ exports[`patches link to media fields > richTextExisting > slice 1`] = ` }, ], }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } @@ -722,12 +1107,12 @@ exports[`patches link to media fields > richTextExisting > static zone 1`] = ` { "data": { "height": "1", - "id": "id-existing", + "id": "asset.id-existing", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", "width": "1", }, "end": 5, @@ -757,12 +1142,12 @@ exports[`patches link to media fields > richTextNew > group 1`] = ` { "data": { "height": "1", - "id": "dfdd322ca9d", + "id": "fdd322ca9d5", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", "width": "1", }, "end": 5, @@ -783,7 +1168,7 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": [ @@ -797,12 +1182,12 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` { "data": { "height": "1", - "id": "0eddbd0255b", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "end": 5, @@ -828,12 +1213,12 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` { "data": { "height": "1", - "id": "0eddbd0255b", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "end": 5, @@ -858,12 +1243,12 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` { "data": { "height": "1", - "id": "0eddbd0255b", + "id": "c0eddbd0255", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", "width": "1", }, "end": 5, @@ -879,9 +1264,9 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } @@ -891,7 +1276,7 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": [ @@ -905,12 +1290,12 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` { "data": { "height": "1", - "id": "2beb55ec2f4", + "id": "82beb55ec2f", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", "width": "1", }, "end": 5, @@ -936,12 +1321,12 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` { "data": { "height": "1", - "id": "2beb55ec2f4", + "id": "82beb55ec2f", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1444464666168-49d633b86797?w=4844&h=3234&fit=crop", + "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", "width": "1", }, "end": 5, @@ -954,8 +1339,8 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` }, ], }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } @@ -974,264 +1359,12 @@ exports[`patches link to media fields > richTextNew > static zone 1`] = ` { "data": { "height": "1", - "id": "bbad670dad7", - "kind": "image", - "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - "width": "1", - }, - "end": 5, - "start": 0, - "type": "hyperlink", - }, - ], - "text": "lorem", - "type": "paragraph", - }, - ], -} -`; - -exports[`patches link to media fields > richTextOtherRepository > group 1`] = ` -{ - "group": [ - { - "field": [ - { - "spans": [ - { - "end": 5, - "start": 0, - "type": "strong", - }, - { - "data": { - "height": "1", - "id": "cfc26fe8d09", - "kind": "image", - "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", - "width": "1", - }, - "end": 5, - "start": 0, - "type": "hyperlink", - }, - ], - "text": "lorem", - "type": "paragraph", - }, - ], - }, - ], -} -`; - -exports[`patches link to media fields > richTextOtherRepository > shared slice 1`] = ` -{ - "slices": [ - { - "id": "b9dd0f2c3f8", - "items": [ - { - "field": [ - { - "spans": [ - { - "end": 5, - "start": 0, - "type": "strong", - }, - { - "data": { - "height": "1", - "id": "61adcf1f5db", - "kind": "image", - "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", - "width": "1", - }, - "end": 5, - "start": 0, - "type": "hyperlink", - }, - ], - "text": "lorem", - "type": "paragraph", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [ - { - "end": 5, - "start": 0, - "type": "strong", - }, - { - "data": { - "height": "1", - "id": "61adcf1f5db", - "kind": "image", - "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", - "width": "1", - }, - "end": 5, - "start": 0, - "type": "hyperlink", - }, - ], - "text": "lorem", - "type": "paragraph", - }, - ], - "group": [ - { - "field": [ - { - "spans": [ - { - "end": 5, - "start": 0, - "type": "strong", - }, - { - "data": { - "height": "1", - "id": "61adcf1f5db", - "kind": "image", - "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", - "width": "1", - }, - "end": 5, - "start": 0, - "type": "hyperlink", - }, - ], - "text": "lorem", - "type": "paragraph", - }, - ], - }, - ], - }, - "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", - }, - ], -} -`; - -exports[`patches link to media fields > richTextOtherRepository > slice 1`] = ` -{ - "slices": [ - { - "id": "306297c5eda", - "items": [ - { - "field": [ - { - "spans": [ - { - "end": 5, - "start": 0, - "type": "strong", - }, - { - "data": { - "height": "1", - "id": "4f4a69ff72d", - "kind": "image", - "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - "width": "1", - }, - "end": 5, - "start": 0, - "type": "hyperlink", - }, - ], - "text": "lorem", - "type": "paragraph", - }, - ], - }, - ], - "primary": { - "field": [ - { - "spans": [ - { - "end": 5, - "start": 0, - "type": "strong", - }, - { - "data": { - "height": "1", - "id": "4f4a69ff72d", - "kind": "image", - "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", - "width": "1", - }, - "end": 5, - "start": 0, - "type": "hyperlink", - }, - ], - "text": "lorem", - "type": "paragraph", - }, - ], - }, - "slice_label": "Aliquet", - "slice_type": "vel", - }, - ], -} -`; - -exports[`patches link to media fields > richTextOtherRepository > static zone 1`] = ` -{ - "field": [ - { - "spans": [ - { - "end": 5, - "start": 0, - "type": "strong", - }, - { - "data": { - "height": "1", - "id": "b1d17b04a95", + "id": "bad670dad77", "kind": "image", "link_type": "Media", "name": "default.jpg", "size": "1", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", "width": "1", }, "end": 5, diff --git a/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap index 5df8c17c..0aa39cff 100644 --- a/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-rtImageNode.test.ts.snap @@ -1,31 +1,31 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`patches rich text image nodes > existing > group 1`] = ` +exports[`patches rich text image nodes (from Prismic) > simple > group 1`] = ` { "group": [ { "field": [ { "spans": [], - "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", - "type": "heading4", + "text": "Pulvinar elementum integer enim neque volutpat ac tincidunt vitae semper quis. Gravida hendrerit lectus a molestie lorem ipsum dolor sit amet. Mauris sit amet massa vitae tortor condimentum lacinia. Blandit volutpat maecenas volutpat blandit aliquam. Massa eget egestas purus viverra accumsan in nisl nisi scelerisque. Imperdiet dui accumsan sit amet nulla facilisi morbi tempus iaculis.", + "type": "paragraph", }, { - "alt": null, + "alt": "Phasellus faucibus scelerisque eleifend donec pretium vulputate", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#14a045", + "x": -1057, + "y": 1363, + "zoom": 1.3020761265191427, }, - "id": "id-existing", + "id": "097ac4a7efe", "type": "image", - "url": "https://images.unsplash.com/reserve/HgZuGu3gSD6db21T3lxm_San%20Zenone.jpg?w=6373&h=4253&fit=crop", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", }, ], }, @@ -33,35 +33,35 @@ exports[`patches rich text image nodes > existing > group 1`] = ` } `; -exports[`patches rich text image nodes > existing > shared slice 1`] = ` +exports[`patches rich text image nodes (from Prismic) > simple > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": [ { "spans": [], - "text": "A molestie lorem ipsum dolor sit amet consectetur adipiscing elit ut", - "type": "list-item", + "text": "Ultrices In Iaculis Nunc Sed Augue Lacus Viverra Vitae Congue Eu", + "type": "heading4", }, { - "alt": null, + "alt": "Dolor morbi non arcu risus quis varius quam quisque id diam vel", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#073a8e", + "x": -531, + "y": 1549, + "zoom": 1.1496011393958705, }, - "id": "id-existing", + "id": "9378bc2e4da", "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", }, ], }, @@ -70,25 +70,25 @@ exports[`patches rich text image nodes > existing > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", - "type": "preformatted", + "text": "Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet nulla malesuada. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh. Eleifend donec pretium vulputate sapien.", + "type": "paragraph", }, { - "alt": null, + "alt": "A scelerisque purus semper eget duis at tellus at urna condimentum mattis", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#53af6d", + "x": 1608, + "y": -790, + "zoom": 1.8290852631509968, }, - "id": "id-existing", + "id": "9378bc2e4da", "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", }, ], "group": [ @@ -96,68 +96,68 @@ exports[`patches rich text image nodes > existing > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Vel Elit Scelerisque Mauris Pellentesque Pulvinar Pellentesque", - "type": "heading3", + "text": "Consectetur adipiscing elit duis tristique sollicitudin nibh sit amet commodo", + "type": "o-list-item", }, { - "alt": null, + "alt": "Lorem mollis aliquam ut porttitor leo a", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#35eaa8", + "x": -757, + "y": -123, + "zoom": 1.2194779259663722, }, - "id": "id-existing", + "id": "9378bc2e4da", "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", }, ], }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches rich text image nodes > existing > slice 1`] = ` +exports[`patches rich text image nodes (from Prismic) > simple > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": [ { "spans": [], - "text": "Diam Ut Venenatis Tellus", - "type": "heading6", + "text": "Auctor neque vitae tempus quam. At ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget gravida cum.", + "type": "paragraph", }, { - "alt": null, + "alt": "Sed id semper risus in hendrerit gravida rutrum", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#104dde", + "x": -1621, + "y": -870, + "zoom": 1.7278759511485409, }, - "id": "id-existing", + "id": "2003a644c30", "type": "image", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", }, ], }, @@ -166,90 +166,94 @@ exports[`patches rich text image nodes > existing > slice 1`] = ` "field": [ { "spans": [], - "text": "Tincidunt Vitae Semper Quis Lectus Nulla", - "type": "heading4", + "text": "Pharetra et ultrices neque ornare aenean euismod. Mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac.", + "type": "preformatted", }, { - "alt": null, + "alt": "Vitae et leo duis ut diam quam nulla porttitor", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#cc54f4", + "x": 637, + "y": -1697, + "zoom": 1.1481742186351154, }, - "id": "id-existing", + "id": "2003a644c30", "type": "image", - "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", }, ], }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches rich text image nodes > existing > static zone 1`] = ` +exports[`patches rich text image nodes (from Prismic) > simple > static zone 1`] = ` { "field": [ { "spans": [], - "text": "Interdum Velit Euismod", - "type": "heading5", + "text": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis. Tincidunt praesent semper feugiat nibh sed pulvinar proin gravida hendrerit lectus a.", + "type": "paragraph", }, { - "alt": null, + "alt": "Adipiscing bibendum est ultricies integer quis auctor elit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#b943c8", + "x": 592, + "y": 260, + "zoom": 1.3971999251277185, }, - "id": "id-existing", + "id": "b5df4522c1a", "type": "image", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", + "url": "https://images.unsplash.com/photo-1504198266287-1659872e6590", }, ], } `; -exports[`patches rich text image nodes > new > group 1`] = ` +exports[`patches rich text image nodes (from Prismic) > withLinkTo > group 1`] = ` { "group": [ { "field": [ { "spans": [], - "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", - "type": "heading4", + "text": "Pulvinar elementum integer enim neque volutpat ac tincidunt vitae semper quis. Gravida hendrerit lectus a molestie lorem ipsum dolor sit amet. Mauris sit amet massa vitae tortor condimentum lacinia. Blandit volutpat maecenas volutpat blandit aliquam. Massa eget egestas purus viverra accumsan in nisl nisi scelerisque. Imperdiet dui accumsan sit amet nulla facilisi morbi tempus iaculis.", + "type": "paragraph", }, { - "alt": null, + "alt": "Phasellus faucibus scelerisque eleifend donec pretium vulputate", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#14a045", + "x": -1057, + "y": 1363, + "zoom": 1.3020761265191427, + }, + "id": "097ac4a7efe", + "linkTo": { + "id": "other.id-existing", + "link_type": "Document", }, - "id": "c1d5d24feae", "type": "image", - "url": "https://images.unsplash.com/photo-1504198266287-1659872e6590?w=4272&h=2848&fit=crop", + "url": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e", }, ], }, @@ -257,35 +261,39 @@ exports[`patches rich text image nodes > new > group 1`] = ` } `; -exports[`patches rich text image nodes > new > shared slice 1`] = ` +exports[`patches rich text image nodes (from Prismic) > withLinkTo > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": [ { "spans": [], - "text": "A molestie lorem ipsum dolor sit amet consectetur adipiscing elit ut", - "type": "list-item", + "text": "Ultrices In Iaculis Nunc Sed Augue Lacus Viverra Vitae Congue Eu", + "type": "heading4", }, { - "alt": null, + "alt": "Dolor morbi non arcu risus quis varius quam quisque id diam vel", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#073a8e", + "x": -531, + "y": 1549, + "zoom": 1.1496011393958705, + }, + "id": "9378bc2e4da", + "linkTo": { + "id": "other.id-existing", + "link_type": "Document", }, - "id": "961adcf1f5d", "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", }, ], }, @@ -294,25 +302,29 @@ exports[`patches rich text image nodes > new > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", - "type": "preformatted", + "text": "Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet nulla malesuada. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh. Eleifend donec pretium vulputate sapien.", + "type": "paragraph", }, { - "alt": null, + "alt": "A scelerisque purus semper eget duis at tellus at urna condimentum mattis", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#53af6d", + "x": 1608, + "y": -790, + "zoom": 1.8290852631509968, + }, + "id": "9378bc2e4da", + "linkTo": { + "id": "other.id-existing", + "link_type": "Document", }, - "id": "961adcf1f5d", "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", }, ], "group": [ @@ -320,68 +332,76 @@ exports[`patches rich text image nodes > new > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Vel Elit Scelerisque Mauris Pellentesque Pulvinar Pellentesque", - "type": "heading3", + "text": "Consectetur adipiscing elit duis tristique sollicitudin nibh sit amet commodo", + "type": "o-list-item", }, { - "alt": null, + "alt": "Lorem mollis aliquam ut porttitor leo a", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#35eaa8", + "x": -757, + "y": -123, + "zoom": 1.2194779259663722, + }, + "id": "9378bc2e4da", + "linkTo": { + "id": "other.id-existing", + "link_type": "Document", }, - "id": "961adcf1f5d", "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", }, ], }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches rich text image nodes > new > slice 1`] = ` +exports[`patches rich text image nodes (from Prismic) > withLinkTo > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": [ { "spans": [], - "text": "Diam Ut Venenatis Tellus", - "type": "heading6", + "text": "Auctor neque vitae tempus quam. At ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget gravida cum.", + "type": "paragraph", }, { - "alt": null, + "alt": "Sed id semper risus in hendrerit gravida rutrum", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#104dde", + "x": -1621, + "y": -870, + "zoom": 1.7278759511485409, + }, + "id": "2003a644c30", + "linkTo": { + "id": "other.id-existing", + "link_type": "Document", }, - "id": "61cc54f4a69", "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", }, ], }, @@ -390,73 +410,81 @@ exports[`patches rich text image nodes > new > slice 1`] = ` "field": [ { "spans": [], - "text": "Tincidunt Vitae Semper Quis Lectus Nulla", - "type": "heading4", + "text": "Pharetra et ultrices neque ornare aenean euismod. Mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac.", + "type": "preformatted", }, { - "alt": null, + "alt": "Vitae et leo duis ut diam quam nulla porttitor", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#cc54f4", + "x": 637, + "y": -1697, + "zoom": 1.1481742186351154, + }, + "id": "2003a644c30", + "linkTo": { + "id": "other.id-existing", + "link_type": "Document", }, - "id": "61cc54f4a69", "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", }, ], }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches rich text image nodes > new > static zone 1`] = ` +exports[`patches rich text image nodes (from Prismic) > withLinkTo > static zone 1`] = ` { "field": [ { "spans": [], - "text": "Interdum Velit Euismod", - "type": "heading5", + "text": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis. Tincidunt praesent semper feugiat nibh sed pulvinar proin gravida hendrerit lectus a.", + "type": "paragraph", }, { - "alt": null, + "alt": "Adipiscing bibendum est ultricies integer quis auctor elit", "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "transparent", - "x": 0, - "y": 0, - "zoom": 1, + "background": "#b943c8", + "x": 592, + "y": 260, + "zoom": 1.3971999251277185, + }, + "id": "b5df4522c1a", + "linkTo": { + "id": "other.id-existing", + "link_type": "Document", }, - "id": "ed908b1e225", "type": "image", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", + "url": "https://images.unsplash.com/photo-1504198266287-1659872e6590", }, ], } `; -exports[`patches rich text image nodes > newLinkTo > group 1`] = ` +exports[`patches rich text image nodes > existing > group 1`] = ` { "group": [ { "field": [ { "spans": [], - "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", - "type": "heading4", + "text": "Pulvinar elementum integer enim neque volutpat ac tincidunt vitae semper quis. Gravida hendrerit lectus a molestie lorem ipsum dolor sit amet. Mauris sit amet massa vitae tortor condimentum lacinia. Blandit volutpat maecenas volutpat blandit aliquam. Massa eget egestas purus viverra accumsan in nisl nisi scelerisque. Imperdiet dui accumsan sit amet nulla facilisi morbi tempus iaculis.", + "type": "paragraph", }, { "alt": null, @@ -471,13 +499,9 @@ exports[`patches rich text image nodes > newLinkTo > group 1`] = ` "y": 0, "zoom": 1, }, - "id": "c1d5d24feae", - "linkTo": { - "id": "id-existing", - "link_type": "Document", - }, + "id": "asset.id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1504198266287-1659872e6590?w=4272&h=2848&fit=crop", + "url": "https://images.unsplash.com/photo-1470770903676-69b98201ea1c?w=4554&h=3036&fit=crop", }, ], }, @@ -485,18 +509,18 @@ exports[`patches rich text image nodes > newLinkTo > group 1`] = ` } `; -exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` +exports[`patches rich text image nodes > existing > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": [ { "spans": [], - "text": "A molestie lorem ipsum dolor sit amet consectetur adipiscing elit ut", - "type": "list-item", + "text": "Diam", + "type": "heading4", }, { "alt": null, @@ -511,13 +535,9 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "961adcf1f5d", - "linkTo": { - "id": "id-existing", - "link_type": "Document", - }, + "id": "asset.id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", }, ], }, @@ -526,8 +546,8 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", - "type": "preformatted", + "text": "Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet nulla malesuada. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh. Eleifend donec pretium vulputate sapien.", + "type": "paragraph", }, { "alt": null, @@ -542,13 +562,9 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "961adcf1f5d", - "linkTo": { - "id": "id-existing", - "link_type": "Document", - }, + "id": "asset.id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", }, ], "group": [ @@ -556,8 +572,8 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Vel Elit Scelerisque Mauris Pellentesque Pulvinar Pellentesque", - "type": "heading3", + "text": "Quis Enim Lobortis", + "type": "heading2", }, { "alt": null, @@ -572,39 +588,35 @@ exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "961adcf1f5d", - "linkTo": { - "id": "id-existing", - "link_type": "Document", - }, + "id": "asset.id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", + "url": "https://images.unsplash.com/photo-1504567961542-e24d9439a724?w=4608&h=3456&fit=crop", }, ], }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` +exports[`patches rich text image nodes > existing > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": [ { "spans": [], - "text": "Diam Ut Venenatis Tellus", - "type": "heading6", + "text": "Ultrices dui sapien eget mi proin sed libero enim sed. Diam ut venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Sed nisi lacus sed viverra tellus in hac habitasse. Eget mi proin sed libero enim sed faucibus turpis. Pellentesque id nibh tortor id aliquet. Aliquam sem et tortor consequat id porta nibh venenatis cras sed felis.", + "type": "paragraph", }, { "alt": null, @@ -619,13 +631,9 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "61cc54f4a69", - "linkTo": { - "id": "id-existing", - "link_type": "Document", - }, + "id": "asset.id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, ], }, @@ -634,8 +642,8 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "field": [ { "spans": [], - "text": "Tincidunt Vitae Semper Quis Lectus Nulla", - "type": "heading4", + "text": "Pharetra et ultrices neque ornare aenean euismod. Mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac.", + "type": "preformatted", }, { "alt": null, @@ -650,30 +658,26 @@ exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` "y": 0, "zoom": 1, }, - "id": "61cc54f4a69", - "linkTo": { - "id": "id-existing", - "link_type": "Document", - }, + "id": "asset.id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", + "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, ], }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches rich text image nodes > newLinkTo > static zone 1`] = ` +exports[`patches rich text image nodes > existing > static zone 1`] = ` { "field": [ { "spans": [], - "text": "Interdum Velit Euismod", - "type": "heading5", + "text": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis. Tincidunt praesent semper feugiat nibh sed pulvinar proin gravida hendrerit lectus a.", + "type": "paragraph", }, { "alt": null, @@ -688,44 +692,40 @@ exports[`patches rich text image nodes > newLinkTo > static zone 1`] = ` "y": 0, "zoom": 1, }, - "id": "ed908b1e225", - "linkTo": { - "id": "id-existing", - "link_type": "Document", - }, + "id": "asset.id-existing", "type": "image", - "url": "https://images.unsplash.com/photo-1446329813274-7c9036bd9a1f?w=6000&h=4000&fit=crop", + "url": "https://images.unsplash.com/photo-1604537529428-15bcbeecfe4d?w=4240&h=2832&fit=crop", }, ], } `; -exports[`patches rich text image nodes > otherRepository > group 1`] = ` +exports[`patches rich text image nodes > new > group 1`] = ` { "group": [ { "field": [ { "spans": [], - "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", - "type": "heading4", + "text": "Pulvinar elementum integer enim neque volutpat ac tincidunt vitae semper quis. Gravida hendrerit lectus a molestie lorem ipsum dolor sit amet. Mauris sit amet massa vitae tortor condimentum lacinia. Blandit volutpat maecenas volutpat blandit aliquam. Massa eget egestas purus viverra accumsan in nisl nisi scelerisque. Imperdiet dui accumsan sit amet nulla facilisi morbi tempus iaculis.", + "type": "paragraph", }, { - "alt": "Feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#5c0990", - "x": 2090, - "y": -1492, - "zoom": 1.854310159304717, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "6442e21ad60", + "id": "26fe8d0985c", "type": "image", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, @@ -733,46 +733,35 @@ exports[`patches rich text image nodes > otherRepository > group 1`] = ` } `; -exports[`patches rich text image nodes > otherRepository > shared slice 1`] = ` +exports[`patches rich text image nodes > new > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": [ { - "oembed": { - "embed_url": "https://twitter.com/prismicio/status/1354835716430319617", - "height": 113, - "html": " - -", - "thumbnail_height": null, - "thumbnail_url": null, - "thumbnail_width": null, - "type": "rich", - "version": "1.0", - "width": 200, - }, - "type": "embed", + "spans": [], + "text": "Diam", + "type": "heading4", }, { - "alt": "Eros donec ac odio tempor orci dapibus", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#1eeca0", - "x": 1722, - "y": 1820, - "zoom": 1.8326837750693512, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "3a8ec9378bc", + "id": "f7b961adcf1", "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, @@ -781,25 +770,25 @@ exports[`patches rich text image nodes > otherRepository > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", - "type": "preformatted", + "text": "Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet nulla malesuada. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh. Eleifend donec pretium vulputate sapien.", + "type": "paragraph", }, { - "alt": "Aliquet lectus proin nibh nisl condimentum id venenatis a", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#87853a", - "x": -1429, - "y": -2019, - "zoom": 1.75840565859303, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "3a8ec9378bc", + "id": "f7b961adcf1", "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], "group": [ @@ -807,68 +796,68 @@ exports[`patches rich text image nodes > otherRepository > shared slice 1`] = ` "field": [ { "spans": [], - "text": "Nunc Non Blandit Massa Enim", - "type": "heading5", + "text": "Quis Enim Lobortis", + "type": "heading2", }, { - "alt": "Id faucibus nisl tincidunt eget nullam non nisi est sit amet", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#252b35", - "x": 855, - "y": 1518, - "zoom": 1.5250952426635416, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "3a8ec9378bc", + "id": "f7b961adcf1", "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches rich text image nodes > otherRepository > slice 1`] = ` +exports[`patches rich text image nodes > new > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": [ { "spans": [], - "text": "At Ultrices Mi Tempus Imperdiet", - "type": "heading2", + "text": "Ultrices dui sapien eget mi proin sed libero enim sed. Diam ut venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Sed nisi lacus sed viverra tellus in hac habitasse. Eget mi proin sed libero enim sed faucibus turpis. Pellentesque id nibh tortor id aliquet. Aliquam sem et tortor consequat id porta nibh venenatis cras sed felis.", + "type": "paragraph", }, { - "alt": "Sed id semper risus in hendrerit gravida rutrum", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#104dde", - "x": -1621, - "y": -870, - "zoom": 1.7278759511485409, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "2003a644c30", + "id": "54f4a69ff72", "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", }, ], }, @@ -877,94 +866,94 @@ exports[`patches rich text image nodes > otherRepository > slice 1`] = ` "field": [ { "spans": [], - "text": "Tincidunt Vitae Semper Quis Lectus Nulla", - "type": "heading4", + "text": "Pharetra et ultrices neque ornare aenean euismod. Mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac.", + "type": "preformatted", }, { - "alt": "Sed nisi lacus sed viverra tellus in hac habitasse", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#63a6c9", - "x": -1609, - "y": -1609, - "zoom": 1.7054690955452316, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "2003a644c30", + "id": "54f4a69ff72", "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", }, ], }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches rich text image nodes > otherRepository > static zone 1`] = ` +exports[`patches rich text image nodes > new > static zone 1`] = ` { "field": [ { "spans": [], - "text": "Interdum Velit Euismod", - "type": "heading5", + "text": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis. Tincidunt praesent semper feugiat nibh sed pulvinar proin gravida hendrerit lectus a.", + "type": "paragraph", }, { - "alt": "Tortor consequat id porta nibh", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#c61c37", - "x": 349, - "y": -429, - "zoom": 1.4911347686990943, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "ff1efb2b271", + "id": "ab1d17b04a9", "type": "image", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", }, ], } `; -exports[`patches rich text image nodes > otherRepositoryLinkTo > group 1`] = ` +exports[`patches rich text image nodes > newLinkTo > group 1`] = ` { "group": [ { "field": [ { "spans": [], - "text": "Donec Enim Diam Vulputate Ut Pharetra Sit Amet Aliquam", - "type": "heading4", + "text": "Pulvinar elementum integer enim neque volutpat ac tincidunt vitae semper quis. Gravida hendrerit lectus a molestie lorem ipsum dolor sit amet. Mauris sit amet massa vitae tortor condimentum lacinia. Blandit volutpat maecenas volutpat blandit aliquam. Massa eget egestas purus viverra accumsan in nisl nisi scelerisque. Imperdiet dui accumsan sit amet nulla facilisi morbi tempus iaculis.", + "type": "paragraph", }, { - "alt": "Feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#5c0990", - "x": 2090, - "y": -1492, - "zoom": 1.854310159304717, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "6442e21ad60", + "id": "26fe8d0985c", "linkTo": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "type": "image", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, @@ -972,50 +961,39 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > group 1`] = ` } `; -exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1`] = ` +exports[`patches rich text image nodes > newLinkTo > shared slice 1`] = ` { "slices": [ { - "id": "b9dd0f2c3f8", + "id": "9b9dd0f2c3f", "items": [ { "field": [ { - "oembed": { - "embed_url": "https://twitter.com/prismicio/status/1354835716430319617", - "height": 113, - "html": " - -", - "thumbnail_height": null, - "thumbnail_url": null, - "thumbnail_width": null, - "type": "rich", - "version": "1.0", - "width": 200, - }, - "type": "embed", + "spans": [], + "text": "Diam", + "type": "heading4", }, { - "alt": "Eros donec ac odio tempor orci dapibus", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#1eeca0", - "x": 1722, - "y": 1820, - "zoom": 1.8326837750693512, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "3a8ec9378bc", + "id": "f7b961adcf1", "linkTo": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, @@ -1024,29 +1002,29 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` "field": [ { "spans": [], - "text": "Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium.", - "type": "preformatted", + "text": "Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Cras fermentum odio eu feugiat pretium. Neque aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet nulla malesuada. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh. Eleifend donec pretium vulputate sapien.", + "type": "paragraph", }, { - "alt": "Aliquet lectus proin nibh nisl condimentum id venenatis a", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#87853a", - "x": -1429, - "y": -2019, - "zoom": 1.75840565859303, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "3a8ec9378bc", + "id": "f7b961adcf1", "linkTo": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], "group": [ @@ -1054,76 +1032,76 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > shared slice 1` "field": [ { "spans": [], - "text": "Nunc Non Blandit Massa Enim", - "type": "heading5", + "text": "Quis Enim Lobortis", + "type": "heading2", }, { - "alt": "Id faucibus nisl tincidunt eget nullam non nisi est sit amet", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#252b35", - "x": 855, - "y": 1518, - "zoom": 1.5250952426635416, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "3a8ec9378bc", + "id": "f7b961adcf1", "linkTo": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "type": "image", - "url": "https://images.unsplash.com/photo-1431794062232-2a99a5431c6c", + "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, ], }, ], }, "slice_label": null, - "slice_type": "dignissim", - "variation": "vitae", - "version": "bfc9053", + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", }, ], } `; -exports[`patches rich text image nodes > otherRepositoryLinkTo > slice 1`] = ` +exports[`patches rich text image nodes > newLinkTo > slice 1`] = ` { "slices": [ { - "id": "306297c5eda", + "id": "5306297c5ed", "items": [ { "field": [ { "spans": [], - "text": "At Ultrices Mi Tempus Imperdiet", - "type": "heading2", + "text": "Ultrices dui sapien eget mi proin sed libero enim sed. Diam ut venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Sed nisi lacus sed viverra tellus in hac habitasse. Eget mi proin sed libero enim sed faucibus turpis. Pellentesque id nibh tortor id aliquet. Aliquam sem et tortor consequat id porta nibh venenatis cras sed felis.", + "type": "paragraph", }, { - "alt": "Sed id semper risus in hendrerit gravida rutrum", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#104dde", - "x": -1621, - "y": -870, - "zoom": 1.7278759511485409, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "2003a644c30", + "id": "54f4a69ff72", "linkTo": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", }, ], }, @@ -1132,67 +1110,67 @@ exports[`patches rich text image nodes > otherRepositoryLinkTo > slice 1`] = ` "field": [ { "spans": [], - "text": "Tincidunt Vitae Semper Quis Lectus Nulla", - "type": "heading4", + "text": "Pharetra et ultrices neque ornare aenean euismod. Mauris sit amet massa vitae tortor condimentum lacinia quis vel eros donec ac.", + "type": "preformatted", }, { - "alt": "Sed nisi lacus sed viverra tellus in hac habitasse", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#63a6c9", - "x": -1609, - "y": -1609, - "zoom": 1.7054690955452316, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "2003a644c30", + "id": "54f4a69ff72", "linkTo": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "type": "image", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff", + "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", }, ], }, - "slice_label": "Aliquet", - "slice_type": "vel", + "slice_label": "Vel", + "slice_type": "hac_habitasse", }, ], } `; -exports[`patches rich text image nodes > otherRepositoryLinkTo > static zone 1`] = ` +exports[`patches rich text image nodes > newLinkTo > static zone 1`] = ` { "field": [ { "spans": [], - "text": "Interdum Velit Euismod", - "type": "heading5", + "text": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis. Tincidunt praesent semper feugiat nibh sed pulvinar proin gravida hendrerit lectus a.", + "type": "paragraph", }, { - "alt": "Tortor consequat id porta nibh", + "alt": null, "copyright": null, "dimensions": { "height": 1, "width": 1, }, "edit": { - "background": "#c61c37", - "x": 349, - "y": -429, - "zoom": 1.4911347686990943, + "background": "transparent", + "x": 0, + "y": 0, + "zoom": 1, }, - "id": "ff1efb2b271", + "id": "ab1d17b04a9", "linkTo": { - "id": "id-existing", + "id": "other.id-existing", "link_type": "Document", }, "type": "image", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1", + "url": "https://images.unsplash.com/photo-1553531384-397c80973a0b?w=4335&h=6502&fit=crop", }, ], } diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index ab9ef6b1..8796093d 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -180,6 +180,14 @@ export const mockPrismicAssetAPI = ( : asset.tags, } + // __pdf__ is used as a magic word to transform created assets into + // documents since we can't read FormData when creating assets... + if (response.tags?.some((tag) => tag.name === "__pdf__")) { + response.kind = "document" + response.width = undefined + response.height = undefined + } + // Update asset in DB for (const cursor in assetsDatabase) { for (const asset in assetsDatabase[cursor]) { diff --git a/test/__testutils__/testMigrationFieldPatching.ts b/test/__testutils__/testMigrationFieldPatching.ts index 74a141b0..b74d8caf 100644 --- a/test/__testutils__/testMigrationFieldPatching.ts +++ b/test/__testutils__/testMigrationFieldPatching.ts @@ -22,10 +22,8 @@ type GetDataArgs = { migration: prismic.Migration existingAssets: Asset[] existingDocuments: prismic.PrismicDocument[] - migrationDocuments: { - other: prismic.PrismicMigrationDocument - otherRepository: prismic.PrismicMigrationDocument - } + otherCreateDocument: prismic.PrismicMigrationDocument + otherFromPrismicDocument: prismic.PrismicMigrationDocument mockedDomain: string } @@ -44,43 +42,23 @@ const internalTestMigrationFieldPatching = ( const client = createTestWriteClient({ ctx }) + // Mock Document API const repository = ctx.mock.api.repository() repository.languages[0].id = "en-us" repository.languages[0].is_master = true - const queryResponse = createPagedQueryResponses({ ctx, pages: 1, pageSize: 1, }) - queryResponse[0].results[0].id = "id-existing" - - const { id: _id, ...otherDocument } = { - ...ctx.mock.value.document(), - uid: ctx.mock.value.keyText(), - } - const otherRepositoryDocument = { - ...ctx.mock.value.document(), - uid: ctx.mock.value.keyText(), - } - const { id: originalID, ...newDocument } = ctx.mock.value.document() - - const newID = "id-new" - + queryResponse[0].results[0].id = "other.id-existing" mockPrismicRestAPIV2({ ctx, repositoryResponse: repository, queryResponse }) + + // Mock Asset API const { assetsDatabase } = mockPrismicAssetAPI({ ctx, client, - existingAssets: [[mockAsset(ctx, { id: "id-existing" })]], - }) - const { documentsDatabase } = mockPrismicMigrationAPI({ - ctx, - client, - newDocuments: [ - { id: "id-other" }, - { id: "id-other-repository" }, - { id: newID }, - ], + existingAssets: [[mockAsset(ctx, { id: "asset.id-existing" })]], }) const mockedDomain = `https://${client.repositoryName}.example.com` @@ -90,28 +68,56 @@ const internalTestMigrationFieldPatching = ( ), ) + // Setup migration const migration = prismic.createMigration() + // Create document + const { id: _createOriginalID, ...createDocument } = { + ...ctx.mock.value.document(), + uid: ctx.mock.value.keyText(), + } + const migrationOtherDocument = migration.createDocument( - otherDocument, - "other", + createDocument, + "other.create", ) + // Create document from Prismic + const fromPrismicDocument = { + ...ctx.mock.value.document(), + uid: ctx.mock.value.keyText(), + } + const migrationOtherRepositoryDocument = migration.createDocumentFromPrismic( - otherRepositoryDocument, - "other-repository", + fromPrismicDocument, + "other.fromPrismic", ) + // Create new document + const { id: originalID, ...newDocument } = ctx.mock.value.document() + const newID = + !args.mode || args.mode === "new" ? "id-new" : "id-fromPrismic" + + // Mock Migration API + const { documentsDatabase } = mockPrismicMigrationAPI({ + ctx, + client, + newDocuments: [ + { id: "other.id-create" }, + { id: "other.id-fromPrismic" }, + { id: newID }, + ], + }) + + // Get new document data newDocument.data = args.getData({ ctx, migration, existingAssets: assetsDatabase.flat(), existingDocuments: queryResponse[0].results, - migrationDocuments: { - other: migrationOtherDocument, - otherRepository: migrationOtherRepositoryDocument, - }, + otherCreateDocument: migrationOtherDocument, + otherFromPrismicDocument: migrationOtherRepositoryDocument, mockedDomain, }) diff --git a/test/writeClient-migrate-documents.test.ts b/test/writeClient-migrate-documents.test.ts index af48bfc9..50062671 100644 --- a/test/writeClient-migrate-documents.test.ts +++ b/test/writeClient-migrate-documents.test.ts @@ -293,6 +293,38 @@ it.concurrent( }, ) +it.concurrent( + "creates new non-master locale document with lazy reference (empty)", + async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const { id: documentID, ...document } = ctx.mock.value.document() + const newDocuments = [ + { + id: documentID, + masterLanguageDocumentID: undefined, + }, + ] + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client, newDocuments: [...newDocuments] }) + + const migration = prismic.createMigration() + + const doc = migration.createDocument(document, "bar", { + masterLanguageDocument: () => undefined, + }) + + const reporter = vi.fn() + + await client.migrate(migration, { reporter }) + + ctx.expect(doc.document.id).toBe(newDocuments[0].id) + ctx.expect.assertions(1) + }, +) + it.concurrent( "creates new non-master locale document with alternate language", async (ctx) => { diff --git a/test/writeClient-migrate-patch-contentRelationship.test.ts b/test/writeClient-migrate-patch-contentRelationship.test.ts index bc0c2344..3036480c 100644 --- a/test/writeClient-migrate-patch-contentRelationship.test.ts +++ b/test/writeClient-migrate-patch-contentRelationship.test.ts @@ -10,33 +10,43 @@ import { LinkType, RichTextNodeType } from "../src" testMigrationFieldPatching< MigrationContentRelationship | InjectMigrationSpecificTypes ->("patches link fields", { +>("patches content relationship fields", { existing: ({ existingDocuments }) => existingDocuments[0], - migration: ({ migrationDocuments }) => { - delete migrationDocuments.other.document.id - - return migrationDocuments.other + existingLongForm: ({ existingDocuments }) => { + return { + link_type: LinkType.Document, + id: existingDocuments[0], + } + }, + existingLongFormWithText: ({ existingDocuments }) => { + return { + link_type: LinkType.Document, + id: existingDocuments[0], + text: "foo", + } }, + otherCreate: ({ otherCreateDocument }) => otherCreateDocument, lazyExisting: ({ existingDocuments }) => { return () => existingDocuments[0] }, - lazyMigration: ({ migration, migrationDocuments }) => { - delete migrationDocuments.other.document.id - + lazyOtherCreate: ({ migration, otherCreateDocument }) => { return () => migration.getByUID( - migrationDocuments.other.document.type, - migrationDocuments.other.document.uid!, + otherCreateDocument.document.type, + otherCreateDocument.document.uid!, ) }, - migrationNoTags: ({ migration, migrationDocuments }) => { - migrationDocuments.other.document.tags = undefined - - return () => - migration.getByUID( - migrationDocuments.other.document.type, - migrationDocuments.other.document.uid!, + lazyOtherCreateMissingID: ({ migration, otherCreateDocument }) => { + return () => { + const doc = migration.getByUID( + otherCreateDocument.document.type, + otherCreateDocument.document.uid!, ) + + delete doc?.document.id + + return doc + } }, richTextLinkNode: ({ existingDocuments }) => [ { @@ -56,28 +66,42 @@ testMigrationFieldPatching< }) testMigrationFieldPatching( - "patches link fields", + "patches content relationship fields (from Prismic)", { - brokenLink: () => { - return { - link_type: LinkType.Document, - id: "id", - type: "type", - tags: [], - lang: "lang", - isBroken: true, - } + simple: ({ ctx, otherFromPrismicDocument }) => { + const contentRelationship = ctx.mock.value.link({ + type: LinkType.Document, + }) + // `migrationDocuments` contains documents from "another repository" + contentRelationship.id = + otherFromPrismicDocument.originalPrismicDocument!.id + + return contentRelationship }, - otherRepositoryContentRelationship: ({ ctx, migrationDocuments }) => { + withText: ({ ctx, otherFromPrismicDocument }) => { const contentRelationship = ctx.mock.value.link({ type: LinkType.Document, }) // `migrationDocuments` contains documents from "another repository" contentRelationship.id = - migrationDocuments.otherRepository.originalPrismicDocument!.id + otherFromPrismicDocument.originalPrismicDocument!.id + + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + contentRelationship.text = "foo" return contentRelationship }, + broken: () => { + return { + link_type: LinkType.Document, + id: "id", + type: "type", + tags: [], + lang: "lang", + isBroken: true, + } + }, }, { mode: "fromPrismic" }, ) diff --git a/test/writeClient-migrate-patch-image.test.ts b/test/writeClient-migrate-patch-image.test.ts index 52c485e8..9f3ce7ab 100644 --- a/test/writeClient-migrate-patch-image.test.ts +++ b/test/writeClient-migrate-patch-image.test.ts @@ -6,6 +6,17 @@ testMigrationFieldPatching( "patches image fields", { new: ({ migration }) => migration.createAsset("foo", "foo.png"), + newLongForm: ({ migration }) => { + return { + id: migration.createAsset("foo", "foo.png"), + } + }, + newThumbnails: ({ migration }) => { + return { + id: migration.createAsset("foo", "foo.png"), + square: migration.createAsset("foo", "foo.png"), + } + }, existing: ({ existingAssets }) => { const asset = existingAssets[0] @@ -22,16 +33,16 @@ testMigrationFieldPatching( ) testMigrationFieldPatching( - "patches image fields", + "patches image fields (from Prismic)", { - otherRepository: ({ ctx, mockedDomain }) => { + simple: ({ ctx, mockedDomain }) => { return { ...ctx.mock.value.image({ state: "filled" }), id: "foo-id", url: `${mockedDomain}/foo.png`, } }, - otherRepositoryWithThumbnails: ({ ctx, mockedDomain }) => { + withThumbnails: ({ ctx, mockedDomain }) => { const image = ctx.mock.value.image({ state: "filled" }) const id = "foo-id" @@ -46,7 +57,7 @@ testMigrationFieldPatching( }, } }, - otherRepositoryWithThumbnailsNoAlt: ({ ctx, mockedDomain }) => { + withThumbnailsNoAlt: ({ ctx, mockedDomain }) => { const image = ctx.mock.value.image({ state: "filled" }) image.alt = null const id = "foo-id" @@ -62,7 +73,7 @@ testMigrationFieldPatching( }, } }, - otherRepositoryWithTypeThumbnail: ({ ctx, mockedDomain }) => { + withSpecialTypeThumbnail: ({ ctx, mockedDomain }) => { const image = ctx.mock.value.image({ state: "filled" }) const id = "foo-id" @@ -77,7 +88,7 @@ testMigrationFieldPatching( }, } }, - otherRepositoryEmpty: ({ ctx }) => ctx.mock.value.image({ state: "empty" }), + empty: ({ ctx }) => ctx.mock.value.image({ state: "empty" }), }, { mode: "fromPrismic" }, ) diff --git a/test/writeClient-migrate-patch-linkToMedia.test.ts b/test/writeClient-migrate-patch-linkToMedia.test.ts index 90ad2b3e..3e394a1e 100644 --- a/test/writeClient-migrate-patch-linkToMedia.test.ts +++ b/test/writeClient-migrate-patch-linkToMedia.test.ts @@ -7,7 +7,6 @@ import type { } from "../src" import { LinkType, RichTextNodeType } from "../src" import type { Asset } from "../src/types/api/asset/asset" -import { AssetType } from "../src/types/api/asset/asset" import type { MigrationLinkToMedia } from "../src/types/migration/Asset" const assetToLinkToMedia = ( @@ -40,16 +39,24 @@ testMigrationFieldPatching< id: migration.createAsset("foo", "foo.png"), } }, - existing: ({ existingAssets }) => assetToLinkToMedia(existingAssets[0]), - existingNonImage: ({ existingAssets }) => { - existingAssets[0].filename = "foo.pdf" - existingAssets[0].extension = "pdf" - existingAssets[0].kind = AssetType.Document - existingAssets[0].width = undefined - existingAssets[0].height = undefined + newWithText: ({ migration }) => { + return { + link_type: LinkType.Media, + id: migration.createAsset("foo", "foo.png"), + text: "foo", + } + }, + newNonImage: ({ migration }) => { + const migrationAsset = migration.createAsset("foo", "foo.pdf", { + tags: ["__pdf__"], + }) - return assetToLinkToMedia(existingAssets[0]) + return { + link_type: LinkType.Media, + id: migrationAsset, + } }, + existing: ({ existingAssets }) => assetToLinkToMedia(existingAssets[0]), richTextNew: ({ migration }) => [ { type: RichTextNodeType.paragraph, @@ -86,16 +93,16 @@ testMigrationFieldPatching< }) testMigrationFieldPatching( - "patches link to media fields", + "patches link to media fields (from Prismic)", { - otherRepository: ({ ctx, mockedDomain }) => { + simple: ({ ctx, mockedDomain }) => { return { ...ctx.mock.value.linkToMedia({ state: "filled" }), id: "foo-id", url: `${mockedDomain}/foo.png`, } }, - richTextOtherRepository: ({ ctx, mockedDomain }) => [ + inRichText: ({ ctx, mockedDomain }) => [ { type: RichTextNodeType.paragraph, text: "lorem", diff --git a/test/writeClient-migrate-patch-rtImageNode.test.ts b/test/writeClient-migrate-patch-rtImageNode.test.ts index d483e056..a7685cc4 100644 --- a/test/writeClient-migrate-patch-rtImageNode.test.ts +++ b/test/writeClient-migrate-patch-rtImageNode.test.ts @@ -40,9 +40,9 @@ testMigrationFieldPatching>( ) testMigrationFieldPatching>( - "patches rich text image nodes", + "patches rich text image nodes (from Prismic)", { - otherRepository: ({ ctx, mockedDomain }) => [ + simple: ({ ctx, mockedDomain }) => [ ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), { type: RichTextNodeType.image, @@ -51,7 +51,7 @@ testMigrationFieldPatching>( url: `${mockedDomain}/foo.png`, }, ], - otherRepositoryLinkTo: ({ ctx, mockedDomain, existingDocuments }) => [ + withLinkTo: ({ ctx, mockedDomain, existingDocuments }) => [ ...ctx.mock.value.richText({ pattern: "short", state: "filled" }), { type: RichTextNodeType.image, diff --git a/test/writeClient-migrate-patch-simpleField.test.ts b/test/writeClient-migrate-patch-simpleField.test.ts index 848fa844..0900323b 100644 --- a/test/writeClient-migrate-patch-simpleField.test.ts +++ b/test/writeClient-migrate-patch-simpleField.test.ts @@ -29,6 +29,12 @@ testMigrationFieldPatching( linkToWeb: ({ ctx }) => ctx.mock.value.link({ type: "Web", withTargetBlank: true }), richTextMisleadingGroup: () => [{ type: "paragraph" }], + documentMisleadingObject: () => { + return { + id: "foo", + href: "bar", + } + }, }, { expectStrictEqual: true }, ) From 9bab813255d99eafc5d4c0ce09cc8c1efe715691 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 18 Sep 2024 10:30:28 +0200 Subject: [PATCH 58/61] refactor: per review --- src/Migration.ts | 7 +- src/WriteClient.ts | 7 +- src/lib/resolveMigrationDocumentData.ts | 14 +- src/types/migration/Asset.ts | 16 + ...ate-patch-contentRelationship.test.ts.snap | 14 +- ...ent-migrate-patch-linkToMedia.test.ts.snap | 315 +++++------------- test/writeClient-migrate-documents.test.ts | 33 +- ...teClient-migrate-patch-linkToMedia.test.ts | 9 + 8 files changed, 140 insertions(+), 275 deletions(-) diff --git a/src/Migration.ts b/src/Migration.ts index d821641f..0f70372d 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -402,7 +402,8 @@ export class Migration { if (input.isBroken) { return { link_type: LinkType.Document, - id: "__broken__", + // ID needs to be 16 characters long to be considered valid by the API + id: "_____broken_____", isBroken: true, // TODO: Remove when link text PR is merged // @ts-expect-error - Future-proofing for link text @@ -412,7 +413,7 @@ export class Migration { return { link_type: LinkType.Document, - id: () => this.#getByOriginalID(input.id), + id: () => this.getByOriginalID(input.id), // TODO: Remove when link text PR is merged // @ts-expect-error - Future-proofing for link text text: input.text, @@ -507,7 +508,7 @@ export class Migration { * @returns The migration document instance for the original ID, if a matching * document is found. */ - #getByOriginalID( + getByOriginalID( id: string, ): | PrismicMigrationDocument> diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 202f7ab8..1d642db2 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -440,10 +440,15 @@ export class WriteClient< masterLanguageDocumentID = "id" in masterLanguageDocument ? masterLanguageDocument.id : undefined } else if (doc.originalPrismicDocument) { - masterLanguageDocumentID = + const maybeOriginalID = doc.originalPrismicDocument.alternate_languages.find( ({ lang }) => lang === masterLocale, )?.id + + if (maybeOriginalID) { + masterLanguageDocumentID = + migration.getByOriginalID(maybeOriginalID)?.document.id + } } const { id } = await this.createDocument( diff --git a/src/lib/resolveMigrationDocumentData.ts b/src/lib/resolveMigrationDocumentData.ts index 8bfd9fbf..dded02c9 100644 --- a/src/lib/resolveMigrationDocumentData.ts +++ b/src/lib/resolveMigrationDocumentData.ts @@ -1,3 +1,4 @@ +import type { MigrationLinkToMediaField } from "../types/migration/Asset" import { type MigrationImage, type MigrationLinkToMedia, @@ -12,7 +13,6 @@ import { PrismicMigrationDocument } from "../types/migration/Document" import type { FilledImageFieldImage } from "../types/value/image" import type { LinkField } from "../types/value/link" import { LinkType } from "../types/value/link" -import type { LinkToMediaField } from "../types/value/linkToMedia" import type { RTImageNode } from "../types/value/richText" import { RichTextNodeType } from "../types/value/richText" @@ -156,24 +156,18 @@ export const resolveMigrationRTImageNode = async ( export const resolveMigrationLinkToMedia = ( linkToMedia: MigrationLinkToMedia, migration: Migration, -): LinkToMediaField<"filled"> | undefined => { +): MigrationLinkToMediaField => { const asset = migration._assets.get(linkToMedia.id.config.id)?.asset if (asset) { return { id: asset.id, link_type: LinkType.Media, - name: asset.filename, - kind: asset.kind, - url: asset.url, - size: `${asset.size}`, - height: typeof asset.height === "number" ? `${asset.height}` : undefined, - width: typeof asset.width === "number" ? `${asset.width}` : undefined, - // TODO: Remove when link text PR is merged - // @ts-expect-error - Future-proofing for link text text: linkToMedia.text, } } + + return { link_type: LinkType.Media } } /** diff --git a/src/types/migration/Asset.ts b/src/types/migration/Asset.ts index a6242e3a..8aa928a9 100644 --- a/src/types/migration/Asset.ts +++ b/src/types/migration/Asset.ts @@ -1,5 +1,6 @@ import type { Asset } from "../api/asset/asset" import type { FilledImageFieldImage } from "../value/image" +import type { EmptyLinkField } from "../value/link" import type { LinkToMediaField } from "../value/linkToMedia" import { type RTImageNode } from "../value/richText" @@ -11,6 +12,11 @@ import type { InjectMigrationSpecificTypes } from "./Document" export type MigrationAssetConfig = { /** * ID the assets is indexed with on the migration instance. + * + * @remarks + * This property's value is not necessarily the same as the as the one in the + * `file` property. It is mainly used for deduplication within a `Migration` + * instance. */ id: string | URL | File | NonNullable[0]>[0] @@ -84,6 +90,16 @@ export type MigrationLinkToMedia = Pick< id: PrismicMigrationAsset } +/** + * The minimum amount of information needed to represent a link to media field + * with the migration API. + */ +export type MigrationLinkToMediaField = + // TODO: Remove when link text PR is merged + // @ts-expect-error - Future-proofing for link text + | Pick, "link_type" | "id" | "text"> + | EmptyLinkField<"Media"> + /** * A rich text image node in a migration. */ diff --git a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap index ae564060..337e410a 100644 --- a/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-contentRelationship.test.ts.snap @@ -5,7 +5,7 @@ exports[`patches content relationship fields (from Prismic) > broken > group 1`] "group": [ { "field": { - "id": "__broken__", + "id": "_____broken_____", "isBroken": true, "link_type": "Document", }, @@ -22,7 +22,7 @@ exports[`patches content relationship fields (from Prismic) > broken > shared sl "items": [ { "field": { - "id": "__broken__", + "id": "_____broken_____", "isBroken": true, "link_type": "Document", }, @@ -30,14 +30,14 @@ exports[`patches content relationship fields (from Prismic) > broken > shared sl ], "primary": { "field": { - "id": "__broken__", + "id": "_____broken_____", "isBroken": true, "link_type": "Document", }, "group": [ { "field": { - "id": "__broken__", + "id": "_____broken_____", "isBroken": true, "link_type": "Document", }, @@ -61,7 +61,7 @@ exports[`patches content relationship fields (from Prismic) > broken > slice 1`] "items": [ { "field": { - "id": "__broken__", + "id": "_____broken_____", "isBroken": true, "link_type": "Document", }, @@ -69,7 +69,7 @@ exports[`patches content relationship fields (from Prismic) > broken > slice 1`] ], "primary": { "field": { - "id": "__broken__", + "id": "_____broken_____", "isBroken": true, "link_type": "Document", }, @@ -84,7 +84,7 @@ exports[`patches content relationship fields (from Prismic) > broken > slice 1`] exports[`patches content relationship fields (from Prismic) > broken > static zone 1`] = ` { "field": { - "id": "__broken__", + "id": "_____broken_____", "isBroken": true, "link_type": "Document", }, diff --git a/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap index ceb749b4..9275be7f 100644 --- a/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap +++ b/test/__snapshots__/writeClient-migrate-patch-linkToMedia.test.ts.snap @@ -14,14 +14,8 @@ exports[`patches link to media fields (from Prismic) > inRichText > group 1`] = }, { "data": { - "height": "1", "id": "fc26fe8d098", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -54,14 +48,8 @@ exports[`patches link to media fields (from Prismic) > inRichText > shared slice }, { "data": { - "height": "1", "id": "961adcf1f5d", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -85,14 +73,8 @@ exports[`patches link to media fields (from Prismic) > inRichText > shared slice }, { "data": { - "height": "1", "id": "961adcf1f5d", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -115,14 +97,8 @@ exports[`patches link to media fields (from Prismic) > inRichText > shared slice }, { "data": { - "height": "1", "id": "961adcf1f5d", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -162,14 +138,8 @@ exports[`patches link to media fields (from Prismic) > inRichText > slice 1`] = }, { "data": { - "height": "1", "id": "54f4a69ff72", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -193,14 +163,8 @@ exports[`patches link to media fields (from Prismic) > inRichText > slice 1`] = }, { "data": { - "height": "1", "id": "54f4a69ff72", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -231,14 +195,8 @@ exports[`patches link to media fields (from Prismic) > inRichText > static zone }, { "data": { - "height": "1", "id": "1d17b04a95c", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -257,14 +215,8 @@ exports[`patches link to media fields (from Prismic) > simple > group 1`] = ` "group": [ { "field": { - "height": "1", "id": "fc26fe8d098", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - "width": "1", }, }, ], @@ -279,39 +231,21 @@ exports[`patches link to media fields (from Prismic) > simple > shared slice 1`] "items": [ { "field": { - "height": "1", "id": "961adcf1f5d", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", - "width": "1", }, }, ], "primary": { "field": { - "height": "1", "id": "961adcf1f5d", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", - "width": "1", }, "group": [ { "field": { - "height": "1", "id": "961adcf1f5d", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1418065460487-3e41a6c84dc5?w=2200&h=1467&fit=crop", - "width": "1", }, }, ], @@ -333,27 +267,15 @@ exports[`patches link to media fields (from Prismic) > simple > slice 1`] = ` "items": [ { "field": { - "height": "1", "id": "54f4a69ff72", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", - "width": "1", }, }, ], "primary": { "field": { - "height": "1", "id": "54f4a69ff72", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1497436072909-60f360e1d4b1?w=2560&h=1440&fit=crop", - "width": "1", }, }, "slice_label": "Vel", @@ -366,14 +288,85 @@ exports[`patches link to media fields (from Prismic) > simple > slice 1`] = ` exports[`patches link to media fields (from Prismic) > simple > static zone 1`] = ` { "field": { - "height": "1", "id": "1d17b04a95c", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", + }, +} +`; + +exports[`patches link to media fields > empty > group 1`] = ` +{ + "group": [ + { + "field": { + "link_type": "Media", + }, + }, + ], +} +`; + +exports[`patches link to media fields > empty > shared slice 1`] = ` +{ + "slices": [ + { + "id": "9b9dd0f2c3f", + "items": [ + { + "field": { + "link_type": "Media", + }, + }, + ], + "primary": { + "field": { + "link_type": "Media", + }, + "group": [ + { + "field": { + "link_type": "Media", + }, + }, + ], + }, + "slice_label": null, + "slice_type": "nunc", + "variation": "ullamcorper", + "version": "8bfc905", + }, + ], +} +`; + +exports[`patches link to media fields > empty > slice 1`] = ` +{ + "slices": [ + { + "id": "5306297c5ed", + "items": [ + { + "field": { + "link_type": "Media", + }, + }, + ], + "primary": { + "field": { + "link_type": "Media", + }, + }, + "slice_label": "Vel", + "slice_type": "hac_habitasse", + }, + ], +} +`; + +exports[`patches link to media fields > empty > static zone 1`] = ` +{ + "field": { + "link_type": "Media", }, } `; @@ -509,14 +502,8 @@ exports[`patches link to media fields > new > group 1`] = ` "group": [ { "field": { - "height": "1", "id": "fdd322ca9d5", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", - "width": "1", }, }, ], @@ -531,39 +518,21 @@ exports[`patches link to media fields > new > shared slice 1`] = ` "items": [ { "field": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, }, ], "primary": { "field": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, "group": [ { "field": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, }, ], @@ -585,27 +554,15 @@ exports[`patches link to media fields > new > slice 1`] = ` "items": [ { "field": { - "height": "1", "id": "82beb55ec2f", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - "width": "1", }, }, ], "primary": { "field": { - "height": "1", "id": "82beb55ec2f", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - "width": "1", }, }, "slice_label": "Vel", @@ -618,14 +575,8 @@ exports[`patches link to media fields > new > slice 1`] = ` exports[`patches link to media fields > new > static zone 1`] = ` { "field": { - "height": "1", "id": "bad670dad77", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - "width": "1", }, } `; @@ -636,11 +587,7 @@ exports[`patches link to media fields > newNonImage > group 1`] = ` { "field": { "id": "fdd322ca9d5", - "kind": "document", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", }, }, ], @@ -656,32 +603,20 @@ exports[`patches link to media fields > newNonImage > shared slice 1`] = ` { "field": { "id": "c0eddbd0255", - "kind": "document", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], "primary": { "field": { "id": "c0eddbd0255", - "kind": "document", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, "group": [ { "field": { "id": "c0eddbd0255", - "kind": "document", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", }, }, ], @@ -704,22 +639,14 @@ exports[`patches link to media fields > newNonImage > slice 1`] = ` { "field": { "id": "82beb55ec2f", - "kind": "document", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, ], "primary": { "field": { "id": "82beb55ec2f", - "kind": "document", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", }, }, "slice_label": "Vel", @@ -733,11 +660,7 @@ exports[`patches link to media fields > newNonImage > static zone 1`] = ` { "field": { "id": "bad670dad77", - "kind": "document", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", }, } `; @@ -747,15 +670,9 @@ exports[`patches link to media fields > newWithText > group 1`] = ` "group": [ { "field": { - "height": "1", "id": "fdd322ca9d5", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", "text": "foo", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", - "width": "1", }, }, ], @@ -770,42 +687,24 @@ exports[`patches link to media fields > newWithText > shared slice 1`] = ` "items": [ { "field": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", "text": "foo", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, }, ], "primary": { "field": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", "text": "foo", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, "group": [ { "field": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", "text": "foo", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, }, ], @@ -827,29 +726,17 @@ exports[`patches link to media fields > newWithText > slice 1`] = ` "items": [ { "field": { - "height": "1", "id": "82beb55ec2f", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", "text": "foo", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - "width": "1", }, }, ], "primary": { "field": { - "height": "1", "id": "82beb55ec2f", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", "text": "foo", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - "width": "1", }, }, "slice_label": "Vel", @@ -862,15 +749,9 @@ exports[`patches link to media fields > newWithText > slice 1`] = ` exports[`patches link to media fields > newWithText > static zone 1`] = ` { "field": { - "height": "1", "id": "bad670dad77", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", "text": "foo", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - "width": "1", }, } `; @@ -1141,14 +1022,8 @@ exports[`patches link to media fields > richTextNew > group 1`] = ` }, { "data": { - "height": "1", "id": "fdd322ca9d5", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1587502537745-84b86da1204f?w=6550&h=4367&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -1181,14 +1056,8 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` }, { "data": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -1212,14 +1081,8 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` }, { "data": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -1242,14 +1105,8 @@ exports[`patches link to media fields > richTextNew > shared slice 1`] = ` }, { "data": { - "height": "1", "id": "c0eddbd0255", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=5616&h=3744&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -1289,14 +1146,8 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` }, { "data": { - "height": "1", "id": "82beb55ec2f", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -1320,14 +1171,8 @@ exports[`patches link to media fields > richTextNew > slice 1`] = ` }, { "data": { - "height": "1", "id": "82beb55ec2f", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=7372&h=4392&fit=crop", - "width": "1", }, "end": 5, "start": 0, @@ -1358,14 +1203,8 @@ exports[`patches link to media fields > richTextNew > static zone 1`] = ` }, { "data": { - "height": "1", "id": "bad670dad77", - "kind": "image", "link_type": "Media", - "name": "default.jpg", - "size": "1", - "url": "https://images.unsplash.com/photo-1604537466608-109fa2f16c3b?w=4240&h=2832&fit=crop", - "width": "1", }, "end": 5, "start": 0, diff --git a/test/writeClient-migrate-documents.test.ts b/test/writeClient-migrate-documents.test.ts index 50062671..ccfc6d49 100644 --- a/test/writeClient-migrate-documents.test.ts +++ b/test/writeClient-migrate-documents.test.ts @@ -331,29 +331,30 @@ it.concurrent( const client = createTestWriteClient({ ctx }) const { repository, masterLocale } = createRepository(ctx) - const queryResponse = createPagedQueryResponses({ - ctx, - pages: 1, - pageSize: 1, - }) - const masterLanguageDocument = queryResponse[0].results[0] + const masterLanguageDocument = ctx.mock.value.document() masterLanguageDocument.lang = masterLocale const document = ctx.mock.value.document({ alternateLanguages: [masterLanguageDocument], }) - const newDocument = { - id: "foo", - masterLanguageDocumentID: masterLanguageDocument.id, - } + const newDocuments = [ + { + id: "foo", + }, + { + id: "bar", + masterLanguageDocumentID: "foo", + }, + ] - mockPrismicRestAPIV2({ ctx, repositoryResponse: repository, queryResponse }) + mockPrismicRestAPIV2({ ctx, repositoryResponse: repository }) mockPrismicAssetAPI({ ctx, client }) - mockPrismicMigrationAPI({ ctx, client, newDocuments: [newDocument] }) + mockPrismicMigrationAPI({ ctx, client, newDocuments }) const migration = prismic.createMigration() - const doc = migration.createDocumentFromPrismic(document, "foo") + migration.createDocumentFromPrismic(masterLanguageDocument, "foo") + const doc = migration.createDocumentFromPrismic(document, "bar") const reporter = vi.fn() @@ -362,13 +363,13 @@ it.concurrent( ctx.expect(reporter).toHaveBeenCalledWith({ type: "documents:creating", data: { - current: 1, + current: 2, remaining: 0, - total: 1, + total: 2, document: doc, }, }) - ctx.expect(doc.document.id).toBe(newDocument.id) + ctx.expect(doc.document.id).toBe("bar") ctx.expect.assertions(3) }, ) diff --git a/test/writeClient-migrate-patch-linkToMedia.test.ts b/test/writeClient-migrate-patch-linkToMedia.test.ts index 3e394a1e..efdf0984 100644 --- a/test/writeClient-migrate-patch-linkToMedia.test.ts +++ b/test/writeClient-migrate-patch-linkToMedia.test.ts @@ -57,6 +57,15 @@ testMigrationFieldPatching< } }, existing: ({ existingAssets }) => assetToLinkToMedia(existingAssets[0]), + empty: ({ migration }) => { + const asset = migration.createAsset("foo", "foo.png") + asset.config.id = "empty" + + return { + link_type: LinkType.Media, + id: asset, + } + }, richTextNew: ({ migration }) => [ { type: RichTextNodeType.paragraph, From 30de0d1de870ee5a8528b6e2b02d89fd05856f38 Mon Sep 17 00:00:00 2001 From: lihbr Date: Wed, 18 Sep 2024 16:21:22 +0200 Subject: [PATCH 59/61] fix: allow not updating document's title --- src/Migration.ts | 45 +++++++++++++++++++-------------- src/WriteClient.ts | 4 +-- src/types/migration/Document.ts | 12 ++++----- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/Migration.ts b/src/Migration.ts index 0f70372d..0d673441 100644 --- a/src/Migration.ts +++ b/src/Migration.ts @@ -63,12 +63,14 @@ export class Migration { * * @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 + * 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 @@ -78,13 +80,15 @@ export class Migration { * * @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 + * 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, @@ -95,7 +99,7 @@ export class Migration { * * @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 + * 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. @@ -121,7 +125,7 @@ export class Migration { * * @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 + * 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. @@ -227,7 +231,7 @@ export class Migration { * Registers a document to be created in the migration. * * @remarks - * This method does not create the document in Prismic right away. Instead it + * 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. * @@ -236,20 +240,20 @@ export class Migration { * @param document - The document to create. * @param title - The title of the document to create which will be displayed * in the editor. - * @param options - Document master language document ID. + * @param params - Document master language document ID. * * @returns A migration document instance. */ createDocument( document: ExtractDocumentType, TType>, title: string, - options?: { + params?: { masterLanguageDocument?: MigrationContentRelationship }, ): PrismicMigrationDocument> { const doc = new PrismicMigrationDocument< ExtractDocumentType - >(document, title, options) + >(document, title, params) this._documents.push(doc) @@ -260,21 +264,22 @@ export class Migration { * Registers an existing document to be updated in the migration. * * @remarks - * This method does not update the document in Prismic right away. Instead it + * 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 documentTitle - The title of the document to update which will be - * displayed in the editor. + * @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: string, + // Title is optional for existing documents as we might not want to update it. + title?: string, ): PrismicMigrationDocument> { const doc = new PrismicMigrationDocument< ExtractDocumentType @@ -286,17 +291,17 @@ export class Migration { } /** - * Registers a document to be created in the migration. + * 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 + * 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 to create. + * @param document - The document from Prismic to create. * @param title - The title of the document to create which will be displayed * in the editor. - * @param options - Document master language document ID. * * @returns A migration document instance. */ @@ -413,7 +418,7 @@ export class Migration { return { link_type: LinkType.Document, - id: () => this.getByOriginalID(input.id), + id: () => this._getByOriginalID(input.id), // TODO: Remove when link text PR is merged // @ts-expect-error - Future-proofing for link text text: input.text, @@ -497,7 +502,7 @@ export class Migration { * * ```ts * const contentRelationship = migration.createContentRelationship(() => - * migration.getByOriginalID("YhdrDxIAACgAcp_b"), + * migration._getByOriginalID("YhdrDxIAACgAcp_b"), * ) * ``` * @@ -507,8 +512,10 @@ export class Migration { * * @returns The migration document instance for the original ID, if a matching * document is found. + * + * @internal */ - getByOriginalID( + _getByOriginalID( id: string, ): | PrismicMigrationDocument> diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 1d642db2..0c689032 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -447,14 +447,14 @@ export class WriteClient< if (maybeOriginalID) { masterLanguageDocumentID = - migration.getByOriginalID(maybeOriginalID)?.document.id + migration._getByOriginalID(maybeOriginalID)?.document.id } } const { id } = await this.createDocument( // We'll upload docuements data later on. { ...doc.document, data: {} }, - doc.title, + doc.title!, { masterLanguageDocumentID, ...fetchParams, diff --git a/src/types/migration/Document.ts b/src/types/migration/Document.ts index 45d0a007..3bcfddf3 100644 --- a/src/types/migration/Document.ts +++ b/src/types/migration/Document.ts @@ -103,7 +103,7 @@ export class PrismicMigrationDocument< /** * The name of the document displayed in the editor. */ - title: string + title?: string /** * The link to the master language document to relate the document to if any. @@ -126,22 +126,22 @@ export class PrismicMigrationDocument< * * @param document - The document to be sent to the Migration API. * @param title - The name of the document displayed in the editor. - * @param options - Parameters to create/update the document with on the + * @param params - Parameters to create/update the document with on the * Migration API. * * @returns A Prismic migration document instance. */ constructor( document: MigrationDocument, - title: string, - options?: { + title?: string, + params?: { masterLanguageDocument?: MigrationContentRelationship originalPrismicDocument?: ExistingPrismicDocument }, ) { this.document = document this.title = title - this.masterLanguageDocument = options?.masterLanguageDocument - this.originalPrismicDocument = options?.originalPrismicDocument + this.masterLanguageDocument = params?.masterLanguageDocument + this.originalPrismicDocument = params?.originalPrismicDocument } } From 5e7ce0a0b9c75bb391e0c7e736664843387cf77d Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 23 Sep 2024 11:11:53 +0200 Subject: [PATCH 60/61] fix: missing migration embed and slice types --- src/WriteClient.ts | 2 +- src/types/migration/Document.ts | 22 ++++++++++++++++------ test/types/migration-document.types.ts | 15 ++++++++++++--- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 0c689032..0d5a867f 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -452,7 +452,7 @@ export class WriteClient< } const { id } = await this.createDocument( - // We'll upload docuements data later on. + // We'll upload documents data later on. { ...doc.document, data: {} }, doc.title!, { diff --git a/src/types/migration/Document.ts b/src/types/migration/Document.ts index 3bcfddf3..a82d0860 100644 --- a/src/types/migration/Document.ts +++ b/src/types/migration/Document.ts @@ -1,8 +1,10 @@ import type { FilledContentRelationshipField } from "../value/contentRelationship" import type { PrismicDocument, PrismicDocumentWithUID } from "../value/document" +import type { AnyOEmbed, EmbedField, OEmbedExtra } from "../value/embed" import type { FilledImageFieldImage } from "../value/image" import type { FilledLinkToMediaField } from "../value/linkToMedia" import type { RTImageNode } from "../value/richText" +import type { SharedSlice } from "../value/sharedSlice" import type { MigrationImage, @@ -29,12 +31,20 @@ export type InjectMigrationSpecificTypes = T extends RTImageNode ? T | MigrationLinkToMedia | undefined : T extends FilledContentRelationshipField ? T | MigrationContentRelationship - : // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends Record - ? { [P in keyof T]: InjectMigrationSpecificTypes } - : T extends Array - ? Array> - : T + : T extends EmbedField + ? T | Pick + : T extends SharedSlice + ? + | T + | InjectMigrationSpecificTypes< + Omit + > + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends Record + ? { [P in keyof T]: InjectMigrationSpecificTypes } + : T extends Array + ? Array> + : T /** * A utility type that ties the type and data of a Prismic document, creating a diff --git a/test/types/migration-document.types.ts b/test/types/migration-document.types.ts index afe5ccea..21d39a62 100644 --- a/test/types/migration-document.types.ts +++ b/test/types/migration-document.types.ts @@ -104,6 +104,8 @@ type Fields = { migrationLinkToMedia: prismic.LinkToMediaField contentRelationship: prismic.ContentRelationshipField migrationContentRelationship: prismic.ContentRelationshipField + embedField: prismic.EmbedField + migrationEmbedField: prismic.EmbedField } type StaticDocument = prismic.PrismicDocument @@ -142,6 +144,8 @@ expectType({ migrationLinkToMedia: {} as prismic.MigrationLinkToMedia, contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.MigrationContentRelationship, + embedField: {} as prismic.EmbedField, + migrationEmbedField: { embed_url: "https://example.com" }, }, }) @@ -160,6 +164,8 @@ expectType({ contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.MigrationContentRelationship, + embedField: {} as prismic.EmbedField, + migrationEmbedField: { embed_url: "https://example.com" }, }, ], }, @@ -174,10 +180,7 @@ expectType({ slices: [ { slice_type: "default", - slice_label: null, - id: "", variation: "default", - version: "", primary: { image: {} as prismic.ImageField, migrationImage: {} as prismic.MigrationImage, @@ -186,6 +189,8 @@ expectType({ contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.MigrationContentRelationship, + embedField: {} as prismic.EmbedField, + migrationEmbedField: { embed_url: "https://example.com" }, group: [ { image: {} as prismic.ImageField, @@ -195,6 +200,8 @@ expectType({ contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.MigrationContentRelationship, + embedField: {} as prismic.EmbedField, + migrationEmbedField: { embed_url: "https://example.com" }, }, ], }, @@ -207,6 +214,8 @@ expectType({ contentRelationship: {} as prismic.ContentRelationshipField, migrationContentRelationship: {} as prismic.MigrationContentRelationship, + embedField: {} as prismic.EmbedField, + migrationEmbedField: { embed_url: "https://example.com" }, }, ], }, From bac953b5078381cf6e477f4c0bcc9702441a3d3e Mon Sep 17 00:00:00 2001 From: lihbr Date: Mon, 23 Sep 2024 16:54:11 +0200 Subject: [PATCH 61/61] docs: add `migrate` example --- examples/migrate/README.md | 26 ++++++ examples/migrate/migrate.mjs | 101 +++++++++++++++++++++ examples/migrate/migrate.ts | 102 ++++++++++++++++++++++ examples/migrate/package.json | 11 +++ examples/migrate/slicemachine.config.json | 6 ++ examples/migrate/tsconfig.json | 14 +++ 6 files changed, 260 insertions(+) create mode 100644 examples/migrate/README.md create mode 100644 examples/migrate/migrate.mjs create mode 100644 examples/migrate/migrate.ts create mode 100644 examples/migrate/package.json create mode 100644 examples/migrate/slicemachine.config.json create mode 100644 examples/migrate/tsconfig.json diff --git a/examples/migrate/README.md b/examples/migrate/README.md new file mode 100644 index 00000000..7d89e546 --- /dev/null +++ b/examples/migrate/README.md @@ -0,0 +1,26 @@ +# Migrate + +This example shows a migration script leveraging `@prismicio/client`'s write client +and the migration helper to migrate existing content to Prismic. `migrate.ts` is the +TypeScript exact equivalent of `migrate.mjs`. + +Learn more about migrating to Prismic on the [migration documentation](https://prismic.io/docs/migration). + +## How to run the example + +> Scripts in this example uses an hypothetical WordPress client, therefore, they are not runnable as-is. + +```sh +# Clone the repository to your computer +git clone https://github.com/prismicio/prismic-client.git +cd prismic-client/examples/migrate + +# Install the dependencies +npm install + +# Run the example (TypeScript) +npx tsx migrate.ts + +# Run the example (JavaScript) +node migrate.mjs +``` diff --git a/examples/migrate/migrate.mjs b/examples/migrate/migrate.mjs new file mode 100644 index 00000000..99d199db --- /dev/null +++ b/examples/migrate/migrate.mjs @@ -0,0 +1,101 @@ +import * as prismic from "@prismicio/client" +import { htmlAsRichText } from "@prismicio/migrate" +import "dotenv/config" +// An hypothetical WordPress client +import { createWordPressClient } from "wordpress" + +import { repositoryName } from "./slicemachine.config.json" + +// Prismic setup +const writeClient = prismic.createWriteClient(repositoryName, { + writeToken: process.env.PRISMIC_WRITE_TOKEN, +}) + +const migration = prismic.createMigration() + +// Custom migration script logic + +const convertWPDocument = (wpDocument) => { + switch (wpDocument.type) { + case "page": + return convertWPPage(wpDocument) + case "settings": + return convertWPSettings(wpDocument) + } + + throw new Error(`Unsupported document type: ${wpDocument.type}`) +} + +const convertWPPage = (wpPage) => { + return migration.createDocument( + { + type: "page", + lang: wpPage.lang, + uid: wpPage.slug, + tags: ["wordpress"], + data: { + meta_title: wpPage.meta_title, + meta_description: wpPage.meta_description, + meta_image: migration.createAsset( + wpPage.meta_image.url, + wpPage.meta_image.name, + ), + title: wpHTMLAsRichText(wpPage.title), + body: wpHTMLAsRichText(wpPage.content), + }, + }, + wpPage.name, + { + masterLanguageDocument: () => + migration.getByUID( + wpPage.masterLanguageDocument.type, + wpPage.masterLanguageDocument.uid, + ), + }, + ) +} + +const convertWPSettings = (wpSettings) => { + return migration.createDocument( + { + type: "settings", + lang: wpSettings.lang, + tags: ["wordpress"], + data: { + title: wpHTMLAsRichText(wpSettings.name), + }, + }, + "Settings", + ) +} + +const wpHTMLAsRichText = (html) => { + return htmlAsRichText(html, { + serializer: { + img: ({ node }) => { + const src = node.properties.src + const filename = src.split("/").pop() + const alt = node.properties.alt + + return { + type: "image", + id: migration.createAsset(src, filename, { alt }), + } + }, + }, + }).result +} + +// Fetching and converting WordPress documents +const wpClient = createWordPressClient("https://example.com/wp-json") + +const wpDocuments = await wpClient.dangerouslyGetAllDocuments() + +for (const wpDocument of wpDocuments) { + convertWPDocument(wpDocument) +} + +// Execute the prepared migration at the very end of the script +await writeClient.migrate(migration, { + reporter: (event) => console.info(event), +}) diff --git a/examples/migrate/migrate.ts b/examples/migrate/migrate.ts new file mode 100644 index 00000000..abdc7603 --- /dev/null +++ b/examples/migrate/migrate.ts @@ -0,0 +1,102 @@ +import * as prismic from "@prismicio/client" +import { htmlAsRichText } from "@prismicio/migrate" +import "dotenv/config" +// An hypothetical WordPress client +// @ts-expect-error - This is an hypothetical WordPress client +import { type WPDocument, createWordPressClient } from "wordpress" + +import { repositoryName } from "./slicemachine.config.json" + +// Prismic setup +const writeClient = prismic.createWriteClient(repositoryName, { + writeToken: process.env.PRISMIC_WRITE_TOKEN!, +}) + +const migration = prismic.createMigration() + +// Custom migration script logic + +const convertWPDocument = (wpDocument: WPDocument) => { + switch (wpDocument.type) { + case "page": + return convertWPPage(wpDocument) + case "settings": + return convertWPSettings(wpDocument) + } + + throw new Error(`Unsupported document type: ${wpDocument.type}`) +} + +const convertWPPage = (wpPage: WPDocument) => { + return migration.createDocument( + { + type: "page", + lang: wpPage.lang, + uid: wpPage.slug, + tags: ["wordpress"], + data: { + meta_title: wpPage.meta_title, + meta_description: wpPage.meta_description, + meta_image: migration.createAsset( + wpPage.meta_image.url, + wpPage.meta_image.name, + ), + title: wpHTMLAsRichText(wpPage.title), + body: wpHTMLAsRichText(wpPage.content), + }, + }, + wpPage.name, + { + masterLanguageDocument: () => + migration.getByUID( + wpPage.masterLanguageDocument.type, + wpPage.masterLanguageDocument.uid, + ), + }, + ) +} + +const convertWPSettings = (wpSettings: WPDocument) => { + return migration.createDocument( + { + type: "settings", + lang: wpSettings.lang, + tags: ["wordpress"], + data: { + title: wpHTMLAsRichText(wpSettings.name), + }, + }, + "Settings", + ) +} + +const wpHTMLAsRichText = (html: string) => { + return htmlAsRichText(html, { + serializer: { + img: ({ node }) => { + const src = node.properties.src as string + const filename = src.split("/").pop()! + const alt = node.properties.alt as string + + return { + type: "image", + id: migration.createAsset(src, filename, { alt }), + } + }, + }, + }).result +} + +// Fetching and converting WordPress documents +const wpClient = createWordPressClient("https://example.com/wp-json") + +const wpDocuments = await wpClient.dangerouslyGetAllDocuments() + +for (const wpDocument of wpDocuments) { + convertWPDocument(wpDocument) +} + +// Execute the prepared migration at the very end of the script +await writeClient.migrate(migration, { + reporter: (event) => console.info(event), +}) diff --git a/examples/migrate/package.json b/examples/migrate/package.json new file mode 100644 index 00000000..e46fa25b --- /dev/null +++ b/examples/migrate/package.json @@ -0,0 +1,11 @@ +{ + "type": "module", + "dependencies": { + "@prismicio/client": "latest", + "@prismicio/migrate": "latest" + }, + "devDependencies": { + "tsx": "^4.19.1", + "typescript": "^5.6.2" + } +} diff --git a/examples/migrate/slicemachine.config.json b/examples/migrate/slicemachine.config.json new file mode 100644 index 00000000..4e437b89 --- /dev/null +++ b/examples/migrate/slicemachine.config.json @@ -0,0 +1,6 @@ +{ + "libraries": ["./slices"], + "adapter": "@slicemachine/adapter-next", + "repositoryName": "qwerty", + "localSliceSimulatorURL": "http://localhost:3000/slice-simulator" +} diff --git a/examples/migrate/tsconfig.json b/examples/migrate/tsconfig.json new file mode 100644 index 00000000..7569e1d0 --- /dev/null +++ b/examples/migrate/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true + } +}