UNPKG

astro-integration-pocketbase

Version:

An Astro integration to support developers working with astro-loader-pocketbase.

237 lines (236 loc) 10.5 kB
import { fileURLToPath } from "node:url"; import { EventSource } from "eventsource"; import { z } from "astro/zod"; //#region src/core/refresh-collections.ts /** * Listen for the refresh event of the toolbar. * When the event is triggered in the toolbar, refresh the content loaded by the PocketBase loader. */ function handleRefreshCollections({ toolbar, refreshContent, logger }) { if (!refreshContent) return; logger.info("Setting up refresh listener for PocketBase integration"); toolbar.on("astro-integration-pocketbase:refresh", async ({ force }) => { toolbar.send("astro-integration-pocketbase:refresh", { loading: true }); logger.info(`Refreshing ${force ? "all " : ""}content loaded by PocketBase loader`); await refreshContent({ loaders: ["pocketbase-loader"], context: { source: "astro-integration-pocketbase", force } }); toolbar.send("astro-integration-pocketbase:refresh", { loading: false }); }); } //#endregion //#region src/types/pocketbase-api-response.type.ts /** * The schema for a PocketBase error response. */ const pocketBaseErrorResponse = z.object({ /** * The error message returned by PocketBase. */ message: z.string() }); /** * The schema for a PocketBase login response. */ const pocketBaseLoginResponse = z.object({ /** * The authentication token returned by PocketBase. */ token: z.string() }); //#endregion //#region src/utils/get-superuser-token.ts /** * This function will get a superuser token from the given PocketBase instance. * * @param url URL of the PocketBase instance * @param superuserCredentials Credentials of the superuser * * @returns A superuser token to access all resources of the PocketBase instance. */ async function getSuperuserToken(url, superuserCredentials, logger) { const loginUrl = new URL(`api/collections/_superusers/auth-with-password`, url).href; const loginData = new FormData(); loginData.set("identity", superuserCredentials.email); loginData.set("password", superuserCredentials.password); const loginRequest = await fetch(loginUrl, { method: "POST", body: loginData }); if (!loginRequest.ok) { if (loginRequest.status === 429) { logger.info("A rate limit was hit while trying to authenticate with PocketBase. Consider using an `impersonateToken` as credentials to avoid this issue."); const retryAfter = Math.random() * 5 + 3; await new Promise((resolve) => { setTimeout(resolve, retryAfter * 1e3); }); return getSuperuserToken(url, superuserCredentials, logger); } const errorMessage = `The given email / password for ${url} was not correct. Astro can't generate type definitions automatically and may not have access to all resources.\nReason: ${pocketBaseErrorResponse.parse(await loginRequest.json()).message}`; logger.error(errorMessage); return; } return pocketBaseLoginResponse.parse(await loginRequest.json()).token; } //#endregion //#region src/utils/push-to-map-array.ts /** * Adds a value to an array in a map. If the key does not exist, it will be created. */ function pushToMapArray(map, key, value) { const array = map.get(key); if (array) { array.push(value); return; } map.set(key, [value]); } //#endregion //#region src/utils/map-collections-to-watch.ts /** * Create a map of remote collections to watch. * Each key in the map represents a remote collection to subscribe to. * The value is an array of local collections that should be refreshed when an entry in the remote collection changes. */ function mapCollectionsToWatch(collectionsToWatch) { if (!collectionsToWatch) return; if (Array.isArray(collectionsToWatch)) { if (collectionsToWatch.length === 0) return; return new Map(collectionsToWatch.map((collection) => [collection, [collection]])); } if (Object.keys(collectionsToWatch).length === 0) return; const collectionsMap = /* @__PURE__ */ new Map(); for (const localCollection in collectionsToWatch) { const watch = collectionsToWatch[localCollection]; if (!watch) continue; if (watch === true) { pushToMapArray(collectionsMap, localCollection, localCollection); continue; } for (const remoteCollection of watch) pushToMapArray(collectionsMap, remoteCollection, localCollection); } return collectionsMap; } //#endregion //#region src/core/refresh-collections-realtime.ts function refreshCollectionsRealtime(options, { logger, refreshContent, toolbar }) { const collectionsMap = mapCollectionsToWatch(options.collectionsToWatch); if (!collectionsMap) return; const remoteCollections = [...collectionsMap.keys()]; if (!refreshContent) { logger.warn("No content loader available, skipping subscription to PocketBase realtime API."); return; } if (!EventSource) { logger.warn("EventSource is not available, skipping subscription to PocketBase realtime API.\nPlease install the 'eventsource' package."); return; } let refreshEnabled = true; toolbar.on("astro-integration-pocketbase:real-time", (enabled) => { refreshEnabled = enabled; }); const eventSourceUrl = new URL("api/realtime", options.url).href; const eventSource = new EventSource(eventSourceUrl); let wasConnectedOnce = false; let isConnected = false; eventSource.addEventListener("error", (error) => { isConnected = false; setTimeout(() => { if (isConnected) return; logger.error(`Error while connecting to PocketBase realtime API: ${error.type}`); }, 5e3); }); for (const collection of remoteCollections) eventSource.addEventListener(`${collection}/*`, async (event) => { if (!refreshEnabled) return; logger.info(`Received update for ${collection}. Refreshing content...`); await refreshContent({ loaders: ["pocketbase-loader"], context: { source: "astro-integration-pocketbase", collection: collectionsMap.get(collection), data: JSON.parse(event.data) } }); }); eventSource.addEventListener("PB_CONNECT", async (event) => { isConnected = await handleConnectEvent(event, remoteCollections, wasConnectedOnce, options, logger); if (isConnected) wasConnectedOnce = true; }); return eventSource; } async function handleConnectEvent(event, remoteCollections, wasConnectedOnce, options, logger) { const clientId = event.lastEventId; let superuserToken; if (options.superuserCredentials) if ("impersonateToken" in options.superuserCredentials) superuserToken = options.superuserCredentials.impersonateToken; else superuserToken = await getSuperuserToken(options.url, options.superuserCredentials, logger); const subscriptionUrl = new URL("api/realtime", options.url).href; const result = await fetch(subscriptionUrl, { method: "POST", headers: { "Content-Type": "application/json", Authorization: superuserToken || "" }, body: JSON.stringify({ clientId, subscriptions: remoteCollections.map((collection) => `${collection}/*`) }) }); if (!result.ok) { logger.error(`Error while subscribing to PocketBase realtime API: ${result.status}`); return false; } if (!wasConnectedOnce) logger.info(`Subscribed to PocketBase realtime API. Waiting for updates on ${remoteCollections.join(", ")}.`); return true; } //#endregion //#region src/pocketbase-integration.ts function pocketbaseIntegration(options) { let eventSource = void 0; let initialSetupDone = false; return { name: "pocketbase-integration", hooks: { "astro:config:setup": ({ addDevToolbarApp, addMiddleware, command }) => { if (command !== "dev") return; addDevToolbarApp({ name: "PocketBase", id: `pocketbase-entry`, icon: `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PocketBase</title><path fill="currentColor" d="M5.684 12a.632.632 0 0 1-.631-.632V4.421c0-.349.282-.632.631-.632h2.37c.46 0 .889.047 1.287.139.407.084.758.23 1.053.44.303.202.541.475.715.82.173.335.26.75.26 1.246 0 .479-.092.894-.273 1.247a2.373 2.373 0 0 1-.715.869 3.11 3.11 0 0 1-1.053.503c-.398.11-.823.164-1.273.164h-.46a.632.632 0 0 0-.632.632v1.52a.632.632 0 0 1-.632.631Zm1.279-4.888c0 .349.283.632.632.632h.343c1.04 0 1.56-.437 1.56-1.31 0-.428-.135-.73-.404-.907-.26-.176-.645-.264-1.156-.264h-.343a.632.632 0 0 0-.632.631Zm6.3 13.098a.632.632 0 0 1-.631-.631v-6.947a.63.63 0 0 1 .631-.632h2.203c.44 0 .845.034 1.216.1.38.06.708.169.984.328.276.16.492.37.647.63.164.26.246.587.246.982 0 .185-.03.37-.09.554a1.537 1.537 0 0 1-.26.516 1.857 1.857 0 0 1-1.076.7.031.031 0 0 0-.023.03c0 .015.01.028.025.03.591.111 1.04.32 1.346.626.311.31.466.743.466 1.297 0 .42-.082.78-.246 1.083a2.153 2.153 0 0 1-.685.755 3.4 3.4 0 0 1-1.036.441 5.477 5.477 0 0 1-1.268.139zm1.271-5.542c0 .349.283.631.632.631h.21c.465 0 .802-.088 1.009-.264.207-.176.31-.424.31-.743 0-.302-.107-.516-.323-.642-.207-.135-.535-.202-.984-.202h-.222a.632.632 0 0 0-.632.632Zm0 3.463c0 .349.283.631.632.631h.39c1.019 0 1.528-.369 1.528-1.108 0-.36-.125-.621-.376-.78-.241-.16-.625-.24-1.152-.24h-.39a.632.632 0 0 0-.632.632zM1.389 0C.629 0 0 .629 0 1.389V15.03a1.4 1.4 0 0 0 1.389 1.39H8.21a.632.632 0 0 0 .63-.632.632.632 0 0 0-.63-.63H1.389c-.078 0-.125-.05-.125-.128V1.39c0-.078.047-.125.125-.125H15.03c.078 0 .127.047.127.125v6.82a.632.632 0 0 0 .631.63.632.632 0 0 0 .633-.63V1.389A1.4 1.4 0 0 0 15.032 0ZM15.79 7.578a.632.632 0 0 0-.632.633.632.632 0 0 0 .631.63h6.822c.078 0 .125.05.125.128V22.61c0 .078-.047.125-.125.125H8.97c-.077 0-.127-.047-.127-.125v-6.82a.632.632 0 0 0-.631-.63.632.632 0 0 0-.633.63v6.822A1.4 1.4 0 0 0 8.968 24h13.643c.76 0 1.389-.629 1.389-1.389V8.97a1.4 1.4 0 0 0-1.389-1.39Z"/></svg>`, entrypoint: fileURLToPath(new URL("./toolbar", import.meta.url)) }); addMiddleware({ order: "post", entrypoint: fileURLToPath(new URL("./middleware", import.meta.url)) }); }, "astro:server:setup": (setupOptions) => { if (!initialSetupDone) handleRefreshCollections(setupOptions); if (eventSource) { eventSource.close(); eventSource = void 0; } eventSource = refreshCollectionsRealtime(options, setupOptions); setupOptions.toolbar.onAppInitialized("pocketbase-entry", () => { setupOptions.toolbar.send("astro-integration-pocketbase:settings", { hasContentLoader: !!setupOptions.refreshContent, realtime: !!eventSource, baseUrl: options.url }); }); initialSetupDone = true; }, "astro:server:done": ({ logger }) => { if (eventSource) { logger.info("Closing EventSource connection"); eventSource.close(); eventSource = void 0; } } } }; } //#endregion export { pocketbaseIntegration }; //# sourceMappingURL=index.mjs.map