astro-loader-pocketbase 2.2.1 → 2.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-loader-pocketbase",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "A content loader for Astro that uses the PocketBase API",
5
5
  "license": "MIT",
6
6
  "author": "Luis Wolf <development@pawcode.de> (https://pawcode.de)",
@@ -21,15 +21,15 @@
21
21
  "astro": "^5.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@eslint/js": "^9.18.0",
25
- "@stylistic/eslint-plugin": "^2.13.0",
26
- "@types/node": "^22.10.7",
27
- "astro": "^5.1.8",
28
- "eslint": "^9.18.0",
24
+ "@eslint/js": "^9.19.0",
25
+ "@stylistic/eslint-plugin": "^3.0.1",
26
+ "@types/node": "^22.13.0",
27
+ "astro": "^5.2.3",
28
+ "eslint": "^9.19.0",
29
29
  "globals": "^15.14.0",
30
30
  "husky": "^9.1.7",
31
31
  "typescript": "^5.7.3",
32
- "typescript-eslint": "^8.21.0"
32
+ "typescript-eslint": "^8.22.0"
33
33
  },
34
34
  "keywords": [
35
35
  "astro",
@@ -45,7 +45,7 @@ export async function cleanupEntries(
45
45
  // If the collection is locked, an superuser token is required
46
46
  if (collectionRequest.status === 403) {
47
47
  context.logger.error(
48
- `(${options.collectionName}) The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
48
+ `The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
49
49
  );
50
50
  return;
51
51
  }
@@ -53,7 +53,7 @@ export async function cleanupEntries(
53
53
  const reason = await collectionRequest
54
54
  .json()
55
55
  .then((data) => data.message);
56
- const errorMessage = `(${options.collectionName}) Fetching ids failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
56
+ const errorMessage = `Fetching ids failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
57
57
  context.logger.error(errorMessage);
58
58
  return;
59
59
  }
@@ -74,7 +74,9 @@ export async function cleanupEntries(
74
74
  let cleanedUp = 0;
75
75
 
76
76
  // Get all ids of the entries in the store
77
- const storedIds = context.store.values().map((entry) => entry.data.id) as Array<string>;
77
+ const storedIds = context.store
78
+ .values()
79
+ .map((entry) => entry.data.id) as Array<string>;
78
80
  for (const id of storedIds) {
79
81
  // If the id is not in the entries set, remove the entry from the store
80
82
  if (!entries.has(id)) {
@@ -85,8 +87,6 @@ export async function cleanupEntries(
85
87
 
86
88
  if (cleanedUp > 0) {
87
89
  // Log the number of cleaned up entries
88
- context.logger.info(
89
- `(${options.collectionName}) Cleaned up ${cleanedUp} old entries.`
90
- );
90
+ context.logger.info(`Cleaned up ${cleanedUp} old entries.`);
91
91
  }
92
92
  }
@@ -0,0 +1,48 @@
1
+ import type { LoaderContext } from "astro/loaders";
2
+ import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
3
+ import { isRealtimeData } from "./utils/is-realtime-data";
4
+ import { parseEntry } from "./utils/parse-entry";
5
+
6
+ /**
7
+ * Handles realtime updates for the loader without making any new network requests.
8
+ *
9
+ * Returns `true` if the data was handled and no further action is needed.
10
+ */
11
+ export async function handleRealtimeUpdates(
12
+ context: LoaderContext,
13
+ options: PocketBaseLoaderOptions
14
+ ): Promise<boolean> {
15
+ // Check if data was provided via the refresh context
16
+ if (!context.refreshContextData?.data) {
17
+ return false;
18
+ }
19
+
20
+ // Check if the data is PocketBase realtime data
21
+ const data = context.refreshContextData.data;
22
+ if (!isRealtimeData(data)) {
23
+ return false;
24
+ }
25
+
26
+ // Check if the collection name matches the current collection
27
+ if (data.record.collectionName !== options.collectionName) {
28
+ return false;
29
+ }
30
+
31
+ // Handle deleted entry
32
+ if (data.action === "delete") {
33
+ context.logger.info("Removing deleted entry");
34
+ context.store.delete(data.record.id);
35
+ return true;
36
+ }
37
+
38
+ // Handle updated or new entry
39
+ if (data.action === "update") {
40
+ context.logger.info("Updating outdated entry");
41
+ } else {
42
+ context.logger.info("Creating new entry");
43
+ }
44
+
45
+ // Parse the entry and store
46
+ await parseEntry(data.record, context, options);
47
+ return true;
48
+ }
@@ -32,11 +32,9 @@ export async function loadEntries(
32
32
 
33
33
  // Log the fetching of the entries
34
34
  context.logger.info(
35
- `(${options.collectionName}) Fetching${
36
- lastModified ? " modified" : ""
37
- } data${lastModified ? ` starting at ${lastModified}` : ""}${
38
- superuserToken ? " as superuser" : ""
39
- }`
35
+ `Fetching${lastModified ? " modified" : ""} data${
36
+ lastModified ? ` starting at ${lastModified}` : ""
37
+ }${superuserToken ? " as superuser" : ""}`
40
38
  );
41
39
 
42
40
  // Prepare pagination variables
@@ -64,7 +62,7 @@ export async function loadEntries(
64
62
  // If the collection is locked, an superuser token is required
65
63
  if (collectionRequest.status === 403) {
66
64
  throw new Error(
67
- `(${options.collectionName}) The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
65
+ `The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
68
66
  );
69
67
  }
70
68
 
@@ -72,7 +70,7 @@ export async function loadEntries(
72
70
  const reason = await collectionRequest
73
71
  .json()
74
72
  .then((data) => data.message);
75
- const errorMessage = `(${options.collectionName}) Fetching data failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
73
+ const errorMessage = `Fetching data failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
76
74
  throw new Error(errorMessage);
77
75
  }
78
76
 
@@ -92,8 +90,6 @@ export async function loadEntries(
92
90
 
93
91
  // Log the number of fetched entries
94
92
  context.logger.info(
95
- `(${options.collectionName}) Fetched ${entries}${
96
- lastModified ? " changed" : ""
97
- } entries.`
93
+ `Fetched ${entries}${lastModified ? " changed" : ""} entries.`
98
94
  );
99
95
  }
@@ -1,7 +1,9 @@
1
1
  import type { Loader, LoaderContext } from "astro/loaders";
2
+ import type { ZodSchema } from "astro/zod";
2
3
  import packageJson from "./../package.json";
3
4
  import { cleanupEntries } from "./cleanup-entries";
4
5
  import { generateSchema } from "./generate-schema";
6
+ import { handleRealtimeUpdates } from "./handle-realtime-updates";
5
7
  import { loadEntries } from "./load-entries";
6
8
  import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
7
9
  import { getSuperuserToken } from "./utils/get-superuser-token";
@@ -16,6 +18,8 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
16
18
  return {
17
19
  name: "pocketbase-loader",
18
20
  load: async (context: LoaderContext): Promise<void> => {
21
+ context.logger.label = `pocketbase-loader:${options.collectionName}`;
22
+
19
23
  // Check if the collection should be refreshed.
20
24
  const refresh = shouldRefresh(
21
25
  context.refreshContextData,
@@ -25,6 +29,12 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
25
29
  return;
26
30
  }
27
31
 
32
+ // Handle realtime updates
33
+ const handled = await handleRealtimeUpdates(context, options);
34
+ if (handled) {
35
+ return;
36
+ }
37
+
28
38
  // Get the date of the last fetch to only update changed entries.
29
39
  let lastModified = context.meta.get("last-modified");
30
40
 
@@ -45,7 +55,7 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
45
55
  // Disable incremental builds if no updated field is provided
46
56
  if (!options.updatedField) {
47
57
  context.logger.info(
48
- `(${options.collectionName}) No "updatedField" was provided. Incremental builds are disabled.`
58
+ `No "updatedField" was provided. Incremental builds are disabled.`
49
59
  );
50
60
  lastModified = undefined;
51
61
  }
@@ -76,7 +86,7 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
76
86
 
77
87
  context.meta.set("version", packageJson.version);
78
88
  },
79
- schema: async () => {
89
+ schema: async (): Promise<ZodSchema> => {
80
90
  // Generate the schema for the collection according to the API
81
91
  return await generateSchema(options);
82
92
  }
@@ -0,0 +1,34 @@
1
+ import { z } from "astro/zod";
2
+
3
+ /**
4
+ * Schema for realtime data received from PocketBase.
5
+ */
6
+ const realtimeDataSchema = z.object({
7
+ action: z.union([
8
+ z.literal("create"),
9
+ z.literal("update"),
10
+ z.literal("delete")
11
+ ]),
12
+ record: z.object({
13
+ id: z.string(),
14
+ collectionName: z.string(),
15
+ collectionId: z.string()
16
+ })
17
+ });
18
+
19
+ /**
20
+ * Type for realtime data received from PocketBase.
21
+ */
22
+ export type RealtimeData = z.infer<typeof realtimeDataSchema>;
23
+
24
+ /**
25
+ * Checks if the given data is realtime data received from PocketBase.
26
+ */
27
+ export function isRealtimeData(data: unknown): data is RealtimeData {
28
+ try {
29
+ realtimeDataSchema.parse(data);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
@@ -106,7 +106,7 @@ export function parseSchema(
106
106
  function parseSingleOrMultipleValues(
107
107
  field: PocketBaseSchemaEntry,
108
108
  type: z.ZodType
109
- ) {
109
+ ): z.ZodType {
110
110
  // If the select allows multiple values, create an array of the enum
111
111
  if (field.maxSelect === undefined || field.maxSelect === 1) {
112
112
  return type;