@prismicio/client
Version:
The official JavaScript + TypeScript client library for Prismic
488 lines (486 loc) • 18.2 kB
JavaScript
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