UNPKG

@prismicio/client

Version:

The official JavaScript + TypeScript client library for Prismic

488 lines (486 loc) 18.2 kB
import { name, version } from "./package.js"; import { devMsg } from "./lib/devMsg.js"; import { pLimit } from "./lib/pLimit.js"; import { request } from "./lib/request.js"; import { ForbiddenError, InvalidDataError, NotFoundError, PrismicError } from "./errors.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 = {}) { var _params$reporter, _params$reporter2; (_params$reporter = params.reporter) === null || _params$reporter === void 0 || _params$reporter.call(params, { 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$reporter2 = params.reporter) === null || _params$reporter2 === void 0 || _params$reporter2.call(params, { 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 === null || reporter === void 0 || 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 === null || reporter === void 0 || 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 === null || reporter === void 0 || 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 === null || reporter === void 0 || 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) { var _doc$originalPrismicD; const maybeOriginalID = (_doc$originalPrismicD = doc.originalPrismicDocument.alternate_languages.find(({ lang }) => lang === masterLocale)) === null || _doc$originalPrismicD === void 0 ? void 0 : _doc$originalPrismicD.id; if (maybeOriginalID) { var _migration$_getByOrig; masterLanguageDocumentID = (_migration$_getByOrig = migration._getByOriginalID(maybeOriginalID)) === null || _migration$_getByOrig === void 0 ? void 0 : _migration$_getByOrig.document.id; } } const { id } = await this.createDocument({ ...doc.document, data: {} }, doc.title, { masterLanguageDocumentID, ...fetchParams }); doc.document.id = id; } reporter === null || reporter === void 0 || 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 === null || reporter === void 0 || 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 === null || reporter === void 0 || 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 blob = await res.blob(); return new File([blob], "", { 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$1, params) { const url = new URL("tags", this.assetAPIEndpoint); const response = await this.#request(url, params, { method: "POST", body: JSON.stringify({ name: name$1 }), 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) { var _this$fetchOptions, _params$fetchOptions, _params$fetchOptions2, _this$fetchOptions2; return await request(url, { ...this.fetchOptions, ...params === null || params === void 0 ? void 0 : params.fetchOptions, ...init, headers: { ...(_this$fetchOptions = this.fetchOptions) === null || _this$fetchOptions === void 0 ? void 0 : _this$fetchOptions.headers, ...params === null || params === void 0 || (_params$fetchOptions = params.fetchOptions) === null || _params$fetchOptions === void 0 ? void 0 : _params$fetchOptions.headers, ...init === null || init === void 0 ? void 0 : init.headers, repository: this.repositoryName, authorization: `Bearer ${this.writeToken}` }, signal: (params === null || params === void 0 || (_params$fetchOptions2 = params.fetchOptions) === null || _params$fetchOptions2 === void 0 ? void 0 : _params$fetchOptions2.signal) || (params === null || params === void 0 ? void 0 : params.signal) || ((_this$fetchOptions2 = this.fetchOptions) === null || _this$fetchOptions2 === void 0 ? void 0 : _this$fetchOptions2.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); case 500: case 503: 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); case 500: default: throw new PrismicError(message, response.url, payload); } } }; //#endregion export { WriteClient }; //# sourceMappingURL=WriteClient.js.map