astro-integration-pocketbase
Version:
An Astro integration to support developers working with astro-loader-pocketbase.
237 lines (236 loc) • 10.5 kB
JavaScript
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