UNPKG

@prismicio/client

Version:

The official JavaScript + TypeScript client library for Prismic

446 lines (445 loc) 16.6 kB
import { name, version } from "./package.js"; import { devMsg } from "./lib/devMsg.js"; import { ForbiddenError, InvalidDataError, NotFoundError, PrismicError } from "./errors.js"; import { pLimit } from "./lib/pLimit.js"; import { request } from "./lib/request.js"; import { Client } from "./Client.js"; import { resolveMigrationContentRelationship, resolveMigrationDocumentData } from "./lib/resolveMigrationDocumentData.js"; //#region src/WriteClient.ts const CLIENT_IDENTIFIER = `${name.replace("@", "").replace("/", "-")}/${version}`; /** * 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. */ var WriteClient = class extends Client { writeToken; 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, options) { super(repositoryName, options); 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. Consider using Prismic write client in a server environment only, preferring the regular client for browser environment. For more details, see ${devMsg("avoid-write-client-in-browser")}`); this.writeToken = options.writeToken; if (options.assetAPIEndpoint) this.assetAPIEndpoint = `${options.assetAPIEndpoint}/`; if (options.migrationAPIEndpoint) this.migrationAPIEndpoint = `${options.migrationAPIEndpoint}/`; } /** * 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 reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ async migrate(migration, params = {}) { params.reporter?.({ type: "start", data: { pending: { documents: migration._documents.length, assets: migration._assets.size } } }); await this.migrateCreateAssets(migration, params); await this.migrateCreateDocuments(migration, params); await this.migrateUpdateDocuments(migration, params); params.reporter?.({ type: "end", data: { migrated: { documents: migration._documents.length, assets: migration._assets.size } } }); } /** * 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. * @internal This method is one of the step performed by the {@link migrate} method. */ async migrateCreateAssets(migration, { reporter, ...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 } }); const { file, filename, notes, credits, alt, tags } = migrationAsset.config; let resolvedFile; if (typeof file === "string") { let url; try { url = new URL(file); } catch {} 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; migrationAsset.asset = await this.createAsset(resolvedFile, filename, { notes, credits, alt, tags, ...fetchParams }); } reporter?.({ type: "assets:created", data: { created } }); } /** * 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. * @internal This method is one of the step performed by the {@link migrate} method. */ async migrateCreateDocuments(migration, { reporter, ...fetchParams } = {}) { const masterLocale = (await this.getRepository(fetchParams)).languages[0].id; reporter?.({ type: "documents:masterLocale", data: { masterLocale } }); const documentsToCreate = []; for (const doc of migration._documents) if (!doc.document.id) if (doc.document.lang === masterLocale) documentsToCreate.unshift(doc); else documentsToCreate.push(doc); let created = 0; for (const doc of documentsToCreate) { reporter?.({ type: "documents:creating", data: { current: ++created, remaining: documentsToCreate.length - created, total: documentsToCreate.length, document: doc } }); let masterLanguageDocumentID; if (doc.masterLanguageDocument) { const masterLanguageDocument = await resolveMigrationContentRelationship(doc.masterLanguageDocument); masterLanguageDocumentID = "id" in masterLanguageDocument ? masterLanguageDocument.id : void 0; } else if (doc.originalPrismicDocument) { const maybeOriginalID = doc.originalPrismicDocument.alternate_languages.find(({ lang }) => lang === masterLocale)?.id; if (maybeOriginalID) masterLanguageDocumentID = migration._getByOriginalID(maybeOriginalID)?.document.id; } const { id } = await this.createDocument({ ...doc.document, data: {} }, doc.title, { masterLanguageDocumentID, ...fetchParams }); doc.document.id = id; } reporter?.({ type: "documents:created", data: { created } }); } /** * Updates documents in the Prismic repository's migration release with their patched data. * * @param migration - A migration prepared with {@link createMigration}. * @param params - An event listener and additional fetch parameters. * @internal This method is one of the step performed by the {@link migrate} method. */ async migrateUpdateDocuments(migration, { reporter, ...fetchParams } = {}) { let i = 0; for (const doc of migration._documents) { reporter?.({ type: "documents:updating", data: { current: ++i, remaining: migration._documents.length - i, total: migration._documents.length, document: doc } }); await this.updateDocument(doc.document.id, { ...doc.document, documentTitle: doc.title, data: await resolveMigrationDocumentData(doc.document.data, migration) }, fetchParams); } reporter?.({ type: "documents:updated", data: { updated: migration._documents.length } }); } /** * 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. */ async createAsset(file, filename, { notes, credits, alt, tags, ...params } = {}) { const url = new URL("assets", this.assetAPIEndpoint); const formData = new FormData(); formData.append("file", new File([file], filename, { type: file instanceof File ? file.type : void 0 })); if (notes) formData.append("notes", notes); if (credits) formData.append("credits", credits); if (alt) formData.append("alt", alt); const response = await this.#request(url, params, { method: "POST", body: formData }); switch (response.status) { case 200: { const asset = await response.json(); if (tags && tags.length) return this.updateAsset(asset.id, { tags }); return asset; } default: return await this.#handleAssetAPIError(response); } } /** * 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. */ async updateAsset(id, { notes, credits, alt, filename, tags, ...params } = {}) { const url = new URL(`assets/${id}`, this.assetAPIEndpoint); if (tags && tags.length) tags = await this.resolveAssetTagIDs(tags, { createTags: true, ...params }); const response = await this.#request(url, params, { method: "PATCH", body: JSON.stringify({ notes, credits, alt, filename, tags }), headers: { "content-type": "application/json" } }); switch (response.status) { case 200: return await response.json(); default: return await this.#handleAssetAPIError(response); } } /** * 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. */ async fetchForeignAsset(url, params = {}) { const res = await this.#request(new URL(url), params); if (!res.ok) throw new PrismicError("Could not fetch foreign asset", url, void 0); const buffer = await res.arrayBuffer(); return new File([buffer], "", { type: res.headers.get("content-type") || void 0 }); } /** {@link resolveAssetTagIDs} rate limiter. */ _resolveAssetTagIDsLimit = pLimit(); /** * Resolves asset tag IDs from tag names. * * @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. */ async resolveAssetTagIDs(tagNames = [], { createTags, ...params } = {}) { return this._resolveAssetTagIDsLimit(async () => { const existingTags = await this.getAssetTags(params); const existingTagMap = {}; for (const tag of existingTags) existingTagMap[tag.name] = tag; const resolvedTagIDs = []; for (const tagName of tagNames) { if (!existingTagMap[tagName] && createTags) existingTagMap[tagName] = await this.createAssetTag(tagName, params); if (existingTagMap[tagName]) resolvedTagIDs.push(existingTagMap[tagName].id); } return resolvedTagIDs; }); } /** * 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. */ async createAssetTag(name, params) { const url = new URL("tags", this.assetAPIEndpoint); const response = await this.#request(url, params, { method: "POST", body: JSON.stringify({ name }), headers: { "content-type": "application/json" } }); switch (response.status) { case 201: return await response.json(); default: return await this.#handleAssetAPIError(response); } } /** * Queries existing tags from the Asset API. * * @param params - Additional fetch parameters. * @returns An array of existing tags. */ async getAssetTags(params) { const url = new URL("tags", this.assetAPIEndpoint); const response = await this.#request(url, params); switch (response.status) { case 200: return (await response.json()).items; default: return await this.#handleAssetAPIError(response); } } /** * Creates a document in the repository's migration release. * * @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 and additional fetch parameters. * @returns The ID of the created document. * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ async createDocument(document, documentTitle, { masterLanguageDocumentID, ...params } = {}) { const url = new URL("documents", this.migrationAPIEndpoint); const response = await this.#request(url, params, { method: "POST", body: JSON.stringify({ title: documentTitle, type: document.type, uid: document.uid || void 0, lang: document.lang, alternate_language_id: masterLanguageDocumentID, tags: document.tags, data: document.data }), headers: { "content-type": "application/json", "x-client": CLIENT_IDENTIFIER } }); switch (response.status) { case 201: return { id: (await response.json()).id }; default: return await this.#handleMigrationAPIError(response); } } /** * 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 content to update. * @param params - Additional fetch parameters. * @see Prismic Migration API technical reference: {@link https://prismic.io/docs/migration-api-technical-reference} */ async updateDocument(id, document, params) { const url = new URL(`documents/${id}`, this.migrationAPIEndpoint); const response = await this.#request(url, params, { method: "PUT", body: JSON.stringify({ title: document.documentTitle, uid: document.uid || void 0, tags: document.tags, data: document.data }), headers: { "content-type": "application/json", "x-client": CLIENT_IDENTIFIER } }); switch (response.status) { case 200: return; default: await this.#handleMigrationAPIError(response); } } /** * Makes an authenticated HTTP request for write operations using the client's configured fetch * function and options. * * @param url - The URL to request. * @param params - Fetch options from the user. * @param init - Additional fetch options to merge with the user-provided options. * @returns The response from the fetch request. */ async #request(url, params, init) { return await request(url, { ...this.fetchOptions, ...params?.fetchOptions, ...init, headers: { ...this.fetchOptions?.headers, ...params?.fetchOptions?.headers, ...init?.headers, repository: this.repositoryName, authorization: `Bearer ${this.writeToken}` }, signal: params?.fetchOptions?.signal || params?.signal || this.fetchOptions?.signal }, this.fetchFn); } /** * Handles error responses from the Asset API with comprehensive error parsing. * * @param response - The HTTP response from the Asset API. * @throws {@link InvalidDataError} For 400 errors. * @throws {@link ForbiddenError} For 401 and 403 errors. * @throws {@link NotFoundError} For 404 errors. * @throws {@link PrismicError} For 500, 503, and other unexpected errors. */ async #handleAssetAPIError(response) { const json = await response.json(); switch (response.status) { case 401: case 403: throw new ForbiddenError(json.error, response.url, json); case 404: throw new NotFoundError(json.error, response.url, json); case 400: throw new InvalidDataError(json.error, response.url, json); default: throw new PrismicError(json.error, response.url, json); } } /** * Handles error responses from the Migration API with comprehensive error parsing. * * @param response - The HTTP response from the Migration API. * @throws {@link InvalidDataError} For 400 errors. * @throws {@link ForbiddenError} For 401 and 403 errors. * @throws {@link NotFoundError} For 404 errors. * @throws {@link PrismicError} For 500, and other unexpected errors. */ async #handleMigrationAPIError(response) { const payload = await response.json(); const message = payload.message; switch (response.status) { case 400: throw new InvalidDataError(message, response.url, payload); case 401: throw new ForbiddenError(message, response.url, payload); case 403: throw new ForbiddenError(message ?? payload.Message, response.url, payload); case 404: throw new NotFoundError(message, response.url, payload); default: throw new PrismicError(message, response.url, payload); } } }; //#endregion export { WriteClient }; //# sourceMappingURL=WriteClient.js.map