pocketbase
Version:
PocketBase JavaScript SDK
1 lines • 165 kB
Source Map (JSON)
{"version":3,"file":"pocketbase.es.mjs","sources":["../src/ClientResponseError.ts","../src/stores/utils/cookie.ts","../src/stores/utils/jwt.ts","../src/stores/BaseAuthStore.ts","../src/stores/LocalAuthStore.ts","../src/services/utils/BaseService.ts","../src/services/SettingsService.ts","../src/services/utils/CrudService.ts","../src/services/utils/legacy.ts","../src/services/utils/refresh.ts","../src/services/AdminService.ts","../src/services/utils/options.ts","../src/services/RealtimeService.ts","../src/services/RecordService.ts","../src/services/CollectionService.ts","../src/services/LogService.ts","../src/services/HealthService.ts","../src/services/FileService.ts","../src/services/BackupService.ts","../src/Client.ts","../src/stores/AsyncAuthStore.ts"],"sourcesContent":["/**\n * ClientResponseError is a custom Error class that is intended to wrap\n * and normalize any error thrown by `Client.send()`.\n */\nexport class ClientResponseError extends Error {\n url: string = '';\n status: number = 0;\n response: {[key: string]: any} = {};\n isAbort: boolean = false;\n originalError: any = null;\n\n constructor(errData?: any) {\n super(\"ClientResponseError\");\n\n // Set the prototype explicitly.\n // https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work\n Object.setPrototypeOf(this, ClientResponseError.prototype);\n\n if (errData !== null && typeof errData === 'object') {\n this.url = typeof errData.url === 'string' ? errData.url : '';\n this.status = typeof errData.status === 'number' ? errData.status : 0;\n this.isAbort = !!errData.isAbort;\n this.originalError = errData.originalError;\n\n if (errData.response !== null && typeof errData.response === 'object') {\n this.response = errData.response;\n } else if (errData.data !== null && typeof errData.data === 'object') {\n this.response = errData.data;\n } else {\n this.response = {};\n }\n }\n\n if (!this.originalError && !(errData instanceof ClientResponseError)) {\n this.originalError = errData;\n }\n\n if (typeof DOMException !== 'undefined' && errData instanceof DOMException) {\n this.isAbort = true;\n }\n\n this.name = \"ClientResponseError \" + this.status;\n this.message = this.response?.message;\n if (!this.message) {\n if (this.isAbort) {\n this.message = 'The request was autocancelled. You can find more info in https://github.com/pocketbase/js-sdk#auto-cancellation.';\n } else if (this.originalError?.cause?.message?.includes(\"ECONNREFUSED ::1\")) {\n 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).';\n } else {\n this.message = 'Something went wrong while processing your request.';\n }\n }\n }\n\n /**\n * Alias for `this.response` to preserve the backward compatibility.\n */\n get data() {\n return this.response;\n }\n\n /**\n * Make a POJO's copy of the current error class instance.\n * @see https://github.com/vuex-orm/vuex-orm/issues/255\n */\n toJSON() {\n return { ...this };\n }\n}\n","/**\n * -------------------------------------------------------------------\n * Simple cookie parse and serialize utilities mostly based on the\n * node module https://github.com/jshttp/cookie.\n * -------------------------------------------------------------------\n */\n\n/**\n * RegExp to match field-content in RFC 7230 sec 3.2\n *\n * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]\n * field-vchar = VCHAR / obs-text\n * obs-text = %x80-FF\n */\nconst fieldContentRegExp = /^[\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+$/;\n\nexport interface ParseOptions{\n decode?: (val: string) => string,\n}\n\n/**\n* Parses the given cookie header string into an object\n* The object has the various cookies as keys(names) => values\n*/\nexport function cookieParse(str: string, options?: ParseOptions): { [key: string]: any } {\n const result: { [key: string]: any } = {};\n\n if (typeof str !== 'string') {\n return result;\n }\n\n const opt = Object.assign({}, options || {});\n const decode = opt.decode || defaultDecode;\n\n let index = 0;\n while (index < str.length) {\n const eqIdx = str.indexOf('=', index);\n\n // no more cookie pairs\n if (eqIdx === -1) {\n break;\n }\n\n let endIdx = str.indexOf(';', index);\n\n if (endIdx === -1) {\n endIdx = str.length;\n } else if (endIdx < eqIdx) {\n // backtrack on prior semicolon\n index = str.lastIndexOf(';', eqIdx - 1) + 1;\n continue;\n }\n\n const key = str.slice(index, eqIdx).trim();\n\n // only assign once\n if (undefined === result[key]) {\n let val = str.slice(eqIdx + 1, endIdx).trim();\n\n // quoted values\n if (val.charCodeAt(0) === 0x22) {\n val = val.slice(1, -1);\n }\n\n try {\n result[key] = decode(val);\n } catch (_) {\n result[key] = val; // no decoding\n }\n }\n\n index = endIdx + 1;\n }\n\n return result;\n};\n\nexport interface SerializeOptions {\n encode?: (val: string | number | boolean) => string,\n maxAge?: number,\n domain?: string,\n path?: string,\n expires?: Date,\n httpOnly?: boolean,\n secure?: boolean,\n priority?: string,\n sameSite?: boolean|string,\n}\n\n/**\n * Serialize data into a cookie header.\n *\n * Serialize the a name value pair into a cookie string suitable for\n * http headers. An optional options object specified cookie parameters.\n *\n * ```js\n * cookieSerialize('foo', 'bar', { httpOnly: true }) // \"foo=bar; httpOnly\"\n * ```\n */\nexport function cookieSerialize(name: string, val: string, options?: SerializeOptions): string {\n const opt = Object.assign({}, options || {});\n const encode = opt.encode || defaultEncode;\n\n if (!fieldContentRegExp.test(name)) {\n throw new TypeError('argument name is invalid');\n }\n\n const value = encode(val);\n\n if (value && !fieldContentRegExp.test(value)) {\n throw new TypeError('argument val is invalid');\n }\n\n let result = name + '=' + value;\n\n if (opt.maxAge != null) {\n const maxAge = opt.maxAge - 0;\n\n if (isNaN(maxAge) || !isFinite(maxAge)) {\n throw new TypeError('option maxAge is invalid');\n }\n\n result += '; Max-Age=' + Math.floor(maxAge);\n }\n\n if (opt.domain) {\n if (!fieldContentRegExp.test(opt.domain)) {\n throw new TypeError('option domain is invalid');\n }\n\n result += '; Domain=' + opt.domain;\n }\n\n if (opt.path) {\n if (!fieldContentRegExp.test(opt.path)) {\n throw new TypeError('option path is invalid');\n }\n\n result += '; Path=' + opt.path;\n }\n\n if (opt.expires) {\n if (!isDate(opt.expires) || isNaN(opt.expires.valueOf())) {\n throw new TypeError('option expires is invalid');\n }\n\n result += '; Expires=' + opt.expires.toUTCString();\n }\n\n if (opt.httpOnly) {\n result += '; HttpOnly';\n }\n\n if (opt.secure) {\n result += '; Secure';\n }\n\n if (opt.priority) {\n const priority = typeof opt.priority === 'string' ? opt.priority.toLowerCase() : opt.priority;\n\n switch (priority) {\n case 'low':\n result += '; Priority=Low';\n break;\n case 'medium':\n result += '; Priority=Medium';\n break;\n case 'high':\n result += '; Priority=High';\n break;\n default:\n throw new TypeError('option priority is invalid');\n }\n }\n\n if (opt.sameSite) {\n const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite;\n\n switch (sameSite) {\n case true:\n result += '; SameSite=Strict';\n break;\n case 'lax':\n result += '; SameSite=Lax';\n break;\n case 'strict':\n result += '; SameSite=Strict';\n break;\n case 'none':\n result += '; SameSite=None';\n break;\n default:\n throw new TypeError('option sameSite is invalid');\n }\n }\n\n return result;\n};\n\n/**\n * Default URL-decode string value function.\n * Optimized to skip native call when no `%`.\n */\nfunction defaultDecode(val: string): string {\n return val.indexOf('%') !== -1\n ? decodeURIComponent(val)\n : val;\n}\n\n/**\n * Default URL-encode value function.\n */\nfunction defaultEncode(val: string | number | boolean): string {\n return encodeURIComponent(val);\n}\n\n/**\n * Determines if value is a Date.\n */\nfunction isDate(val: any): boolean {\n return (\n Object.prototype.toString.call(val) === '[object Date]' ||\n val instanceof Date\n );\n}\n","let atobPolyfill: Function;\nif (typeof atob === 'function') {\n atobPolyfill = atob\n} else {\n /**\n * The code was extracted from:\n * https://github.com/davidchambers/Base64.js\n */\n atobPolyfill = (input: any) => {\n const chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\";\n\n let str = String(input).replace(/=+$/, \"\");\n if (str.length % 4 == 1) {\n throw new Error(\"'atob' failed: The string to be decoded is not correctly encoded.\");\n }\n\n for (\n // initialize result and counters\n var bc = 0, bs, buffer, idx = 0, output = \"\";\n // get next character\n (buffer = str.charAt(idx++));\n // character found in table? initialize bit storage and add its ascii value;\n ~buffer &&\n ((bs = bc % 4 ? (bs as any) * 64 + buffer : buffer),\n // and if not first of each 4 characters,\n // convert the first 8 bits to one ascii character\n bc++ % 4) ?\n (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) :\n 0\n ) {\n // try to find character in table (0-63, not found => -1)\n buffer = chars.indexOf(buffer);\n }\n\n return output;\n };\n}\n\n/**\n * Returns JWT token's payload data.\n */\nexport function getTokenPayload(token: string): { [key: string]: any } {\n if (token) {\n try {\n const encodedPayload = decodeURIComponent(atobPolyfill(token.split('.')[1]).split('').map(function (c: string) {\n return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);\n }).join(''));\n\n return JSON.parse(encodedPayload) || {};\n } catch (e) {\n }\n }\n\n return {};\n}\n\n/**\n * Checks whether a JWT token is expired or not.\n * Tokens without `exp` payload key are considered valid.\n * Tokens with empty payload (eg. invalid token strings) are considered expired.\n *\n * @param token The token to check.\n * @param [expirationThreshold] Time in seconds that will be subtracted from the token `exp` property.\n */\nexport function isTokenExpired(token: string, expirationThreshold = 0): boolean {\n let payload = getTokenPayload(token);\n\n if (\n Object.keys(payload).length > 0 &&\n (!payload.exp || (payload.exp - expirationThreshold) > (Date.now() / 1000))\n ) {\n return false;\n }\n\n return true;\n}\n","import { cookieParse, cookieSerialize, SerializeOptions } from '@/stores/utils/cookie';\nimport { isTokenExpired, getTokenPayload } from '@/stores/utils/jwt';\n\nexport type AuthModel = { [key: string]: any }|null;\n\nexport type OnStoreChangeFunc = (token: string, model: AuthModel) => void;\n\nconst defaultCookieKey = 'pb_auth';\n\n/**\n * Base AuthStore class that is intended to be extended by all other\n * PocketBase AuthStore implementations.\n */\nexport abstract class BaseAuthStore {\n protected baseToken: string = '';\n protected baseModel: AuthModel = null;\n\n private _onChangeCallbacks: Array<OnStoreChangeFunc> = [];\n\n /**\n * Retrieves the stored token (if any).\n */\n get token(): string {\n return this.baseToken;\n }\n\n /**\n * Retrieves the stored model data (if any).\n */\n get model(): AuthModel {\n return this.baseModel;\n }\n\n /**\n * Loosely checks if the store has valid token (aka. existing and unexpired exp claim).\n */\n get isValid(): boolean {\n return !isTokenExpired(this.token);\n }\n\n /**\n * Checks whether the current store state is for admin authentication.\n */\n get isAdmin(): boolean {\n return getTokenPayload(this.token).type === \"admin\";\n }\n\n /**\n * Checks whether the current store state is for auth record authentication.\n */\n get isAuthRecord(): boolean {\n return getTokenPayload(this.token).type === \"authRecord\";\n }\n\n /**\n * Saves the provided new token and model data in the auth store.\n */\n save(token: string, model?: AuthModel): void {\n this.baseToken = token || '';\n this.baseModel = model || null;\n\n this.triggerChange();\n }\n\n /**\n * Removes the stored token and model data form the auth store.\n */\n clear(): void {\n this.baseToken = '';\n this.baseModel = null;\n this.triggerChange();\n }\n\n /**\n * Parses the provided cookie string and updates the store state\n * with the cookie's token and model data.\n *\n * NB! This function doesn't validate the token or its data.\n * Usually this isn't a concern if you are interacting only with the\n * PocketBase API because it has the proper server-side security checks in place,\n * but if you are using the store `isValid` state for permission controls\n * in a node server (eg. SSR), then it is recommended to call `authRefresh()`\n * after loading the cookie to ensure an up-to-date token and model state.\n * For example:\n *\n * ```js\n * pb.authStore.loadFromCookie(\"cookie string...\");\n *\n * try {\n * // get an up-to-date auth store state by veryfing and refreshing the loaded auth model (if any)\n * pb.authStore.isValid && await pb.collection('users').authRefresh();\n * } catch (_) {\n * // clear the auth store on failed refresh\n * pb.authStore.clear();\n * }\n * ```\n */\n loadFromCookie(cookie: string, key = defaultCookieKey): void {\n const rawData = cookieParse(cookie || '')[key] || '';\n\n let data: { [key: string]: any } = {};\n try {\n data = JSON.parse(rawData);\n // normalize\n if (typeof data === null || typeof data !== 'object' || Array.isArray(data)) {\n data = {};\n }\n } catch (_) {}\n\n this.save(data.token || '', data.model || null);\n }\n\n /**\n * Exports the current store state as cookie string.\n *\n * By default the following optional attributes are added:\n * - Secure\n * - HttpOnly\n * - SameSite=Strict\n * - Path=/\n * - Expires={the token expiration date}\n *\n * NB! If the generated cookie exceeds 4096 bytes, this method will\n * strip the model data to the bare minimum to try to fit within the\n * recommended size in https://www.rfc-editor.org/rfc/rfc6265#section-6.1.\n */\n exportToCookie(options?: SerializeOptions, key = defaultCookieKey): string {\n const defaultOptions: SerializeOptions = {\n secure: true,\n sameSite: true,\n httpOnly: true,\n path: \"/\",\n };\n\n // extract the token expiration date\n const payload = getTokenPayload(this.token);\n if (payload?.exp) {\n defaultOptions.expires = new Date(payload.exp * 1000);\n } else {\n defaultOptions.expires = new Date('1970-01-01');\n }\n\n // merge with the user defined options\n options = Object.assign({}, defaultOptions, options);\n\n const rawData = {\n token: this.token,\n model: this.model ? JSON.parse(JSON.stringify(this.model)) : null,\n };\n\n let result = cookieSerialize(key, JSON.stringify(rawData), options);\n\n const resultLength = typeof Blob !== 'undefined' ?\n (new Blob([result])).size : result.length;\n\n // strip down the model data to the bare minimum\n if (rawData.model && resultLength > 4096) {\n rawData.model = {id: rawData?.model?.id, email: rawData?.model?.email};\n const extraProps = [\"collectionId\", \"username\", \"verified\"];\n for (const prop in this.model) {\n if (extraProps.includes(prop)) {\n rawData.model[prop] = this.model[prop];\n }\n }\n result = cookieSerialize(key, JSON.stringify(rawData), options);\n }\n\n return result;\n }\n\n /**\n * Register a callback function that will be called on store change.\n *\n * You can set the `fireImmediately` argument to true in order to invoke\n * the provided callback right after registration.\n *\n * Returns a removal function that you could call to \"unsubscribe\" from the changes.\n */\n onChange(callback: OnStoreChangeFunc, fireImmediately = false): () => void {\n this._onChangeCallbacks.push(callback);\n\n if (fireImmediately) {\n callback(this.token, this.model);\n }\n\n return () => {\n for (let i = this._onChangeCallbacks.length - 1; i >= 0; i--) {\n if (this._onChangeCallbacks[i] == callback) {\n delete this._onChangeCallbacks[i]; // removes the function reference\n this._onChangeCallbacks.splice(i, 1); // reindex the array\n return;\n }\n }\n }\n }\n\n protected triggerChange(): void {\n for (const callback of this._onChangeCallbacks) {\n callback && callback(this.token, this.model);\n }\n }\n}\n","import { BaseAuthStore, AuthModel } from '@/stores/BaseAuthStore';\n\n/**\n * The default token store for browsers with auto fallback\n * to runtime/memory if local storage is undefined (eg. in node env).\n */\nexport class LocalAuthStore extends BaseAuthStore {\n private storageFallback: { [key: string]: any } = {};\n private storageKey: string\n\n constructor(storageKey = \"pocketbase_auth\") {\n super();\n\n this.storageKey = storageKey;\n\n this._bindStorageEvent();\n }\n\n /**\n * @inheritdoc\n */\n get token(): string {\n const data = this._storageGet(this.storageKey) || {};\n\n return data.token || '';\n }\n\n /**\n * @inheritdoc\n */\n get model(): AuthModel {\n const data = this._storageGet(this.storageKey) || {};\n\n return data.model || null;\n }\n\n /**\n * @inheritdoc\n */\n save(token: string, model?: AuthModel) {\n this._storageSet(this.storageKey, {\n 'token': token,\n 'model': model,\n });\n\n super.save(token, model);\n }\n\n /**\n * @inheritdoc\n */\n clear() {\n this._storageRemove(this.storageKey);\n\n super.clear();\n }\n\n // ---------------------------------------------------------------\n // Internal helpers:\n // ---------------------------------------------------------------\n\n /**\n * Retrieves `key` from the browser's local storage\n * (or runtime/memory if local storage is undefined).\n */\n private _storageGet(key: string): any {\n if (typeof window !== 'undefined' && window?.localStorage) {\n const rawValue = window.localStorage.getItem(key) || '';\n try {\n return JSON.parse(rawValue);\n } catch (e) { // not a json\n return rawValue;\n }\n }\n\n // fallback\n return this.storageFallback[key];\n }\n\n /**\n * Stores a new data in the browser's local storage\n * (or runtime/memory if local storage is undefined).\n */\n private _storageSet(key: string, value: any) {\n if (typeof window !== 'undefined' && window?.localStorage) {\n // store in local storage\n let normalizedVal = value;\n if (typeof value !== 'string') {\n normalizedVal = JSON.stringify(value);\n }\n window.localStorage.setItem(key, normalizedVal);\n } else {\n // store in fallback\n this.storageFallback[key] = value;\n }\n }\n\n /**\n * Removes `key` from the browser's local storage and the runtime/memory.\n */\n private _storageRemove(key: string) {\n // delete from local storage\n if (typeof window !== 'undefined' && window?.localStorage) {\n window.localStorage?.removeItem(key);\n }\n\n // delete from fallback\n delete this.storageFallback[key];\n }\n\n /**\n * Updates the current store state on localStorage change.\n */\n private _bindStorageEvent() {\n if (typeof window === 'undefined' || !window?.localStorage || !window.addEventListener) {\n return;\n }\n\n window.addEventListener('storage', (e) => {\n if (e.key != this.storageKey) {\n return;\n }\n\n const data = this._storageGet(this.storageKey) || {};\n\n super.save(data.token || '', data.model || null);\n });\n }\n}\n","import Client from '@/Client';\n\n/**\n * BaseService class that should be inherited from all API services.\n */\nexport abstract class BaseService {\n readonly client: Client\n\n constructor(client: Client) {\n this.client = client;\n }\n}\n","import { BaseService } from '@/services/utils/BaseService';\nimport { CommonOptions } from '@/services/utils/options';\n\ninterface appleClientSecret {\n secret: string;\n}\n\nexport class SettingsService extends BaseService {\n /**\n * Fetch all available app settings.\n */\n getAll(options?: CommonOptions): Promise<{[key: string]:any}> {\n options = Object.assign({\n 'method': 'GET',\n }, options);\n\n return this.client.send('/api/settings', options);\n }\n\n /**\n * Bulk updates app settings.\n */\n update(\n bodyParams?: {[key:string]:any}|FormData,\n options?: CommonOptions,\n ): Promise<{[key: string]:any}> {\n options = Object.assign({\n 'method': 'PATCH',\n 'body': bodyParams,\n }, options);\n\n return this.client.send('/api/settings', options);\n }\n\n /**\n * Performs a S3 filesystem connection test.\n *\n * The currently supported `filesystem` are \"storage\" and \"backups\".\n */\n testS3(filesystem: string = \"storage\", options?: CommonOptions): Promise<boolean> {\n options = Object.assign({\n 'method': 'POST',\n 'body': {\n 'filesystem': filesystem,\n },\n }, options);\n\n return this.client.send('/api/settings/test/s3', options)\n .then(() => true);\n }\n\n /**\n * Sends a test email.\n *\n * The possible `emailTemplate` values are:\n * - verification\n * - password-reset\n * - email-change\n */\n testEmail(toEmail: string, emailTemplate: string, options?: CommonOptions): Promise<boolean> {\n options = Object.assign({\n 'method': 'POST',\n 'body': {\n 'email': toEmail,\n 'template': emailTemplate,\n },\n }, options);\n\n return this.client.send('/api/settings/test/email', options)\n .then(() => true);\n }\n\n /**\n * Generates a new Apple OAuth2 client secret.\n */\n generateAppleClientSecret(\n clientId: string,\n teamId: string,\n keyId: string,\n privateKey: string,\n duration: number,\n options?: CommonOptions,\n ): Promise<appleClientSecret> {\n options = Object.assign({\n 'method': 'POST',\n 'body': {\n clientId,\n teamId,\n keyId,\n privateKey,\n duration,\n },\n }, options);\n\n return this.client.send('/api/settings/apple/generate-client-secret', options);\n }\n}\n","import { BaseService } from '@/services/utils/BaseService';\nimport { ClientResponseError } from '@/ClientResponseError';\nimport { ListResult } from '@/services/utils/dtos';\nimport {\n CommonOptions,\n ListOptions,\n FullListOptions\n} from '@/services/utils/options';\n\nexport abstract class CrudService<M> extends BaseService {\n /**\n * Base path for the crud actions (without trailing slash, eg. '/admins').\n */\n abstract get baseCrudPath(): string\n\n /**\n * Response data decoder.\n */\n decode<T = M>(data: { [key: string]: any }): T {\n return data as T;\n }\n\n /**\n * Returns a promise with all list items batch fetched at once\n * (by default 500 items per request; to change it set the `batch` query param).\n *\n * You can use the generic T to supply a wrapper type of the crud model.\n */\n getFullList<T = M>(options?: FullListOptions): Promise<Array<T>>\n\n /**\n * Legacy version of getFullList with explicitly specified batch size.\n */\n getFullList<T = M>(batch?: number, options?: ListOptions): Promise<Array<T>>\n\n getFullList<T = M>(batchOrqueryParams?: number|FullListOptions, options?: ListOptions): Promise<Array<T>> {\n if (typeof batchOrqueryParams == \"number\") {\n return this._getFullList<T>(batchOrqueryParams, options);\n }\n\n options = Object.assign({}, batchOrqueryParams, options);\n\n let batch = 500;\n if (options.batch) {\n batch = options.batch;\n delete options.batch;\n }\n\n return this._getFullList<T>(batch, options);\n }\n\n /**\n * Returns paginated items list.\n *\n * You can use the generic T to supply a wrapper type of the crud model.\n */\n getList<T = M>(page = 1, perPage = 30, options?: ListOptions): Promise<ListResult<T>> {\n options = Object.assign({\n method: 'GET'\n }, options);\n\n options.query = Object.assign({\n 'page': page,\n 'perPage': perPage,\n }, options.query);\n\n return this.client.send(this.baseCrudPath, options)\n .then((responseData: any) => {\n responseData.items = responseData.items?.map((item: any) => {\n return this.decode<T>(item);\n }) || [];\n\n return responseData;\n });\n }\n\n /**\n * Returns the first found item by the specified filter.\n *\n * Internally it calls `getList(1, 1, { filter, skipTotal })` and\n * returns the first found item.\n *\n * You can use the generic T to supply a wrapper type of the crud model.\n *\n * For consistency with `getOne`, this method will throw a 404\n * ClientResponseError if no item was found.\n */\n getFirstListItem<T = M>(filter: string, options?: CommonOptions): Promise<T> {\n options = Object.assign({\n 'requestKey': 'one_by_filter_' + this.baseCrudPath + \"_\" + filter,\n }, options);\n\n options.query = Object.assign({\n 'filter': filter,\n 'skipTotal': 1,\n }, options.query);\n\n return this.getList<T>(1, 1, options)\n .then((result) => {\n if (!result?.items?.length) {\n throw new ClientResponseError({\n status: 404,\n data: {\n code: 404,\n message: \"The requested resource wasn't found.\",\n data: {},\n },\n });\n }\n\n return result.items[0];\n });\n }\n\n /**\n * Returns single item by its id.\n *\n * You can use the generic T to supply a wrapper type of the crud model.\n */\n getOne<T = M>(id: string, options?: CommonOptions): Promise<T> {\n options = Object.assign({\n 'method': 'GET',\n }, options);\n\n return this.client.send(this.baseCrudPath + '/' + encodeURIComponent(id), options)\n .then((responseData: any) => this.decode<T>(responseData));\n }\n\n /**\n * Creates a new item.\n *\n * You can use the generic T to supply a wrapper type of the crud model.\n */\n create<T = M>(\n bodyParams?: {[key:string]:any}|FormData,\n options?: CommonOptions,\n ): Promise<T> {\n options = Object.assign({\n 'method': 'POST',\n 'body': bodyParams,\n }, options);\n\n return this.client.send(this.baseCrudPath, options)\n .then((responseData: any) => this.decode<T>(responseData));\n }\n\n /**\n * Updates an existing item by its id.\n *\n * You can use the generic T to supply a wrapper type of the crud model.\n */\n update<T = M>(\n id: string,\n bodyParams?: {[key:string]:any}|FormData,\n options?: CommonOptions,\n ): Promise<T> {\n options = Object.assign({\n 'method': 'PATCH',\n 'body': bodyParams,\n }, options);\n\n return this.client.send(this.baseCrudPath + '/' + encodeURIComponent(id), options)\n .then((responseData: any) => this.decode<T>(responseData));\n }\n\n /**\n * Deletes an existing item by its id.\n */\n delete(id: string, options?: CommonOptions): Promise<boolean> {\n options = Object.assign({\n 'method': 'DELETE',\n }, options);\n\n return this.client.send(this.baseCrudPath + '/' + encodeURIComponent(id), options)\n .then(() => true);\n }\n\n /**\n * Returns a promise with all list items batch fetched at once.\n */\n protected _getFullList<T = M>(batchSize = 500, options?: ListOptions): Promise<Array<T>> {\n options = options || {};\n options.query = Object.assign({\n 'skipTotal': 1,\n }, options.query);\n\n let result: Array<T> = [];\n\n let request = async (page: number): Promise<Array<any>> => {\n return this.getList(page, batchSize || 500, options).then((list) => {\n const castedList = (list as any as ListResult<T>);\n const items = castedList.items;\n\n result = result.concat(items);\n\n if (items.length == list.perPage) {\n return request(page + 1);\n }\n\n return result;\n });\n }\n\n return request(1);\n }\n}\n","import { SendOptions } from '@/services/utils/options';\n\nexport function normalizeLegacyOptionsArgs(legacyWarn: string, baseOptions: SendOptions, bodyOrOptions?: any, query?: any): SendOptions {\n const hasBodyOrOptions = typeof bodyOrOptions !== 'undefined';\n const hasQuery = typeof query !== 'undefined';\n\n if (!hasQuery && !hasBodyOrOptions) {\n return baseOptions;\n }\n\n if (hasQuery) {\n console.warn(legacyWarn);\n baseOptions.body = Object.assign({}, baseOptions.body, bodyOrOptions);\n baseOptions.query = Object.assign({}, baseOptions.query, query);\n\n return baseOptions;\n }\n\n return Object.assign(baseOptions, bodyOrOptions);\n}\n","import Client from '@/Client';\nimport { isTokenExpired } from '@/stores/utils/jwt';\n\n// reset previous auto refresh registrations\nexport function resetAutoRefresh(client: Client) {\n (client as any)._resetAutoRefresh?.();\n}\n\nexport function registerAutoRefresh(\n client: Client,\n threshold: number,\n refreshFunc: () => Promise<any>,\n reauthenticateFunc: () => Promise<any>,\n) {\n resetAutoRefresh(client);\n\n const oldBeforeSend = client.beforeSend;\n const oldModel = client.authStore.model;\n\n // unset the auto refresh in case the auth store was cleared\n // OR a new model was authenticated\n const unsubStoreChange = client.authStore.onChange((newToken, model) => {\n if (\n !newToken ||\n model?.id != oldModel?.id ||\n // check the collection id in case an admin and auth record share the same id\n ((model?.collectionId || oldModel?.collectionId) && model?.collectionId != oldModel?.collectionId)\n ) {\n resetAutoRefresh(client);\n }\n });\n\n // initialize a reset function and attach it dynamically to the client\n (client as any)._resetAutoRefresh = function() {\n unsubStoreChange();\n client.beforeSend = oldBeforeSend;\n delete (client as any)._resetAutoRefresh;\n };\n\n client.beforeSend = async (url, sendOptions) => {\n const oldToken = client.authStore.token;\n\n if (sendOptions.query?.autoRefresh) {\n return oldBeforeSend ? oldBeforeSend(url, sendOptions) : { url, sendOptions };\n }\n\n let isValid = client.authStore.isValid;\n if (\n // is loosely valid\n isValid &&\n // but it is going to expire in the next \"threshold\" seconds\n isTokenExpired(client.authStore.token, threshold)\n ) {\n try {\n await refreshFunc();\n } catch (_) {\n isValid = false;\n }\n }\n\n // still invalid -> reauthenticate\n if (!isValid) {\n await reauthenticateFunc();\n }\n\n // the request wasn't sent with a custom token\n const headers = sendOptions.headers || {};\n for (let key in headers) {\n if (\n key.toLowerCase() == \"authorization\" &&\n // the request wasn't sent with a custom token\n oldToken == headers[key] &&\n client.authStore.token\n ) {\n // set the latest store token\n headers[key] = client.authStore.token;\n break;\n }\n }\n sendOptions.headers = headers;\n\n return oldBeforeSend ? oldBeforeSend(url, sendOptions) : { url, sendOptions };\n }\n}\n","import { CrudService } from '@/services/utils/CrudService';\nimport { AdminModel } from '@/services/utils/dtos';\nimport { AuthOptions, CommonOptions } from '@/services/utils/options';\nimport { normalizeLegacyOptionsArgs } from '@/services/utils/legacy';\nimport { registerAutoRefresh, resetAutoRefresh } from '@/services/utils/refresh';\n\nexport interface AdminAuthResponse {\n [key: string]: any;\n\n token: string;\n admin: AdminModel;\n}\n\nexport class AdminService extends CrudService<AdminModel> {\n /**\n * @inheritdoc\n */\n get baseCrudPath(): string {\n return '/api/admins';\n }\n\n // ---------------------------------------------------------------\n // Post update/delete AuthStore sync\n // ---------------------------------------------------------------\n\n /**\n * @inheritdoc\n *\n * If the current `client.authStore.model` matches with the updated id, then\n * on success the `client.authStore.model` will be updated with the result.\n */\n update<T = AdminModel>(\n id: string,\n bodyParams?: {[key:string]:any}|FormData,\n options?: CommonOptions,\n ): Promise<T> {\n return super.update(id, bodyParams, options).then((item) => {\n // update the store state if the updated item id matches with the stored model\n if (\n this.client.authStore.model?.id === item.id &&\n typeof this.client.authStore.model?.collectionId === 'undefined' // is not record auth\n ) {\n this.client.authStore.save(this.client.authStore.token, item);\n }\n\n return item as any as T;\n });\n }\n\n /**\n * @inheritdoc\n *\n * If the current `client.authStore.model` matches with the deleted id,\n * then on success the `client.authStore` will be cleared.\n */\n delete(id: string, options?: CommonOptions): Promise<boolean> {\n return super.delete(id, options).then((success) => {\n // clear the store state if the deleted item id matches with the stored model\n if (\n success &&\n this.client.authStore.model?.id === id &&\n typeof this.client.authStore.model?.collectionId === 'undefined' // is not record auth\n ) {\n this.client.authStore.clear();\n }\n\n return success;\n });\n }\n\n // ---------------------------------------------------------------\n // Auth handlers\n // ---------------------------------------------------------------\n\n /**\n * Prepare successful authorize response.\n */\n protected authResponse(responseData: any): AdminAuthResponse {\n const admin = this.decode(responseData?.admin || {});\n\n if (responseData?.token && responseData?.admin) {\n this.client.authStore.save(responseData.token, admin);\n }\n\n return Object.assign({}, responseData, {\n // normalize common fields\n 'token': responseData?.token || '',\n 'admin': admin,\n });\n }\n\n /**\n * Authenticate an admin account with its email and password\n * and returns a new admin token and data.\n *\n * On success this method automatically updates the client's AuthStore data.\n */\n authWithPassword(email: string, password: string, options?: AuthOptions): Promise<AdminAuthResponse>\n\n /**\n * @deprecated\n * Consider using authWithPassword(email, password, options?).\n */\n authWithPassword(email: string, password: string, body?: any, query?: any): Promise<AdminAuthResponse>\n\n async authWithPassword(\n email: string,\n password: string,\n bodyOrOptions?: any,\n query?: any,\n ): Promise<AdminAuthResponse> {\n let options: any = {\n 'method': 'POST',\n 'body': {\n 'identity': email,\n 'password': password,\n },\n };\n\n options = normalizeLegacyOptionsArgs(\n 'This form of authWithPassword(email, pass, body?, query?) is deprecated. Consider replacing it with authWithPassword(email, pass, options?).',\n options,\n bodyOrOptions,\n query\n );\n\n const autoRefreshThreshold = options.autoRefreshThreshold;\n delete options.autoRefreshThreshold;\n\n // not from auto refresh reauthentication\n if (!options.autoRefresh) {\n resetAutoRefresh(this.client);\n }\n\n let authData = await this.client.send(this.baseCrudPath + '/auth-with-password', options);\n\n authData = this.authResponse(authData);\n\n if (autoRefreshThreshold) {\n registerAutoRefresh(\n this.client,\n autoRefreshThreshold,\n () => this.authRefresh({autoRefresh: true}),\n () => this.authWithPassword(email, password, Object.assign({autoRefresh: true}, options)),\n );\n }\n\n return authData;\n }\n\n /**\n * Refreshes the current admin authenticated instance and\n * returns a new token and admin data.\n *\n * On success this method automatically updates the client's AuthStore data.\n */\n authRefresh(options?: CommonOptions): Promise<AdminAuthResponse>\n\n /**\n * @deprecated\n * Consider using authRefresh(options?).\n */\n authRefresh(body?: any, query?: any): Promise<AdminAuthResponse>\n\n authRefresh(bodyOrOptions?: any, query?: any): Promise<AdminAuthResponse> {\n let options: any = {\n 'method': 'POST',\n };\n\n options = normalizeLegacyOptionsArgs(\n 'This form of authRefresh(body?, query?) is deprecated. Consider replacing it with authRefresh(options?).',\n options,\n bodyOrOptions,\n query\n );\n\n return this.client.send(this.baseCrudPath + '/auth-refresh', options)\n .then(this.authResponse.bind(this));\n }\n\n /**\n * Sends admin password reset request.\n */\n requestPasswordReset(email: string, options?: CommonOptions): Promise<boolean>\n\n /**\n * @deprecated\n * Consider using requestPasswordReset(email, options?).\n */\n requestPasswordReset(email: string, body?: any, query?: any): Promise<boolean>\n\n requestPasswordReset(email: string, bodyOrOptions?: any, query?: any): Promise<boolean> {\n let options: any = {\n 'method': 'POST',\n 'body': {\n 'email': email,\n },\n };\n\n options = normalizeLegacyOptionsArgs(\n 'This form of requestPasswordReset(email, body?, query?) is deprecated. Consider replacing it with requestPasswordReset(email, options?).',\n options,\n bodyOrOptions,\n query\n );\n\n return this.client.send(this.baseCrudPath + '/request-password-reset', options)\n .then(() => true);\n }\n\n /**\n * Confirms admin password reset request.\n */\n confirmPasswordReset(resetToken: string, password: string, passwordConfirm: string, options?: CommonOptions): Promise<boolean>\n\n /**\n * @deprecated\n * Consider using confirmPasswordReset(resetToken, password, passwordConfirm, options?).\n */\n confirmPasswordReset(resetToken: string, password: string, passwordConfirm: string, body?: any, query?: any): Promise<boolean>\n\n confirmPasswordReset(resetToken: string, password: string, passwordConfirm: string, bodyOrOptions?: any, query?: any): Promise<boolean>{\n let options: any = {\n 'method': 'POST',\n 'body': {\n 'token': resetToken,\n 'password': password,\n 'passwordConfirm': passwordConfirm,\n },\n };\n\n options = normalizeLegacyOptionsArgs(\n 'This form of confirmPasswordReset(resetToken, password, passwordConfirm, body?, query?) is deprecated. Consider replacing it with confirmPasswordReset(resetToken, password, passwordConfirm, options?).',\n options,\n bodyOrOptions,\n query\n );\n\n return this.client.send(this.baseCrudPath + '/confirm-password-reset', options)\n .then(() => true);\n }\n}\n","export interface SendOptions extends RequestInit {\n // for backward compatibility and to minimize the verbosity,\n // any top-level field that doesn't exist in RequestInit or the\n // fields below will be treated as query parameter.\n [key: string]: any;\n\n /**\n * Optional custom fetch function to use for sending the request.\n */\n fetch?: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response>;\n\n /**\n * Custom headers to send with the requests.\n */\n headers?: { [key: string]: string };\n\n /**\n * The body of the request (serialized automatically for json requests).\n */\n body?: any;\n\n /**\n * Query parameters that will be appended to the request url.\n */\n query?: { [key: string]: any };\n\n /**\n * @deprecated use `query` instead\n *\n * for backward-compatibility `params` values are merged with `query`,\n * but this option may get removed in the final v1 release\n */\n params?: { [key: string]: any };\n\n /**\n * The request identifier that can be used to cancel pending requests.\n */\n requestKey?: string|null;\n\n /**\n * @deprecated use `requestKey:string` instead\n */\n $cancelKey?: string;\n\n /**\n * @deprecated use `requestKey:null` instead\n */\n $autoCancel?: boolean;\n}\n\nexport interface CommonOptions extends SendOptions {\n fields?: string;\n}\n\nexport interface ListOptions extends CommonOptions {\n page?: number;\n perPage?: number;\n sort?: string;\n filter?: string;\n skipTotal?: boolean;\n}\n\nexport interface FullListOptions extends ListOptions {\n batch?: number;\n}\n\nexport interface RecordOptions extends CommonOptions {\n expand?: string;\n}\n\nexport interface RecordListOptions extends ListOptions, RecordOptions {\n}\n\nexport interface RecordFullListOptions extends FullListOptions, RecordOptions {\n}\n\nexport interface LogStatsOptions extends CommonOptions {\n filter?: string;\n}\n\nexport interface FileOptions extends CommonOptions {\n thumb?: string;\n download?: boolean;\n}\n\nexport interface AuthOptions extends CommonOptions {\n /**\n * If autoRefreshThreshold is set it will take care to auto refresh\n * when necessary the auth data before each request to ensure that\n * the auth state is always valid.\n *\n * The value must be in seconds, aka. the amount of seconds\n * that will be subtracted from the current token `exp` claim in order\n * to determine whether it is going to expire within the specified time threshold.\n *\n * For example, if you want to auto refresh the token if it is\n * going to expire in the next 30mins (or already has expired),\n * it can be set to `1800`\n */\n autoRefreshThreshold?: number;\n}\n\n// -------------------------------------------------------------------\n\n// list of known SendOptions keys (everything else is treated as query param)\nconst knownSendOptionsKeys = [\n 'requestKey',\n '$cancelKey',\n '$autoCancel',\n 'fetch',\n 'headers',\n 'body',\n 'query',\n 'params',\n // ---,\n 'cache',\n 'credentials',\n 'headers',\n 'integrity',\n 'keepalive',\n 'method',\n 'mode',\n 'redirect',\n 'referrer',\n 'referrerPolicy',\n 'signal',\n 'window',\n];\n\n// modifies in place the provided options by moving unknown send options as query parameters.\nexport function normalizeUnknownQueryParams(options?: SendOptions): void {\n if (!options) {\n return\n }\n\n options.query = options.query || {};\n for (let key in options) {\n if (knownSendOptionsKeys.includes(key)) {\n continue\n }\n\n options.query[key] = options[key];\n delete (options[key]);\n }\n}\n","import { ClientResponseError } from '@/ClientResponseError';\nimport { BaseService } from '@/services/utils/BaseService';\nimport { SendOptions, normalizeUnknownQueryParams } from '@/services/utils/options';\n\ninterface promiseCallbacks {\n resolve: Function\n reject: Function\n}\n\ntype Subscriptions = { [key: string]: Array<EventListener> };\n\nexport type UnsubscribeFunc = () => Promise<void>;\n\nexport class RealtimeService extends BaseService {\n clientId: string = \"\";\n\n private eventSource: EventSource | null = null;\n private subscriptions: Subscriptions = {};\n private lastSentSubscriptions: Array<string> = [];\n private connectTimeoutId: any;\n private maxConnectTimeout: number = 15000;\n private reconnectTimeoutId: any;\n private reconnectAttempts: number = 0;\n private maxReconnectAttempts: number = Infinity;\n private predefinedReconnectIntervals: Array<number> = [\n 200, 300, 500, 1000, 1200, 1500, 2000,\n ];\n private pendingConnects: Array<promiseCallbacks> = [];\n\n /**\n * Returns whether the realtime connection has been established.\n */\n get isConnected(): boolean {\n return !!this.eventSource && !!this.clientId && !this.pendingConnects.length;\n }\n\n /**\n * Register the subscription listener.\n *\n * You can subscribe multiple times to the same topic.\n *\n * If the SSE connection is not started yet,\n * this method will also initialize it.\n */\n async subscribe(\n topic: string,\n callback: (data: any) => void,\n options?: SendOptions,\n ): Promise<UnsubscribeFunc> {\n if (!topic) {\n throw new Error('topic must be set.')\n }\n\n let key = topic;\n\n // serialize and ap