astro-integration-pocketbase 0.2.0 → 1.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # astro-integration-pocketbase
2
2
 
3
- <!-- ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/pawcoding/astro-integration-pocketbase/release.yaml?style=flat-square) -->
4
-
3
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/pawcoding/astro-integration-pocketbase/release.yaml?style=flat-square)
5
4
  [![NPM Version](https://img.shields.io/npm/v/astro-integration-pocketbase?style=flat-square)](https://www.npmjs.com/package/astro-integration-pocketbase)
6
5
  [![NPM Downloads](https://img.shields.io/npm/dw/astro-integration-pocketbase?style=flat-square)](https://www.npmjs.com/package/astro-integration-pocketbase)
7
6
  [![GitHub License](https://img.shields.io/github/license/pawcoding/astro-integration-pocketbase?style=flat-square)](https://github.com/pawcoding/astro-integration-pocketbase/blob/master/LICENSE)
@@ -11,9 +10,11 @@ This package provides an Astro toolbar for users of [astro-loader-pocketbase](ht
11
10
 
12
11
  ![PocketBase Toolbar](/assets/toolbar.png)
13
12
 
14
- > [!WARNING]
15
- > This package is still under development.
16
- > Until the first stable 1.0 release **breaking changes can occur at any time**.
13
+ ## Compatibility
14
+
15
+ | Integration | Loader | Astro | PocketBase |
16
+ | ----------- | ------ | ----- | ---------- |
17
+ | 1.0.0 | 2.0.0 | 5.0.0 | >= 0.23.0 |
17
18
 
18
19
  ## Basic usage
19
20
 
@@ -40,6 +41,29 @@ If you click on the icon, you can see the PocketBase entity viewer.
40
41
 
41
42
  If a loader is found, the viewer will show a refresh button to reload all entries from the loaders.
42
43
 
44
+ ## Realtime updates
45
+
46
+ PocketBase allows you to subscribe to collection changes via its [Realtime API](https://pocketbase.io/docs/api-realtime/).
47
+ This integration allows you to subscribe to these changes and reload the entries / collections.
48
+ Note that Node.js currently does not support the [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) API, so the integration uses the [eventsource](https://www.npmjs.com/package/eventsource) package to provide the same functionality.
49
+
50
+ If you want realtime updates for collections with a [restricted list / search rule](https://pocketbase.io/docs/api-rules-and-filters/), you need to provide superuser credentials to the integration.
51
+
52
+ ```ts
53
+ pocketbaseIntegration({
54
+ ...options,
55
+ // List of PocketBase collections to watch for changes
56
+ collectionsToWatch: ["posts", "comments"],
57
+ // Superuser credentials for restricted collections (optional)
58
+ superuserCredentials: {
59
+ email: "<superuser-email>",
60
+ password: "<superuser-password>"
61
+ }
62
+ });
63
+ ```
64
+
65
+ **Tip:** You can disable the realtime updates temporarily via the toolbar.
66
+
43
67
  ## Entity viewer
44
68
 
45
69
  To view the PocketBase entries inside the entity viewer, you need to use `Astro.props` to pass the entries to your page (and thus to the toolbar).
@@ -82,6 +106,8 @@ The integration will automatically detect PocketBase entries in the props and di
82
106
 
83
107
  ## All options
84
108
 
85
- | Option | Type | Required | Description |
86
- | ------ | -------- | -------- | ------------------------------------ |
87
- | `url` | `string` | x | The URL of your PocketBase instance. |
109
+ | Option | Type | Required | Description |
110
+ | ---------------------- | ------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
111
+ | `url` | `string` | x | The URL of your PocketBase instance. |
112
+ | `collectionsToWatch` | `Array<string>` | | Collections to watch for changes. |
113
+ | `superuserCredentials` | `{ email: string, password: string }` | | The email and password of a superuser of the PocketBase instance. This is used for realtime updates of restricted collection. |
package/index.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  import { pocketbaseIntegration } from "./src/pocketbase-integration";
2
+ import type { PocketBaseIntegrationOptions } from "./src/types/pocketbase-integration-options.type";
2
3
 
3
4
  export { pocketbaseIntegration };
5
+ export type { PocketBaseIntegrationOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-integration-pocketbase",
3
- "version": "0.2.0",
3
+ "version": "1.0.0-next.1",
4
4
  "license": "MIT",
5
5
  "author": "Luis Wolf <development@pawcode.de> (https://pawcode.de)",
6
6
  "homepage": "https://github.com/pawcoding/astro-integration-pocketbase",
@@ -17,18 +17,20 @@
17
17
  "prepare": "husky"
18
18
  },
19
19
  "peerDependencies": {
20
- "astro": "^4.0.0 || ^5.0.0"
20
+ "astro": "^5.0.0",
21
+ "eventsource": "^3.0.2"
21
22
  },
22
23
  "devDependencies": {
23
- "@eslint/js": "^9.12.0",
24
- "@stylistic/eslint-plugin": "^2.9.0",
25
- "@types/node": "^22.7.4",
26
- "astro": "^5.0.0-beta.3",
27
- "eslint": "^9.12.0",
28
- "globals": "^15.10.0",
29
- "husky": "^9.1.6",
30
- "typescript": "^5.6.2",
31
- "typescript-eslint": "^8.8.0"
24
+ "@eslint/js": "^9.17.0",
25
+ "@stylistic/eslint-plugin": "^2.12.1",
26
+ "@types/node": "^22.10.2",
27
+ "astro": "^5.1.1",
28
+ "eslint": "^9.17.0",
29
+ "eventsource": "^3.0.2",
30
+ "globals": "^15.14.0",
31
+ "husky": "^9.1.7",
32
+ "typescript": "^5.7.2",
33
+ "typescript-eslint": "^8.18.2"
32
34
  },
33
35
  "keywords": [
34
36
  "astro",
@@ -0,0 +1,2 @@
1
+ export * from "./refresh-collections";
2
+ export * from "./refresh-collections-realtime";
@@ -0,0 +1,134 @@
1
+ import type { BaseIntegrationHooks } from "astro";
2
+ import { EventSource } from "eventsource";
3
+ import type { PocketBaseIntegrationOptions } from "../types/pocketbase-integration-options.type";
4
+ import { getSuperuserToken } from "../utils/get-superuser-token";
5
+
6
+ export function refreshCollectionsRealtime(
7
+ {
8
+ url,
9
+ superuserCredentials,
10
+ collectionsToWatch
11
+ }: PocketBaseIntegrationOptions,
12
+ {
13
+ logger,
14
+ refreshContent,
15
+ toolbar
16
+ }: Parameters<BaseIntegrationHooks["astro:server:setup"]>[0]
17
+ ): EventSource | undefined {
18
+ // Check if collections should be watched
19
+ if (!collectionsToWatch || collectionsToWatch.length === 0) {
20
+ return undefined;
21
+ }
22
+
23
+ // Check if content loader is used
24
+ if (!refreshContent) {
25
+ logger.warn(
26
+ "No content loader available, skipping subscription to PocketBase realtime API."
27
+ );
28
+ return undefined;
29
+ }
30
+
31
+ // Check if EventSource is available
32
+ if (!EventSource) {
33
+ logger.warn(
34
+ "EventSource is not available, skipping subscription to PocketBase realtime API.\n" +
35
+ "Please install the 'eventsource' package."
36
+ );
37
+ return undefined;
38
+ }
39
+
40
+ let refreshEnabled = true;
41
+ // Enable or disable real-time updates via the toolbar
42
+ toolbar.on("astro-integration-pocketbase:real-time", (enabled: boolean) => {
43
+ refreshEnabled = enabled;
44
+ });
45
+
46
+ const eventSource = new EventSource(`${url}/api/realtime`);
47
+ let wasConnectedOnce = false;
48
+ let isConnected = false;
49
+
50
+ // Log potential errors
51
+ eventSource.onerror = (error) => {
52
+ isConnected = false;
53
+
54
+ // Wait for 5 seconds in case of a connection error
55
+ setTimeout(() => {
56
+ if (isConnected) {
57
+ // Connection was automatically re-established, no need to log the error
58
+ return;
59
+ }
60
+
61
+ logger.error(
62
+ `Error while connecting to PocketBase realtime API: ${error.type}`
63
+ );
64
+ }, 5000);
65
+ };
66
+
67
+ // Add event listeners for all collections
68
+ for (const collection of collectionsToWatch) {
69
+ eventSource.addEventListener(`${collection}/*`, async () => {
70
+ // Do not refresh if the refresh is disabled
71
+ if (!refreshEnabled) {
72
+ return;
73
+ }
74
+
75
+ // Refresh the content
76
+ logger.info(`Received update for ${collection}. Refreshing content...`);
77
+ await refreshContent({
78
+ loaders: ["pocketbase-loader"],
79
+ // TODO: add context to refresh one or all collections
80
+ context: {}
81
+ });
82
+ });
83
+ }
84
+
85
+ // Add event listener for the connection event
86
+ eventSource.addEventListener("PB_CONNECT", async (event) => {
87
+ // Extract the clientId
88
+ const clientId = event.lastEventId;
89
+
90
+ // Get the superuser token if credentials are available
91
+ let superuserToken: string | undefined;
92
+ if (superuserCredentials) {
93
+ superuserToken = await getSuperuserToken(
94
+ url,
95
+ superuserCredentials,
96
+ logger
97
+ );
98
+ }
99
+
100
+ // Subscribe to the PocketBase realtime API
101
+ const result = await fetch(`${url}/api/realtime`, {
102
+ method: "POST",
103
+ headers: {
104
+ "Content-Type": "application/json",
105
+ Authorization: superuserToken || ""
106
+ },
107
+ body: JSON.stringify({
108
+ clientId: clientId,
109
+ subscriptions: collectionsToWatch.map((collection) => `${collection}/*`)
110
+ })
111
+ });
112
+
113
+ // Log the connection status
114
+ if (!result.ok) {
115
+ logger.error(
116
+ `Error while subscribing to PocketBase realtime API: ${result.status}`
117
+ );
118
+ return;
119
+ }
120
+
121
+ if (!wasConnectedOnce) {
122
+ wasConnectedOnce = true;
123
+ logger.info(
124
+ `Subscribed to PocketBase realtime API. Waiting for updates on ${collectionsToWatch.join(
125
+ ", "
126
+ )}.`
127
+ );
128
+ }
129
+
130
+ isConnected = true;
131
+ });
132
+
133
+ return eventSource;
134
+ }
@@ -0,0 +1,38 @@
1
+ import type { BaseIntegrationHooks } from "astro";
2
+
3
+ /**
4
+ * Listen for the refresh event of the toolbar.
5
+ * When the event is triggered in the toolbar, refresh the content loaded by the PocketBase loader.
6
+ */
7
+ export function handleRefreshCollections({
8
+ toolbar,
9
+ refreshContent,
10
+ logger
11
+ }: Parameters<BaseIntegrationHooks["astro:server:setup"]>[0]): void {
12
+ if (!refreshContent) {
13
+ return;
14
+ }
15
+
16
+ logger.info("Setting up refresh listener for PocketBase integration");
17
+
18
+ // Listen for the refresh event of the toolbar
19
+ toolbar.on("astro-integration-pocketbase:refresh", async () => {
20
+ // Send a loading state to the toolbar
21
+ toolbar.send("astro-integration-pocketbase:refresh", {
22
+ loading: true
23
+ });
24
+
25
+ // Refresh content loaded by the PocketBase loader
26
+ logger.info("Refreshing content loaded by PocketBase loader");
27
+ await refreshContent({
28
+ loaders: ["pocketbase-loader"],
29
+ // TODO: add context to refresh one or all collections
30
+ context: {}
31
+ });
32
+
33
+ // Reset the loading state in the toolbar
34
+ toolbar.send("astro-integration-pocketbase:refresh", {
35
+ loading: false
36
+ });
37
+ });
38
+ }
@@ -1,11 +1,15 @@
1
1
  import type { AstroIntegration } from "astro";
2
+ import { EventSource } from "eventsource";
2
3
  import { fileURLToPath } from "node:url";
4
+ import { handleRefreshCollections, refreshCollectionsRealtime } from "./core";
5
+ import type { ToolbarOptions } from "./toolbar/types/options";
6
+ import type { PocketBaseIntegrationOptions } from "./types/pocketbase-integration-options.type";
7
+
8
+ export function pocketbaseIntegration(
9
+ options: PocketBaseIntegrationOptions
10
+ ): AstroIntegration {
11
+ let eventSource: EventSource | undefined = undefined;
3
12
 
4
- export function pocketbaseIntegration({
5
- url
6
- }: {
7
- url: string;
8
- }): AstroIntegration {
9
13
  return {
10
14
  name: "pocketbase-integration",
11
15
  hooks: {
@@ -29,38 +33,27 @@ export function pocketbaseIntegration({
29
33
  entrypoint: fileURLToPath(new URL("./middleware", import.meta.url))
30
34
  });
31
35
  },
32
- "astro:server:setup": ({ toolbar, logger, refreshContent }) => {
33
- // Setup the listener for the refresh event if a loader is available
34
- if (refreshContent) {
35
- logger.info("Setting up refresh listener for PocketBase integration");
36
- // Listen for the refresh event of the toolbar
37
- toolbar.on("astro-integration-pocketbase:refresh", async () => {
38
- // Send a loading state to the toolbar
39
- toolbar.send("astro-integration-pocketbase:refresh", {
40
- loading: true
41
- });
42
-
43
- // Refresh content loaded by the PocketBase loader
44
- await refreshContent({
45
- loaders: ["pocketbase-loader"],
46
- // TODO: add context to refresh one or all collections
47
- context: {}
48
- });
36
+ "astro:server:setup": (setupOptions) => {
37
+ // Listen for the refresh event of the toolbar
38
+ handleRefreshCollections(setupOptions);
49
39
 
50
- // Reset the loading state in the toolbar
51
- toolbar.send("astro-integration-pocketbase:refresh", {
52
- loading: false
53
- });
54
- });
55
- }
40
+ // Subscribe to PocketBase realtime API
41
+ eventSource = refreshCollectionsRealtime(options, setupOptions);
56
42
 
57
43
  // Send settings to the toolbar on initialization
58
- toolbar.onAppInitialized("pocketbase-entry", () => {
59
- toolbar.send("astro-integration-pocketbase:settings", {
60
- enabled: !!refreshContent,
61
- baseUrl: url
62
- });
44
+ setupOptions.toolbar.onAppInitialized("pocketbase-entry", () => {
45
+ setupOptions.toolbar.send("astro-integration-pocketbase:settings", {
46
+ hasContentLoader: !!setupOptions.refreshContent,
47
+ realtime: !!eventSource,
48
+ baseUrl: options.url
49
+ } satisfies ToolbarOptions);
63
50
  });
51
+ },
52
+ "astro:server:done": () => {
53
+ // Close the EventSource connection when the server is done
54
+ if (eventSource) {
55
+ eventSource.close();
56
+ }
64
57
  }
65
58
  }
66
59
  };
@@ -2,12 +2,16 @@ import type { ToolbarServerHelpers } from "astro";
2
2
  import type { DevToolbarButton } from "astro/runtime/client/dev-toolbar/ui-library/button.js";
3
3
  import { default as packageJson } from "../../../package.json";
4
4
 
5
+ export interface HeaderElements {
6
+ header: HTMLElement;
7
+ refresh: DevToolbarButton;
8
+ toggleContainer: HTMLDivElement;
9
+ }
10
+
5
11
  /**
6
12
  * Creates the header for the PocketBase toolbar.
7
13
  */
8
- export function createHeader(
9
- server: ToolbarServerHelpers
10
- ): [HTMLElement, DevToolbarButton] {
14
+ export function createHeader(server: ToolbarServerHelpers): HeaderElements {
11
15
  // Create the header
12
16
  const header = document.createElement("header");
13
17
  header.style.display = "grid";
@@ -26,18 +30,62 @@ export function createHeader(
26
30
  version.badgeStyle = "yellow";
27
31
  header.appendChild(version);
28
32
 
33
+ // Create the actions container
34
+ const actions = document.createElement("div");
35
+ actions.style.display = "flex";
36
+ actions.style.alignItems = "start";
37
+ actions.style.justifyContent = "flex-end";
38
+ actions.style.gap = "0.25rem";
39
+ header.appendChild(actions);
40
+
41
+ // Create the real-time toggle
42
+ const toggleContainer = document.createElement("div");
43
+ toggleContainer.style.alignItems = "center";
44
+ // The toggle container is hidden by default
45
+ toggleContainer.style.display = "none";
46
+ actions.appendChild(toggleContainer);
47
+
48
+ const toggleLabel = document.createElement("label");
49
+ toggleLabel.textContent = "Real-time updates";
50
+ toggleLabel.htmlFor = "real-time-toggle";
51
+ toggleLabel.style.fontSize = "0.8rem";
52
+ toggleContainer.appendChild(toggleLabel);
53
+
54
+ const toggle = document.createElement("astro-dev-toolbar-toggle");
55
+ toggle.input.id = "real-time-toggle";
56
+ // Set the toggle state based on the local storage, default to true
57
+ toggle.input.checked = !(
58
+ localStorage.getItem("astro-integration-pocketbase:real-time") === "false"
59
+ );
60
+ toggle.input.addEventListener("change", () => {
61
+ // Store the toggle state in the local storage
62
+ localStorage.setItem(
63
+ "astro-integration-pocketbase:real-time",
64
+ toggle.input.checked.toString()
65
+ );
66
+
67
+ // Send the toggle state to the server
68
+ server.send("astro-integration-pocketbase:real-time", toggle.input.checked);
69
+ });
70
+ // Send the initial toggle state to the server
71
+ server.send("astro-integration-pocketbase:real-time", toggle.input.checked);
72
+ toggleContainer.appendChild(toggle);
73
+
29
74
  // Create the refresh button
30
75
  const refresh = document.createElement("astro-dev-toolbar-button");
31
76
  refresh.size = "small";
32
77
  refresh.buttonStyle = "green";
33
- refresh.style.marginLeft = "auto";
34
78
  refresh.textContent = "Refresh content";
35
79
  // The refresh button is hidden by default
36
80
  refresh.style.display = "none";
37
81
  refresh.addEventListener("click", () => {
38
82
  server.send("astro-integration-pocketbase:refresh", true);
39
83
  });
40
- header.appendChild(refresh);
84
+ actions.appendChild(refresh);
41
85
 
42
- return [header, refresh];
86
+ return {
87
+ header,
88
+ refresh,
89
+ toggleContainer
90
+ };
43
91
  }
@@ -5,6 +5,7 @@ import type {
5
5
  import { createEntity, createHeader, createPlaceholder } from "./dom/";
6
6
  import { listenToNavigation } from "./page-navigation-listener";
7
7
  import type { Entity } from "./types/entity";
8
+ import type { ToolbarOptions } from "./types/options";
8
9
 
9
10
  declare global {
10
11
  interface Window {
@@ -25,7 +26,7 @@ export async function initToolbar(
25
26
 
26
27
  const container = document.createElement("astro-dev-toolbar-window");
27
28
 
28
- const [header, refresh] = createHeader(server);
29
+ const { header, refresh, toggleContainer } = createHeader(server);
29
30
  container.appendChild(header);
30
31
 
31
32
  // Container for the main content
@@ -101,12 +102,17 @@ export async function initToolbar(
101
102
 
102
103
  server.on(
103
104
  "astro-integration-pocketbase:settings",
104
- ({ enabled, baseUrl }: { enabled: boolean; baseUrl: string }) => {
105
+ ({ hasContentLoader, realtime, baseUrl }: ToolbarOptions) => {
105
106
  // Show the refresh button if a loader is available
106
- if (enabled) {
107
+ if (hasContentLoader) {
107
108
  refresh.style.display = "unset";
108
109
  }
109
110
 
111
+ // Show the real-time toggle if real-time updates are available
112
+ if (realtime) {
113
+ toggleContainer.style.display = "flex";
114
+ }
115
+
110
116
  // Store the base URL for later use
111
117
  pbUrl = baseUrl;
112
118
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Options for the toolbar.
3
+ */
4
+ export interface ToolbarOptions {
5
+ /**
6
+ * Whether a content loader is active.
7
+ */
8
+ hasContentLoader: boolean;
9
+ /**
10
+ * Whether a realtime connection to PocketBase is active.
11
+ */
12
+ realtime: boolean;
13
+ /**
14
+ * Base URL of the PocketBase instance.
15
+ */
16
+ baseUrl: string;
17
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Options for the PocketBase integration.
3
+ */
4
+ export interface PocketBaseIntegrationOptions {
5
+ /**
6
+ * URL of the PocketBase instance.
7
+ */
8
+ url: string;
9
+ /**
10
+ * Credentials of a superuser to get full access to the PocketBase instance.
11
+ * This is required to access all resources even if they are not public.
12
+ */
13
+ superuserCredentials?: {
14
+ /**
15
+ * Email of the superuser.
16
+ */
17
+ email: string;
18
+ /**
19
+ * Password of the superuser.
20
+ */
21
+ password: string;
22
+ };
23
+ /**
24
+ * List of PocketBase collections to watch for changes.
25
+ * When an entry in one of these collections changes, the content will be reloaded.
26
+ */
27
+ collectionsToWatch?: Array<string>;
28
+ }
@@ -0,0 +1,46 @@
1
+ import type { AstroIntegrationLogger } from "astro";
2
+
3
+ /**
4
+ * This function will get a superuser token from the given PocketBase instance.
5
+ *
6
+ * @param url URL of the PocketBase instance
7
+ * @param superuserCredentials Credentials of the superuser
8
+ *
9
+ * @returns A superuser token to access all resources of the PocketBase instance.
10
+ */
11
+ export async function getSuperuserToken(
12
+ url: string,
13
+ superuserCredentials: {
14
+ email: string;
15
+ password: string;
16
+ },
17
+ logger: AstroIntegrationLogger
18
+ ): Promise<string | undefined> {
19
+ // Build the URL for the login endpoint
20
+ const loginUrl = new URL(
21
+ `api/collections/_superusers/auth-with-password`,
22
+ url
23
+ ).href;
24
+
25
+ // Create a new FormData object to send the login data
26
+ const loginData = new FormData();
27
+ loginData.set("identity", superuserCredentials.email);
28
+ loginData.set("password", superuserCredentials.password);
29
+
30
+ // Send the login request to get a token
31
+ const loginRequest = await fetch(loginUrl, {
32
+ method: "POST",
33
+ body: loginData
34
+ });
35
+
36
+ // If the login request was not successful, print the error message and return undefined
37
+ if (!loginRequest.ok) {
38
+ const reason = await loginRequest.json().then((data) => data.message);
39
+ const errorMessage = `The given email / password for ${url} was not correct. Astro may not have access to all resources and permissions.\nReason: ${reason}`;
40
+ logger.error(errorMessage);
41
+ return undefined;
42
+ }
43
+
44
+ // Return the token
45
+ return await loginRequest.json().then((data) => data.token);
46
+ }