pocketbase-js-sdk-jsvm
Version:
PocketBase JavaScript SDK for JSVM
1,503 lines (1,486 loc) • 72.7 kB
JavaScript
'use strict';
/**
* ClientResponseError is a custom Error class that is intended to wrap
* and normalize any error thrown by `Client.send()`.
*/
class ClientResponseError extends Error {
constructor(errData) {
super("ClientResponseError");
this.url = "";
this.status = 0;
this.response = {};
this.isAbort = false;
this.originalError = null;
// Set the prototype explicitly.
// https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, ClientResponseError.prototype);
if (errData !== null && typeof errData === "object") {
this.url = typeof errData.url === "string" ? errData.url : "";
this.status = typeof errData.status === "number" ? errData.status : 0;
this.isAbort = !!errData.isAbort;
this.originalError = errData.originalError;
if (errData.response !== null && typeof errData.response === "object") {
this.response = errData.response;
}
else if (errData.data !== null && typeof errData.data === "object") {
this.response = errData.data;
}
else {
this.response = {};
}
}
if (!this.originalError && !(errData instanceof ClientResponseError)) {
this.originalError = errData;
}
if (typeof DOMException !== "undefined" && errData instanceof DOMException) {
this.isAbort = true;
}
this.name = "ClientResponseError " + this.status;
this.message = this.response?.message;
if (!this.message) {
if (this.originalError?.cause?.message?.includes("ECONNREFUSED ::1")) {
this.message =
"Failed to connect to the PocketBase server. Try changing the SDK URL from localhost to 127.0.0.1 (https://github.com/pocketbase/js-sdk/issues/21).";
}
else {
this.message = "Something went wrong while processing your request.";
}
}
}
/**
* Alias for `this.response` for backward compatibility.
*/
get data() {
return this.response;
}
/**
* Make a POJO's copy of the current error class instance.
* @see https://github.com/vuex-orm/vuex-orm/issues/255
*/
toJSON() {
return { ...this };
}
}
/**
* -------------------------------------------------------------------
* Simple cookie parse and serialize utilities mostly based on the
* node module https://github.com/jshttp/cookie.
* -------------------------------------------------------------------
*/
/**
* RegExp to match field-content in RFC 7230 sec 3.2
*
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
* obs-text = %x80-FF
*/
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
/**
* Parses the given cookie header string into an object
* The object has the various cookies as keys(names) => values
*/
function cookieParse(str, options) {
const result = {};
if (typeof str !== "string") {
return result;
}
const opt = Object.assign({}, {});
const decode = opt.decode || defaultDecode;
let index = 0;
while (index < str.length) {
const eqIdx = str.indexOf("=", index);
// no more cookie pairs
if (eqIdx === -1) {
break;
}
let endIdx = str.indexOf(";", index);
if (endIdx === -1) {
endIdx = str.length;
}
else if (endIdx < eqIdx) {
// backtrack on prior semicolon
index = str.lastIndexOf(";", eqIdx - 1) + 1;
continue;
}
const key = str.slice(index, eqIdx).trim();
// only assign once
if (undefined === result[key]) {
let val = str.slice(eqIdx + 1, endIdx).trim();
// quoted values
if (val.charCodeAt(0) === 0x22) {
val = val.slice(1, -1);
}
try {
result[key] = decode(val);
}
catch (_) {
result[key] = val; // no decoding
}
}
index = endIdx + 1;
}
return result;
}
/**
* Serialize data into a cookie header.
*
* Serialize the a name value pair into a cookie string suitable for
* http headers. An optional options object specified cookie parameters.
*
* ```js
* cookieSerialize('foo', 'bar', { httpOnly: true }) // "foo=bar; httpOnly"
* ```
*/
function cookieSerialize(name, val, options) {
const opt = Object.assign({}, options || {});
const encode = opt.encode || defaultEncode;
if (!fieldContentRegExp.test(name)) {
throw new TypeError("argument name is invalid");
}
const value = encode(val);
if (value && !fieldContentRegExp.test(value)) {
throw new TypeError("argument val is invalid");
}
let result = name + "=" + value;
if (opt.maxAge != null) {
const maxAge = opt.maxAge - 0;
if (isNaN(maxAge) || !isFinite(maxAge)) {
throw new TypeError("option maxAge is invalid");
}
result += "; Max-Age=" + Math.floor(maxAge);
}
if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) {
throw new TypeError("option domain is invalid");
}
result += "; Domain=" + opt.domain;
}
if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) {
throw new TypeError("option path is invalid");
}
result += "; Path=" + opt.path;
}
if (opt.expires) {
if (!isDate(opt.expires) || isNaN(opt.expires.valueOf())) {
throw new TypeError("option expires is invalid");
}
result += "; Expires=" + opt.expires.toUTCString();
}
if (opt.httpOnly) {
result += "; HttpOnly";
}
if (opt.secure) {
result += "; Secure";
}
if (opt.priority) {
const priority = typeof opt.priority === "string" ? opt.priority.toLowerCase() : opt.priority;
switch (priority) {
case "low":
result += "; Priority=Low";
break;
case "medium":
result += "; Priority=Medium";
break;
case "high":
result += "; Priority=High";
break;
default:
throw new TypeError("option priority is invalid");
}
}
if (opt.sameSite) {
const sameSite = typeof opt.sameSite === "string" ? opt.sameSite.toLowerCase() : opt.sameSite;
switch (sameSite) {
case true:
result += "; SameSite=Strict";
break;
case "lax":
result += "; SameSite=Lax";
break;
case "strict":
result += "; SameSite=Strict";
break;
case "none":
result += "; SameSite=None";
break;
default:
throw new TypeError("option sameSite is invalid");
}
}
return result;
}
/**
* Default URL-decode string value function.
* Optimized to skip native call when no `%`.
*/
function defaultDecode(val) {
return val.indexOf("%") !== -1 ? decodeURIComponent(val) : val;
}
/**
* Default URL-encode value function.
*/
function defaultEncode(val) {
return encodeURIComponent(val);
}
/**
* Determines if value is a Date.
*/
function isDate(val) {
return Object.prototype.toString.call(val) === "[object Date]" || val instanceof Date;
}
let atobPolyfill;
if (typeof atob === "function") {
atobPolyfill = atob;
}
else {
/**
* The code was extracted from:
* https://github.com/davidchambers/Base64.js
*/
atobPolyfill = (input) => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
let str = String(input).replace(/=+$/, "");
if (str.length % 4 == 1) {
throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = "";
// get next character
(buffer = str.charAt(idx++));
// character found in table? initialize bit storage and add its ascii value;
~buffer &&
((bs = bc % 4 ? bs * 64 + buffer : buffer),
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4)
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
: 0) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer);
}
return output;
};
}
/**
* Returns JWT token's payload data.
*/
function getTokenPayload(token) {
if (token) {
try {
const encodedPayload = decodeURIComponent(atobPolyfill(token.split(".")[1])
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join(""));
return JSON.parse(encodedPayload) || {};
}
catch (e) { }
}
return {};
}
/**
* Checks whether a JWT token is expired or not.
* Tokens without `exp` payload key are considered valid.
* Tokens with empty payload (eg. invalid token strings) are considered expired.
*
* @param token The token to check.
* @param [expirationThreshold] Time in seconds that will be subtracted from the token `exp` property.
*/
function isTokenExpired(token, expirationThreshold = 0) {
let payload = getTokenPayload(token);
if (Object.keys(payload).length > 0 &&
(!payload.exp || payload.exp - expirationThreshold > Date.now() / 1000)) {
return false;
}
return true;
}
const defaultCookieKey = "pb_auth";
/**
* Base AuthStore class that stores the auth state in runtime memory (aka. only for the duration of the store instane).
*
* Usually you wouldn't use it directly and instead use the builtin LocalAuthStore, AsyncAuthStore
* or extend it with your own custom implementation.
*/
class BaseAuthStore {
constructor() {
this.baseToken = "";
this.baseModel = null;
this._onChangeCallbacks = [];
}
/**
* Retrieves the stored token (if any).
*/
get token() {
return this.baseToken;
}
/**
* Retrieves the stored model data (if any).
*/
get record() {
return this.baseModel;
}
/**
* @deprecated use `record` instead.
*/
get model() {
return this.baseModel;
}
/**
* Loosely checks if the store has valid token (aka. existing and unexpired exp claim).
*/
get isValid() {
return !isTokenExpired(this.token);
}
/**
* Loosely checks whether the currently loaded store state is for superuser.
*
* Alternatively you can also compare directly `pb.authStore.record?.collectionName`.
*/
get isSuperuser() {
let payload = getTokenPayload(this.token);
return (payload.type == "auth" &&
(this.record?.collectionName == "_superusers" ||
// fallback in case the record field is not populated and assuming
// that the collection crc32 checksum id wasn't manually changed
(!this.record?.collectionName &&
payload.collectionId == "pbc_3142635823")));
}
/**
* @deprecated use `isSuperuser` instead or simply check the record.collectionName property.
*/
get isAdmin() {
console.warn("Please replace pb.authStore.isAdmin with pb.authStore.isSuperuser OR simply check the value of pb.authStore.record?.collectionName");
return this.isSuperuser;
}
/**
* @deprecated use `!isSuperuser` instead or simply check the record.collectionName property.
*/
get isAuthRecord() {
console.warn("Please replace pb.authStore.isAuthRecord with !pb.authStore.isSuperuser OR simply check the value of pb.authStore.record?.collectionName");
return getTokenPayload(this.token).type == "auth" && !this.isSuperuser;
}
/**
* Saves the provided new token and model data in the auth store.
*/
save(token, record) {
this.baseToken = token || "";
this.baseModel = record || null;
this.triggerChange();
}
/**
* Removes the stored token and model data form the auth store.
*/
clear() {
this.baseToken = "";
this.baseModel = null;
this.triggerChange();
}
/**
* Parses the provided cookie string and updates the store state
* with the cookie's token and model data.
*
* NB! This function doesn't validate the token or its data.
* Usually this isn't a concern if you are interacting only with the
* PocketBase API because it has the proper server-side security checks in place,
* but if you are using the store `isValid` state for permission controls
* in a node server (eg. SSR), then it is recommended to call `authRefresh()`
* after loading the cookie to ensure an up-to-date token and model state.
* For example:
*
* ```js
* pb.authStore.loadFromCookie("cookie string...");
*
* try {
* // get an up-to-date auth store state by veryfing and refreshing the loaded auth model (if any)
* pb.authStore.isValid && await pb.collection('users').authRefresh();
* } catch (_) {
* // clear the auth store on failed refresh
* pb.authStore.clear();
* }
* ```
*/
loadFromCookie(cookie, key = defaultCookieKey) {
const rawData = cookieParse(cookie || "")[key] || "";
let data = {};
try {
data = JSON.parse(rawData);
// normalize
if (typeof data === null || typeof data !== "object" || Array.isArray(data)) {
data = {};
}
}
catch (_) { }
this.save(data.token || "", data.record || data.model || null);
}
/**
* Exports the current store state as cookie string.
*
* By default the following optional attributes are added:
* - Secure
* - HttpOnly
* - SameSite=Strict
* - Path=/
* - Expires={the token expiration date}
*
* NB! If the generated cookie exceeds 4096 bytes, this method will
* strip the model data to the bare minimum to try to fit within the
* recommended size in https://www.rfc-editor.org/rfc/rfc6265#section-6.1.
*/
exportToCookie(options, key = defaultCookieKey) {
const defaultOptions = {
secure: true,
sameSite: true,
httpOnly: true,
path: "/",
};
// extract the token expiration date
const payload = getTokenPayload(this.token);
if (payload?.exp) {
defaultOptions.expires = new Date(payload.exp * 1000);
}
else {
defaultOptions.expires = new Date("1970-01-01");
}
// merge with the user defined options
options = Object.assign({}, defaultOptions, options);
const rawData = {
token: this.token,
record: this.record ? JSON.parse(JSON.stringify(this.record)) : null,
};
let result = cookieSerialize(key, JSON.stringify(rawData), options);
const resultLength = typeof Blob !== "undefined" ? new Blob([result]).size : result.length;
// strip down the model data to the bare minimum
if (rawData.record && resultLength > 4096) {
rawData.record = { id: rawData.record?.id, email: rawData.record?.email };
const extraProps = ["collectionId", "collectionName", "verified"];
for (const prop in this.record) {
if (extraProps.includes(prop)) {
rawData.record[prop] = this.record[prop];
}
}
result = cookieSerialize(key, JSON.stringify(rawData), options);
}
return result;
}
/**
* Register a callback function that will be called on store change.
*
* You can set the `fireImmediately` argument to true in order to invoke
* the provided callback right after registration.
*
* Returns a removal function that you could call to "unsubscribe" from the changes.
*/
onChange(callback, fireImmediately = false) {
this._onChangeCallbacks.push(callback);
if (fireImmediately) {
callback(this.token, this.record);
}
return () => {
for (let i = this._onChangeCallbacks.length - 1; i >= 0; i--) {
if (this._onChangeCallbacks[i] == callback) {
delete this._onChangeCallbacks[i]; // removes the function reference
this._onChangeCallbacks.splice(i, 1); // reindex the array
return;
}
}
};
}
triggerChange() {
for (const callback of this._onChangeCallbacks) {
callback && callback(this.token, this.record);
}
}
}
/**
* The default token store for browsers with auto fallback
* to runtime/memory if local storage is undefined (e.g. in node env).
*/
class LocalAuthStore extends BaseAuthStore {
constructor(storageKey = "pocketbase_auth") {
super();
this.storageFallback = {};
this.storageKey = storageKey;
this._bindStorageEvent();
}
/**
* @inheritdoc
*/
get token() {
const data = this._storageGet(this.storageKey) || {};
return data.token || "";
}
/**
* @inheritdoc
*/
get record() {
const data = this._storageGet(this.storageKey) || {};
return data.record || data.model || null;
}
/**
* @deprecated use `record` instead.
*/
get model() {
return this.record;
}
/**
* @inheritdoc
*/
save(token, record) {
this._storageSet(this.storageKey, {
token: token,
record: record,
});
super.save(token, record);
}
/**
* @inheritdoc
*/
clear() {
this._storageRemove(this.storageKey);
super.clear();
}
// ---------------------------------------------------------------
// Internal helpers:
// ---------------------------------------------------------------
/**
* Retrieves `key` from the browser's local storage
* (or runtime/memory if local storage is undefined).
*/
_storageGet(key) {
if (typeof window !== "undefined" && window?.localStorage) {
const rawValue = window.localStorage.getItem(key) || "";
try {
return JSON.parse(rawValue);
}
catch (e) {
// not a json
return rawValue;
}
}
// fallback
return this.storageFallback[key];
}
/**
* Stores a new data in the browser's local storage
* (or runtime/memory if local storage is undefined).
*/
_storageSet(key, value) {
if (typeof window !== "undefined" && window?.localStorage) {
// store in local storage
let normalizedVal = value;
if (typeof value !== "string") {
normalizedVal = JSON.stringify(value);
}
window.localStorage.setItem(key, normalizedVal);
}
else {
// store in fallback
this.storageFallback[key] = value;
}
}
/**
* Removes `key` from the browser's local storage and the runtime/memory.
*/
_storageRemove(key) {
// delete from local storage
if (typeof window !== "undefined" && window?.localStorage) {
window.localStorage?.removeItem(key);
}
// delete from fallback
delete this.storageFallback[key];
}
/**
* Updates the current store state on localStorage change.
*/
_bindStorageEvent() {
if (typeof window === "undefined" ||
!window?.localStorage ||
!window.addEventListener) {
return;
}
window.addEventListener("storage", (e) => {
if (e.key != this.storageKey) {
return;
}
const data = this._storageGet(this.storageKey) || {};
super.save(data.token || "", data.record || data.model || null);
});
}
}
/**
* BaseService class that should be inherited from all API services.
*/
class BaseService {
constructor(client) {
this.client = client;
}
}
class SettingsService extends BaseService {
/**
* Fetch all available app settings.
*
* @throws {ClientResponseError}
*/
getAll(options) {
options = Object.assign({
method: "GET",
}, options);
return this.client.send("/api/settings", options);
}
/**
* Bulk updates app settings.
*
* @throws {ClientResponseError}
*/
update(bodyParams, options) {
options = Object.assign({
method: "PATCH",
body: bodyParams,
}, options);
return this.client.send("/api/settings", options);
}
/**
* Performs a S3 filesystem connection test.
*
* The currently supported `filesystem` are "storage" and "backups".
*
* @throws {ClientResponseError}
*/
testS3(filesystem = "storage", options) {
options = Object.assign({
method: "POST",
body: {
filesystem: filesystem,
},
}, options);
this.client.send("/api/settings/test/s3", options);
return true;
}
/**
* Sends a test email.
*
* The possible `emailTemplate` values are:
* - verification
* - password-reset
* - email-change
*
* @throws {ClientResponseError}
*/
testEmail(collectionIdOrName, toEmail, emailTemplate, options) {
options = Object.assign({
method: "POST",
body: {
email: toEmail,
template: emailTemplate,
collection: collectionIdOrName,
},
}, options);
this.client.send("/api/settings/test/email", options);
return true;
}
/**
* Generates a new Apple OAuth2 client secret.
*
* @throws {ClientResponseError}
*/
generateAppleClientSecret(clientId, teamId, keyId, privateKey, duration, options) {
options = Object.assign({
method: "POST",
body: {
clientId,
teamId,
keyId,
privateKey,
duration,
},
}, options);
return this.client.send("/api/settings/apple/generate-client-secret", options);
}
}
class CrudService extends BaseService {
/**
* Response data decoder.
*/
decode(data) {
return data;
}
getFullList(batchOrqueryParams, options) {
if (typeof batchOrqueryParams == "number") {
return this._getFullList(batchOrqueryParams, options);
}
options = Object.assign({}, batchOrqueryParams, options);
let batch = 500;
if (options.batch) {
batch = options.batch;
delete options.batch;
}
return this._getFullList(batch, options);
}
/**
* Returns paginated items list.
*
* You can use the generic T to supply a wrapper type of the crud model.
*
* @throws {ClientResponseError}
*/
getList(page = 1, perPage = 30, options) {
options = Object.assign({
method: "GET",
}, options);
options.query = Object.assign({
page: page,
perPage: perPage,
}, options.query);
const responseData = this.client.send(this.baseCrudPath, options);
responseData.items =
responseData.items?.map((item) => {
return this.decode(item);
}) || [];
return responseData;
}
/**
* Returns the first found item by the specified filter.
*
* Internally it calls `getList(1, 1, { filter, skipTotal })` and
* returns the first found item.
*
* You can use the generic T to supply a wrapper type of the crud model.
*
* For consistency with `getOne`, this method will throw a 404
* ClientResponseError if no item was found.
*
* @throws {ClientResponseError}
*/
getFirstListItem(filter, options) {
options = Object.assign({
requestKey: "one_by_filter_" + this.baseCrudPath + "_" + filter,
}, options);
options.query = Object.assign({
filter: filter,
skipTotal: 1,
}, options.query);
const result = this.getList(1, 1, options);
if (!result?.items?.length) {
throw new ClientResponseError({
status: 404,
response: {
code: 404,
message: "The requested resource wasn't found.",
data: {},
},
});
}
return result.items[0];
}
/**
* Returns single item by its id.
*
* You can use the generic T to supply a wrapper type of the crud model.
*
* If `id` is empty it will throw a 404 error.
*
* @throws {ClientResponseError}
*/
getOne(id, options) {
if (!id) {
throw new ClientResponseError({
url: this.client.buildURL(this.baseCrudPath + "/"),
status: 404,
response: {
code: 404,
message: "Missing required record id.",
data: {},
},
});
}
options = Object.assign({
method: "GET",
}, options);
const responseData = this.client.send(this.baseCrudPath + "/" + encodeURIComponent(id), options);
return this.decode(responseData);
}
/**
* Creates a new item.
*
* You can use the generic T to supply a wrapper type of the crud model.
*
* @throws {ClientResponseError}
*/
create(bodyParams, options) {
options = Object.assign({
method: "POST",
body: bodyParams,
}, options);
const responseData = this.client.send(this.baseCrudPath, options);
return this.decode(responseData);
}
/**
* Updates an existing item by its id.
*
* You can use the generic T to supply a wrapper type of the crud model.
*
* @throws {ClientResponseError}
*/
update(id, bodyParams, options) {
options = Object.assign({
method: "PATCH",
body: bodyParams,
}, options);
const responseData = this.client.send(this.baseCrudPath + "/" + encodeURIComponent(id), options);
return this.decode(responseData);
}
/**
* Deletes an existing item by its id.
*
* @throws {ClientResponseError}
*/
delete(id, options) {
options = Object.assign({
method: "DELETE",
}, options);
const responseData = this.client.send(this.baseCrudPath + "/" + encodeURIComponent(id), options);
return responseData;
}
/**
* Returns a promise with all list items batch fetched at once.
*/
_getFullList(batchSize = 500, options) {
options = options || {};
options.query = Object.assign({
skipTotal: 1,
}, options.query);
let result = [];
let request = (page) => {
const list = this.getList(page, batchSize || 500, options);
const castedList = list;
const items = castedList.items;
result = result.concat(items);
if (items.length == list.perPage) {
return request(page + 1);
}
return result;
};
return request(1);
}
}
function normalizeLegacyOptionsArgs(legacyWarn, baseOptions, bodyOrOptions, query) {
const hasBodyOrOptions = typeof bodyOrOptions !== "undefined";
const hasQuery = typeof query !== "undefined";
if (!hasQuery && !hasBodyOrOptions) {
return baseOptions;
}
if (hasQuery) {
console.warn(legacyWarn);
baseOptions.body = Object.assign({}, baseOptions.body, bodyOrOptions);
baseOptions.query = Object.assign({}, baseOptions.query, query);
return baseOptions;
}
return Object.assign(baseOptions, bodyOrOptions);
}
class RecordService extends CrudService {
constructor(client, collectionIdOrName) {
super(client);
this.collectionIdOrName = collectionIdOrName;
}
/**
* @inheritdoc
*/
get baseCrudPath() {
return this.baseCollectionPath + "/records";
}
/**
* Returns the current collection service base path.
*/
get baseCollectionPath() {
return "/api/collections/" + encodeURIComponent(this.collectionIdOrName);
}
/**
* Returns whether the current service collection is superusers.
*/
get isSuperusers() {
return (this.collectionIdOrName == "_superusers" ||
this.collectionIdOrName == "_pbc_2773867675");
}
/**
* @inheritdoc
*/
getFullList(batchOrOptions, options) {
if (typeof batchOrOptions == "number") {
return super.getFullList(batchOrOptions, options);
}
const params = Object.assign({}, batchOrOptions, options);
return super.getFullList(params);
}
/**
* @inheritdoc
*/
getList(page = 1, perPage = 30, options) {
return super.getList(page, perPage, options);
}
/**
* @inheritdoc
*/
getFirstListItem(filter, options) {
return super.getFirstListItem(filter, options);
}
/**
* @inheritdoc
*/
getOne(id, options) {
return super.getOne(id, options);
}
/**
* @inheritdoc
*/
create(bodyParams, options) {
return super.create(bodyParams, options);
}
/**
* @inheritdoc
*
* If the current `client.authStore.record` matches with the updated id, then
* on success the `client.authStore.record` will be updated with the new response record fields.
*/
update(id, bodyParams, options) {
const item = super.update(id, bodyParams, options);
if (
// is record auth
this.client.authStore.record?.id === item?.id &&
(this.client.authStore.record?.collectionId === this.collectionIdOrName ||
this.client.authStore.record?.collectionName === this.collectionIdOrName)) {
let authExpand = Object.assign({}, this.client.authStore.record.expand);
let authRecord = Object.assign({}, this.client.authStore.record, item);
if (authExpand) {
// for now "merge" only top-level expand
authRecord.expand = Object.assign(authExpand, item.expand);
}
this.client.authStore.save(this.client.authStore.token, authRecord);
}
return item;
}
/**
* @inheritdoc
*
* If the current `client.authStore.record` matches with the deleted id,
* then on success the `client.authStore` will be cleared.
*/
delete(id, options) {
const success = super.delete(id, options);
if (success &&
// is record auth
this.client.authStore.record?.id === id &&
(this.client.authStore.record?.collectionId === this.collectionIdOrName ||
this.client.authStore.record?.collectionName === this.collectionIdOrName)) {
this.client.authStore.clear();
}
return success;
}
// ---------------------------------------------------------------
// Auth handlers
// ---------------------------------------------------------------
/**
* Prepare successful collection authorization response.
*/
authResponse(responseData) {
const record = this.decode(responseData?.record || {});
this.client.authStore.save(responseData?.token, record);
return Object.assign({}, responseData, {
// normalize common fields
token: responseData?.token || "",
record: record,
});
}
/**
* Returns all available collection auth methods.
*
* @throws {ClientResponseError}
*/
listAuthMethods(options) {
options = Object.assign({
method: "GET",
// @todo remove after deleting the pre v0.23 API response fields
fields: "mfa,otp,password,oauth2",
}, options);
return this.client.send(this.baseCollectionPath + "/auth-methods", options);
}
/**
* Authenticate a single auth collection record via its username/email and password.
*
* On success, this method also automatically updates
* the client's AuthStore data and returns:
* - the authentication token
* - the authenticated record model
*
* @throws {ClientResponseError}
*/
authWithPassword(usernameOrEmail, password, options) {
options = Object.assign({
method: "POST",
body: {
identity: usernameOrEmail,
password: password,
},
}, options);
let authData = this.client.send(this.baseCollectionPath + "/auth-with-password", options);
authData = this.authResponse(authData);
return authData;
}
authWithOAuth2Code(provider, code, codeVerifier, redirectURL, createData, bodyOrOptions, query) {
let options = {
method: "POST",
body: {
provider: provider,
code: code,
codeVerifier: codeVerifier,
redirectURL: redirectURL,
createData: createData,
},
};
options = normalizeLegacyOptionsArgs("This form of authWithOAuth2Code(provider, code, codeVerifier, redirectURL, createData?, body?, query?) is deprecated. Consider replacing it with authWithOAuth2Code(provider, code, codeVerifier, redirectURL, createData?, options?).", options, bodyOrOptions, query);
const data = this.client.send(this.baseCollectionPath + "/auth-with-oauth2", options);
return this.authResponse(data);
}
authRefresh(bodyOrOptions, query) {
let options = {
method: "POST",
};
options = normalizeLegacyOptionsArgs("This form of authRefresh(body?, query?) is deprecated. Consider replacing it with authRefresh(options?).", options, bodyOrOptions, query);
const data = this.client.send(this.baseCollectionPath + "/auth-refresh", options);
return this.authResponse(data);
}
requestPasswordReset(email, bodyOrOptions, query) {
let options = {
method: "POST",
body: {
email: email,
},
};
options = normalizeLegacyOptionsArgs("This form of requestPasswordReset(email, body?, query?) is deprecated. Consider replacing it with requestPasswordReset(email, options?).", options, bodyOrOptions, query);
this.client.send(this.baseCollectionPath + "/request-password-reset", options);
return true;
}
confirmPasswordReset(passwordResetToken, password, passwordConfirm, bodyOrOptions, query) {
let options = {
method: "POST",
body: {
token: passwordResetToken,
password: password,
passwordConfirm: passwordConfirm,
},
};
options = normalizeLegacyOptionsArgs("This form of confirmPasswordReset(token, password, passwordConfirm, body?, query?) is deprecated. Consider replacing it with confirmPasswordReset(token, password, passwordConfirm, options?).", options, bodyOrOptions, query);
this.client.send(this.baseCollectionPath + "/confirm-password-reset", options);
return true;
}
requestVerification(email, bodyOrOptions, query) {
let options = {
method: "POST",
body: {
email: email,
},
};
options = normalizeLegacyOptionsArgs("This form of requestVerification(email, body?, query?) is deprecated. Consider replacing it with requestVerification(email, options?).", options, bodyOrOptions, query);
this.client.send(this.baseCollectionPath + "/request-verification", options);
return true;
}
confirmVerification(verificationToken, bodyOrOptions, query) {
let options = {
method: "POST",
body: {
token: verificationToken,
},
};
options = normalizeLegacyOptionsArgs("This form of confirmVerification(token, body?, query?) is deprecated. Consider replacing it with confirmVerification(token, options?).", options, bodyOrOptions, query);
this.client.send(this.baseCollectionPath + "/confirm-verification", options);
// on success manually update the current auth record verified state
const payload = getTokenPayload(verificationToken);
const model = this.client.authStore.record;
if (model &&
!model.verified &&
model.id === payload.id &&
model.collectionId === payload.collectionId) {
model.verified = true;
this.client.authStore.save(this.client.authStore.token, model);
}
return true;
}
requestEmailChange(newEmail, bodyOrOptions, query) {
let options = {
method: "POST",
body: {
newEmail: newEmail,
},
};
options = normalizeLegacyOptionsArgs("This form of requestEmailChange(newEmail, body?, query?) is deprecated. Consider replacing it with requestEmailChange(newEmail, options?).", options, bodyOrOptions, query);
this.client.send(this.baseCollectionPath + "/request-email-change", options);
return true;
}
confirmEmailChange(emailChangeToken, password, bodyOrOptions, query) {
let options = {
method: "POST",
body: {
token: emailChangeToken,
password: password,
},
};
options = normalizeLegacyOptionsArgs("This form of confirmEmailChange(token, password, body?, query?) is deprecated. Consider replacing it with confirmEmailChange(token, password, options?).", options, bodyOrOptions, query);
this.client.send(this.baseCollectionPath + "/confirm-email-change", options);
// on success manually update the current auth record verified state
const payload = getTokenPayload(emailChangeToken);
const model = this.client.authStore.record;
if (model &&
model.id === payload.id &&
model.collectionId === payload.collectionId) {
this.client.authStore.clear();
}
return true;
}
/**
* Sends auth record OTP to the provided email.
*
* @throws {ClientResponseError}
*/
requestOTP(email, options) {
options = Object.assign({
method: "POST",
body: { email: email },
}, options);
return this.client.send(this.baseCollectionPath + "/request-otp", options);
}
/**
* Authenticate a single auth collection record via OTP.
*
* On success, this method also automatically updates
* the client's AuthStore data and returns:
* - the authentication token
* - the authenticated record model
*
* @throws {ClientResponseError}
*/
authWithOTP(otpId, password, options) {
options = Object.assign({
method: "POST",
body: { otpId, password },
}, options);
const data = this.client.send(this.baseCollectionPath + "/auth-with-otp", options);
return this.authResponse(data);
}
/**
* Impersonate authenticates with the specified recordId and
* returns a new client with the received auth token in a memory store.
*
* If `duration` is 0 the generated auth token will fallback
* to the default collection auth token duration.
*
* This action currently requires superusers privileges.
*
* @throws {ClientResponseError}
*/
impersonate(recordId, duration, options) {
options = Object.assign({
method: "POST",
body: { duration: duration },
}, options);
options.headers = options.headers || {};
if (!options.headers.Authorization) {
options.headers.Authorization = this.client.authStore.token;
}
// create a new client loaded with the impersonated auth state
// ---
const client = new Client(this.client.baseURL, new BaseAuthStore(), this.client.lang);
const authData = client.send(this.baseCollectionPath + "/impersonate/" + encodeURIComponent(recordId), options);
client.authStore.save(authData?.token, this.decode(authData?.record || {}));
// ---
return client;
}
}
class CollectionService extends CrudService {
/**
* @inheritdoc
*/
get baseCrudPath() {
return "/api/collections";
}
/**
* Imports the provided collections.
*
* If `deleteMissing` is `true`, all local collections and their fields,
* that are not present in the imported configuration, WILL BE DELETED
* (including their related records data)!
*
* @throws {ClientResponseError}
*/
import(collections, deleteMissing = false, options) {
options = Object.assign({
method: "PUT",
body: {
collections: collections,
deleteMissing: deleteMissing,
},
}, options);
this.client.send(this.baseCrudPath + "/import", options);
return true;
}
/**
* Returns type indexed map with scaffolded collection models
* populated with their default field values.
*
* @throws {ClientResponseError}
*/
getScaffolds(options) {
options = Object.assign({
method: "GET",
}, options);
return this.client.send(this.baseCrudPath + "/meta/scaffolds", options);
}
/**
* Deletes all records associated with the specified collection.
*
* @throws {ClientResponseError}
*/
truncate(collectionIdOrName, options) {
options = Object.assign({
method: "DELETE",
}, options);
this.client.send(this.baseCrudPath +
"/" +
encodeURIComponent(collectionIdOrName) +
"/truncate", options);
return true;
}
}
class LogService extends BaseService {
/**
* Returns paginated logs list.
*
* @throws {ClientResponseError}
*/
getList(page = 1, perPage = 30, options) {
options = Object.assign({ method: "GET" }, options);
options.query = Object.assign({
page: page,
perPage: perPage,
}, options.query);
return this.client.send("/api/logs", options);
}
/**
* Returns a single log by its id.
*
* If `id` is empty it will throw a 404 error.
*
* @throws {ClientResponseError}
*/
getOne(id, options) {
if (!id) {
throw new ClientResponseError({
url: this.client.buildURL("/api/logs/"),
status: 404,
response: {
code: 404,
message: "Missing required log id.",
data: {},
},
});
}
options = Object.assign({
method: "GET",
}, options);
return this.client.send("/api/logs/" + encodeURIComponent(id), options);
}
/**
* Returns logs statistics.
*
* @throws {ClientResponseError}
*/
getStats(options) {
options = Object.assign({
method: "GET",
}, options);
return this.client.send("/api/logs/stats", options);
}
}
class HealthService extends BaseService {
/**
* Checks the health status of the api.
*
* @throws {ClientResponseError}
*/
check(options) {
options = Object.assign({
method: "GET",
}, options);
return this.client.send("/api/health", options);
}
}
class FileService extends BaseService {
/**
* @deprecated Please replace with `pb.files.getURL()`.
*/
getUrl(record, filename, queryParams = {}) {
console.warn("Please replace pb.files.getUrl() with pb.files.getURL()");
return this.getURL(record, filename, queryParams);
}
/**
* Builds and returns an absolute record file url for the provided filename.
*/
getURL(record, filename, queryParams = {}) {
if (!filename ||
!record?.id ||
!(record?.collectionId || record?.collectionName)) {
return "";
}
const parts = [];
parts.push("api");
parts.push("files");
parts.push(encodeURIComponent(record.collectionId || record.collectionName));
parts.push(encodeURIComponent(record.id));
parts.push(encodeURIComponent(filename));
let result = this.client.buildURL(parts.join("/"));
if (Object.keys(queryParams).length) {
// normalize the download query param for consistency with the Dart sdk
if (queryParams.download === false) {
delete queryParams.download;
}
const params = new URLSearchParams(queryParams);
result += (result.includes("?") ? "&" : "?") + params;
}
return result;
}
/**
* Requests a new private file access token for the current auth model.
*
* @throws {ClientResponseError}
*/
getToken(options) {
options = Object.assign({
method: "POST",
}, options);
const data = this.client.send("/api/files/token", options);
return data?.token || "";
}
}
class BackupService extends BaseService {
/**
* Returns list with all available backup files.
*
* @throws {ClientResponseError}
*/
getFullList(options) {
options = Object.assign({
method: "GET",
}, options);
return this.client.send("/api/backups", options);
}
/**
* Initializes a new backup.
*
* @throws {ClientResponseError}
*/
create(basename, options) {
options = Object.assign({
method: "POST",
body: {
name: basename,
},
}, options);
this.client.send("/api/backups", options);
return true;
}
/**
* Uploads an existing backup file.
*
* Example:
*
* ```js
* await pb.backups.upload({
* file: new Blob([...]),
* });
* ```
*
* @throws {ClientResponseError}
*/
upload(bodyParams, options) {
options = Object.assign({
method: "POST",
body: bodyParams,
}, options);
this.client.send("/api/backups/upload", options);
return true;
}
/**
* Deletes a single backup file.
*
* @throws {ClientResponseError}
*/
delete(key, options) {
options = Object.assign({
method: "DELETE",
}, options);
this.client.send(`/api/backups/${encodeURIComponent(key)}`, options);
return true;
}
/**
* Initializes an app data restore from an existing backup.
*
* @throws {ClientResponseError}
*/
restore(key, options) {
options = Object.assign({
method: "POST",
}, options);
this.client.send(`/api/backups/${encodeURIComponent(key)}/restore`, options);
return true;
}
/**
* Builds a download url for a single existing backup using a
* superuser file token and the backup file key.
*
* The file token can be generated via `pb.files.getToken()`.
*/
getDownloadURL(token, key) {
return this.client.buildURL(`/api/backups/${encodeURIComponent(key)}?token=${encodeURIComponent(token)}`);
}
}
class CronService extends BaseService {
/**
* Returns list with all registered cron jobs.
*
* @throws {ClientResponseError}
*/
getFullList(options) {
options = Object.assign({
method: "GET",
}, options);
return this.client.send("/api/crons", options);
}
/**
* Runs the specified cron job.
*
* @throws {ClientResponseError}
*/
run(jobId, options) {
options = Object.assign({
method: "POST",
}, options);
this.client.send(`/api/crons/${encodeURIComponent(jobId)}`, options);
return true;
}
}
/**
* Checks if the specified value is a file (aka. File, Blob, RN file object).
*/
function isFile(val) {
return ((typeof Blob !== "undefined" && val instanceof Blob) ||
(typeof File !== "undefined" && val instanceof File));
}
/**
* Loosely checks if the specified body is a FormData instance.
*/
function isFormData(body) {
return (body &&
// we are checking the constructor name because FormData
// is not available natively in some environments and the
// po