astro-loader-pocketbase 2.6.2 → 2.7.0-live-collections.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
@@ -143,12 +143,16 @@ const blog = defineCollection({
143
143
  ...options,
144
144
  superuserCredentials: {
145
145
  email: "<superuser-email>",
146
- password: "<superuser-password>"
146
+ password: "<superuser-password>",
147
+ // or
148
+ impersonateToken: "<superuser-impersonate-token>"
147
149
  }
148
150
  })
149
151
  });
150
152
  ```
151
153
 
154
+ _It's recommended to use an [impersonate token (API token)](https://pocketbase.io/docs/authentication/#api-keys) instead of the email and password, as this is more secure and can be easily revoked._
155
+
152
156
  Under the hood, the loader will use the [PocketBase API](https://pocketbase.io/docs/api-collections/#view-collection) to fetch the schema of your collection and generate types with Zod based on that schema.
153
157
 
154
158
  ### Local schema
@@ -193,18 +197,18 @@ This will remove `undefined` from the type of these fields and mark them as requ
193
197
 
194
198
  ## All options
195
199
 
196
- | Option | Type | Required | Description |
197
- | ---------------------- | ------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
198
- | `url` | `string` | x | The URL of your PocketBase instance. |
199
- | `collectionName` | `string` | x | The name of the collection in your PocketBase instance. |
200
- | `idField` | `string` | | The field in the collection to use as unique id. Defaults to `id`. |
201
- | `contentFields` | `string \| Array<string>` | | The field in the collection to use as content. This can also be an array of fields. |
202
- | `updatedField` | `string` | | The field in the collection that stores the last update date of an entry. This is used for incremental builds. |
203
- | `filter` | `string` | | Custom filter to use when fetching entries. Used to filter the entries by specific conditions. |
204
- | `superuserCredentials` | `{ email: string, password: string }` | | The email and password of the superuser of the PocketBase instance. This is used for automatic type generation. |
205
- | `localSchema` | `string` | | The path to a local schema file. This is used for automatic type generation. |
206
- | `jsonSchemas` | `Record<string, z.ZodSchema>` | | A record of Zod schemas to use for type generation of `json` fields. |
207
- | `improveTypes` | `boolean` | | Whether to improve the types of `number` and `boolean` fields, removing `undefined` from them. |
200
+ | Option | Type | Required | Description |
201
+ | ---------------------- | --------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
202
+ | `url` | `string` | x | The URL of your PocketBase instance. |
203
+ | `collectionName` | `string` | x | The name of the collection in your PocketBase instance. |
204
+ | `idField` | `string` | | The field in the collection to use as unique id. Defaults to `id`. |
205
+ | `contentFields` | `string \| Array<string>` | | The field in the collection to use as content. This can also be an array of fields. |
206
+ | `updatedField` | `string` | | The field in the collection that stores the last update date of an entry. This is used for incremental builds. |
207
+ | `filter` | `string` | | Custom filter to use when fetching entries. Used to filter the entries by specific conditions. |
208
+ | `superuserCredentials` | `{ email: string, password: string } \| { impersonateToken: string }` | | The email and password or impersonate token of a superuser of the PocketBase instance. This is used for automatic type generation. |
209
+ | `localSchema` | `string` | | The path to a local schema file. This is used for automatic type generation. |
210
+ | `jsonSchemas` | `Record<string, z.ZodSchema>` | | A record of Zod schemas to use for type generation of `json` fields. |
211
+ | `improveTypes` | `boolean` | | Whether to improve the types of `number` and `boolean` fields, removing `undefined` from them. |
208
212
 
209
213
  ## Special cases
210
214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-loader-pocketbase",
3
- "version": "2.6.2",
3
+ "version": "2.7.0-live-collections.1",
4
4
  "description": "A content loader for Astro that uses the PocketBase API",
5
5
  "keywords": [
6
6
  "astro",
@@ -39,26 +39,27 @@
39
39
  "test:e2e:watch": "vitest watch $(find test -name '*.e2e-spec.ts')",
40
40
  "test:unit": "vitest run $(find test -name '*.spec.ts')",
41
41
  "test:unit:watch": "vitest watch $(find test -name '*.spec.ts')",
42
- "test:watch": "vitest watch"
42
+ "test:watch": "vitest watch",
43
+ "typecheck": "npx tsc --noEmit"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@commitlint/cli": "^19.8.1",
46
47
  "@commitlint/config-conventional": "^19.8.1",
47
- "@eslint/js": "^9.30.0",
48
- "@stylistic/eslint-plugin": "^5.0.0",
48
+ "@eslint/js": "^9.30.1",
49
+ "@stylistic/eslint-plugin": "^5.1.0",
49
50
  "@types/node": "^22.14.1",
50
51
  "@vitest/coverage-v8": "^3.2.4",
51
- "astro": "^5.10.1",
52
- "eslint": "^9.30.0",
52
+ "astro": "^5.11.0",
53
+ "eslint": "^9.30.1",
53
54
  "eslint-config-prettier": "^10.1.5",
54
- "globals": "^16.2.0",
55
+ "globals": "^16.3.0",
55
56
  "husky": "^9.1.7",
56
57
  "lint-staged": "^16.1.2",
57
58
  "prettier": "^3.6.2",
58
59
  "prettier-plugin-organize-imports": "^4.1.0",
59
- "prettier-plugin-packagejson": "^2.5.17",
60
+ "prettier-plugin-packagejson": "^2.5.18",
60
61
  "typescript": "^5.8.3",
61
- "typescript-eslint": "^8.35.0",
62
+ "typescript-eslint": "^8.35.1",
62
63
  "vitest": "^3.2.4"
63
64
  },
64
65
  "peerDependencies": {
@@ -66,7 +67,6 @@
66
67
  },
67
68
  "packageManager": "npm@11.4.2",
68
69
  "publishConfig": {
69
- "access": "public",
70
- "provenance": true
70
+ "access": "public"
71
71
  }
72
72
  }
package/src/index.ts CHANGED
@@ -1,5 +1,14 @@
1
- import { pocketbaseLoader } from "./pocketbase-loader";
2
- import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
1
+ import {
2
+ experimentalPocketbaseLiveLoader,
3
+ pocketbaseLoader
4
+ } from "./pocketbase-loader";
5
+ import type {
6
+ ExperimentalPocketBaseLiveLoaderOptions,
7
+ PocketBaseLoaderOptions
8
+ } from "./types/pocketbase-loader-options.type";
3
9
 
4
- export { pocketbaseLoader };
5
- export type { PocketBaseLoaderOptions };
10
+ export { experimentalPocketbaseLiveLoader, pocketbaseLoader };
11
+ export type {
12
+ ExperimentalPocketBaseLiveLoaderOptions,
13
+ PocketBaseLoaderOptions
14
+ };
@@ -56,9 +56,16 @@ export async function cleanupEntries(
56
56
  if (!collectionRequest.ok) {
57
57
  // If the collection is locked, an superuser token is required
58
58
  if (collectionRequest.status === 403) {
59
- context.logger.error(
60
- `The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
61
- );
59
+ if (
60
+ options.superuserCredentials &&
61
+ "impersonateToken" in options.superuserCredentials
62
+ ) {
63
+ context.logger.error("The given impersonate token is not valid.");
64
+ } else {
65
+ context.logger.error(
66
+ "The collection is not accessible without superuser rights. Please provide superuser credentials in the config."
67
+ );
68
+ }
62
69
  } else {
63
70
  const reason = await collectionRequest
64
71
  .json()
@@ -0,0 +1,99 @@
1
+ import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
2
+ import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
3
+
4
+ export async function fetchCollection<TEntry extends PocketBaseEntry>(
5
+ options: Pick<
6
+ PocketBaseLoaderOptions,
7
+ | "collectionName"
8
+ | "url"
9
+ | "updatedField"
10
+ | "filter"
11
+ | "superuserCredentials"
12
+ >,
13
+ chunkLoaded: (entries: Array<TEntry>) => Promise<void>,
14
+ token: string | undefined,
15
+ lastModified: string | undefined
16
+ ): Promise<void> {
17
+ // Build the URL for the collections endpoint
18
+ const collectionUrl = new URL(
19
+ `api/collections/${options.collectionName}/records`,
20
+ options.url
21
+ ).href;
22
+
23
+ // Create the headers for the request to append the token (if available)
24
+ const collectionHeaders = new Headers();
25
+ if (token) {
26
+ collectionHeaders.set("Authorization", token);
27
+ }
28
+
29
+ // Prepare pagination variables
30
+ let page = 0;
31
+ let totalPages = 0;
32
+
33
+ // Fetch all (modified) entries
34
+ do {
35
+ // Build search parameters
36
+ const searchParams = new URLSearchParams({
37
+ page: `${++page}`,
38
+ perPage: "100"
39
+ });
40
+
41
+ const filters = [];
42
+ if (lastModified && options.updatedField) {
43
+ // If `lastModified` is set, only fetch entries that have been modified since the last fetch
44
+ filters.push(`(${options.updatedField}>"${lastModified}")`);
45
+ // Sort by the updated field and id
46
+ searchParams.set("sort", `-${options.updatedField},id`);
47
+ }
48
+ if (options.filter) {
49
+ filters.push(`(${options.filter})`);
50
+ }
51
+
52
+ // Add filters to search parameters
53
+ if (filters.length > 0) {
54
+ searchParams.set("filter", filters.join("&&"));
55
+ }
56
+
57
+ // Fetch entries from the collection
58
+ const collectionRequest = await fetch(
59
+ `${collectionUrl}?${searchParams.toString()}`,
60
+ {
61
+ headers: collectionHeaders
62
+ }
63
+ );
64
+
65
+ // If the request was not successful, print the error message and return
66
+ if (!collectionRequest.ok) {
67
+ // If the collection is locked, an superuser token is required
68
+ if (collectionRequest.status === 403) {
69
+ if (
70
+ options.superuserCredentials &&
71
+ "impersonateToken" in options.superuserCredentials
72
+ ) {
73
+ throw new Error("The given impersonate token is not valid.");
74
+ } else {
75
+ throw new Error(
76
+ "The collection is not accessible without superuser rights. Please provide superuser credentials in the config."
77
+ );
78
+ }
79
+ }
80
+
81
+ // Get the reason for the error
82
+ const reason = await collectionRequest
83
+ .json()
84
+ .then((data) => data.message);
85
+ const errorMessage = `Fetching data failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
86
+ throw new Error(errorMessage);
87
+ }
88
+
89
+ // Get the data from the response
90
+ const response = await collectionRequest.json();
91
+
92
+ // Return current chunk
93
+ await chunkLoaded(response.items);
94
+
95
+ // Update the page and total pages
96
+ page = response.page;
97
+ totalPages = response.totalPages;
98
+ } while (page < totalPages);
99
+ }
@@ -0,0 +1,52 @@
1
+ import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
2
+ import type { ExperimentalPocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
3
+
4
+ export async function fetchEntry<TEntry extends PocketBaseEntry>(
5
+ id: string,
6
+ options: ExperimentalPocketBaseLiveLoaderOptions,
7
+ token: string | undefined
8
+ ): Promise<TEntry> {
9
+ // Build the URL for the entry endpoint
10
+ const entryUrl = new URL(
11
+ `api/collections/${options.collectionName}/records/${id}`,
12
+ options.url
13
+ ).href;
14
+
15
+ // Create the headers for the request to append the token (if available)
16
+ const entryHeaders = new Headers();
17
+ if (token) {
18
+ entryHeaders.set("Authorization", token);
19
+ }
20
+
21
+ // Fetch the entry from the collection
22
+ const entryRequest = await fetch(entryUrl, {
23
+ headers: entryHeaders
24
+ });
25
+
26
+ // If the request was not successful, return an error
27
+ if (!entryRequest.ok) {
28
+ // If the entry is locked, a superuser token is required
29
+ if (entryRequest.status === 403) {
30
+ if (
31
+ options.superuserCredentials &&
32
+ "impersonateToken" in options.superuserCredentials
33
+ ) {
34
+ throw new Error("The given impersonate token is not valid.");
35
+ } else {
36
+ throw new Error(
37
+ "The entry is not accessible without superuser rights. Please provide superuser credentials in the config."
38
+ );
39
+ }
40
+ }
41
+
42
+ // Get the reason for the error
43
+ const reason = await entryRequest.json().then((data) => data.message);
44
+ const errorMessage = `Fetching entry "${id}" from collection "${options.collectionName}" failed.\nReason: ${reason}`;
45
+ throw new Error(errorMessage);
46
+ }
47
+
48
+ // Get the data from the response
49
+ const response = await entryRequest.json();
50
+
51
+ return response;
52
+ }
@@ -0,0 +1,29 @@
1
+ import type { LiveDataCollection, LiveDataEntry } from "astro";
2
+ import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
3
+ import type { ExperimentalPocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
4
+ import { fetchCollection } from "./fetch-collection";
5
+ import { parseLiveEntry } from "./parse-live-entry";
6
+
7
+ export async function liveCollectionLoader<TEntry extends PocketBaseEntry>(
8
+ options: ExperimentalPocketBaseLiveLoaderOptions,
9
+ token: string | undefined
10
+ ): Promise<LiveDataCollection<TEntry> | { error: Error }> {
11
+ const entries: Array<LiveDataEntry<TEntry>> = [];
12
+
13
+ try {
14
+ await fetchCollection<TEntry>(
15
+ options,
16
+ async (chunk) => {
17
+ entries.push(...chunk.map((entry) => parseLiveEntry(entry, options)));
18
+ },
19
+ token,
20
+ undefined
21
+ );
22
+ } catch (error) {
23
+ return { error: error as Error };
24
+ }
25
+
26
+ return {
27
+ entries
28
+ };
29
+ }
@@ -0,0 +1,18 @@
1
+ import type { LiveDataEntry } from "astro";
2
+ import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
3
+ import type { ExperimentalPocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
4
+ import { fetchEntry } from "./fetch-entry";
5
+ import { parseLiveEntry } from "./parse-live-entry";
6
+
7
+ export async function liveEntryLoader<TEntry extends PocketBaseEntry>(
8
+ id: string,
9
+ options: ExperimentalPocketBaseLiveLoaderOptions,
10
+ token: string | undefined
11
+ ): Promise<LiveDataEntry<TEntry> | { error: Error }> {
12
+ try {
13
+ const entry = await fetchEntry<TEntry>(id, options, token);
14
+ return parseLiveEntry(entry, options);
15
+ } catch (error) {
16
+ return { error: error as Error };
17
+ }
18
+ }
@@ -1,5 +1,6 @@
1
1
  import type { LoaderContext } from "astro/loaders";
2
2
  import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
3
+ import { fetchCollection } from "./fetch-collection";
3
4
  import { parseEntry } from "./parse-entry";
4
5
 
5
6
  /**
@@ -9,8 +10,6 @@ import { parseEntry } from "./parse-entry";
9
10
  * @param context Context of the loader.
10
11
  * @param superuserToken Superuser token to access all resources.
11
12
  * @param lastModified Date of the last fetch to only update changed entries.
12
- *
13
- * @returns `true` if the collection has an updated column, `false` otherwise.
14
13
  */
15
14
  export async function loadEntries(
16
15
  options: PocketBaseLoaderOptions,
@@ -18,18 +17,6 @@ export async function loadEntries(
18
17
  superuserToken: string | undefined,
19
18
  lastModified: string | undefined
20
19
  ): Promise<void> {
21
- // Build the URL for the collections endpoint
22
- const collectionUrl = new URL(
23
- `api/collections/${options.collectionName}/records`,
24
- options.url
25
- ).href;
26
-
27
- // Create the headers for the request to append the superuser token (if available)
28
- const collectionHeaders = new Headers();
29
- if (superuserToken) {
30
- collectionHeaders.set("Authorization", superuserToken);
31
- }
32
-
33
20
  // Log the fetching of the entries
34
21
  context.logger.info(
35
22
  `Fetching${lastModified ? " modified" : ""} data${
@@ -37,80 +24,27 @@ export async function loadEntries(
37
24
  }${superuserToken ? " as superuser" : ""}`
38
25
  );
39
26
 
40
- // Prepare pagination variables
41
- let page = 0;
42
- let totalPages = 0;
43
- let entries = 0;
44
-
45
- // Fetch all (modified) entries
46
- do {
47
- // Build search parameters
48
- const searchParams = new URLSearchParams({
49
- page: `${++page}`,
50
- perPage: "100"
51
- });
52
-
53
- const filters = [];
54
- if (lastModified && options.updatedField) {
55
- // If `lastModified` is set, only fetch entries that have been modified since the last fetch
56
- filters.push(`(${options.updatedField}>"${lastModified}")`);
57
- // Sort by the updated field and id
58
- searchParams.set("sort", `-${options.updatedField},id`);
59
- }
60
- if (options.filter) {
61
- filters.push(`(${options.filter})`);
62
- }
63
-
64
- // Add filters to search parameters
65
- if (filters.length > 0) {
66
- searchParams.set("filter", filters.join("&&"));
67
- }
68
-
69
- // Fetch entries from the collection
70
- const collectionRequest = await fetch(
71
- `${collectionUrl}?${searchParams.toString()}`,
72
- {
73
- headers: collectionHeaders
27
+ let numEntries = 0;
28
+ await fetchCollection(
29
+ options,
30
+ async (entries) => {
31
+ // Parse and store the entries
32
+ for (const entry of entries) {
33
+ await parseEntry(entry, context, options);
74
34
  }
75
- );
76
-
77
- // If the request was not successful, print the error message and return
78
- if (!collectionRequest.ok) {
79
- // If the collection is locked, an superuser token is required
80
- if (collectionRequest.status === 403) {
81
- throw new Error(
82
- `The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
83
- );
84
- }
85
-
86
- // Get the reason for the error
87
- const reason = await collectionRequest
88
- .json()
89
- .then((data) => data.message);
90
- const errorMessage = `Fetching data failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
91
- throw new Error(errorMessage);
92
- }
93
35
 
94
- // Get the data from the response
95
- const response = await collectionRequest.json();
96
-
97
- // Parse and store the entries
98
- for (const entry of response.items) {
99
- await parseEntry(entry, context, options);
100
- }
101
-
102
- // Update the page and total pages
103
- page = response.page;
104
- totalPages = response.totalPages;
105
- entries += response.items.length;
106
- } while (page < totalPages);
36
+ numEntries += entries.length;
37
+ },
38
+ superuserToken,
39
+ lastModified
40
+ );
107
41
 
108
42
  // Log the number of fetched entries
109
43
  if (lastModified) {
110
44
  context.logger.info(
111
- `Updated ${entries}/${context.store.keys().length} entries.`
45
+ `Updated ${numEntries}/${context.store.keys().length} entries.`
112
46
  );
113
47
  } else {
114
- context.logger.info(`Fetched ${entries} entries.`);
48
+ context.logger.info(`Fetched ${numEntries} entries.`);
115
49
  }
116
50
  }
@@ -1,15 +1,18 @@
1
1
  import type { LoaderContext } from "astro/loaders";
2
2
  import packageJson from "../../package.json";
3
3
  import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
4
- import { getSuperuserToken } from "../utils/get-superuser-token";
5
4
  import { shouldRefresh } from "../utils/should-refresh";
6
5
  import { cleanupEntries } from "./cleanup-entries";
7
6
  import { handleRealtimeUpdates } from "./handle-realtime-updates";
8
7
  import { loadEntries } from "./load-entries";
9
8
 
9
+ /**
10
+ * Load entries from a PocketBase collection.
11
+ */
10
12
  export async function loader(
11
13
  context: LoaderContext,
12
- options: PocketBaseLoaderOptions
14
+ options: PocketBaseLoaderOptions,
15
+ token: string | undefined
13
16
  ): Promise<void> {
14
17
  context.logger.label = `pocketbase-loader:${options.collectionName}`;
15
18
 
@@ -59,16 +62,6 @@ export async function loader(
59
62
  lastModified = undefined;
60
63
  }
61
64
 
62
- // Try to get a superuser token to access all resources.
63
- let token: string | undefined;
64
- if (options.superuserCredentials) {
65
- token = await getSuperuserToken(
66
- options.url,
67
- options.superuserCredentials,
68
- context.logger
69
- );
70
- }
71
-
72
65
  if (context.store.keys().length > 0) {
73
66
  // Cleanup entries that are no longer in the collection
74
67
  await cleanupEntries(options, context, token);
@@ -0,0 +1,49 @@
1
+ import type { LiveDataEntry } from "astro";
2
+ import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
3
+ import type { ExperimentalPocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
4
+
5
+ export function parseLiveEntry<TEntry extends PocketBaseEntry>(
6
+ entry: TEntry,
7
+ options: ExperimentalPocketBaseLiveLoaderOptions
8
+ ): LiveDataEntry<TEntry> {
9
+ let updated: string | undefined;
10
+ if (options.updatedField) {
11
+ // If an updated field is provided, use it to determine the last modified date
12
+ updated = `${entry[options.updatedField]}`;
13
+ }
14
+
15
+ if (!options.contentFields) {
16
+ return {
17
+ id: entry.id,
18
+ data: entry,
19
+ cacheHint: {
20
+ tags: [`${options.collectionName}-${entry.id}`],
21
+ lastModified: updated ? new Date(updated) : undefined
22
+ }
23
+ };
24
+ }
25
+
26
+ let content: string;
27
+ if (typeof options.contentFields === "string") {
28
+ // If a single content field is provided, use it directly
29
+ content = `${entry[options.contentFields]}`;
30
+ } else {
31
+ // If multiple content fields are provided, concatenate them with `<section>` tags
32
+ content = options.contentFields
33
+ .map((field) => `<section>${entry[field]}</section>`)
34
+ .join("");
35
+ }
36
+
37
+ return {
38
+ id: entry.id,
39
+ data: entry,
40
+ // @ts-expect-error - Docs say this is possible
41
+ rendered: {
42
+ html: content
43
+ },
44
+ cacheHint: {
45
+ tags: [`${options.collectionName}-${entry.id}`],
46
+ lastModified: updated ? new Date(updated) : undefined
47
+ }
48
+ };
49
+ }
@@ -1,7 +1,16 @@
1
- import type { Loader } from "astro/loaders";
1
+ import type { LiveDataCollection, LiveDataEntry } from "astro";
2
+ import type { LiveLoader, Loader } from "astro/loaders";
3
+ import type { ZodSchema } from "astro/zod";
4
+ import { liveCollectionLoader } from "./loader/live-collection-loader";
5
+ import { liveEntryLoader } from "./loader/live-entry-loader";
2
6
  import { loader } from "./loader/loader";
3
7
  import { generateSchema } from "./schema/generate-schema";
4
- import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
8
+ import type { PocketBaseEntry } from "./types/pocketbase-entry.type";
9
+ import type {
10
+ ExperimentalPocketBaseLiveLoaderOptions,
11
+ PocketBaseLoaderOptions
12
+ } from "./types/pocketbase-loader-options.type";
13
+ import { getSuperuserToken } from "./utils/get-superuser-token";
5
14
 
6
15
  /**
7
16
  * Loader for collections stored in PocketBase.
@@ -9,11 +18,97 @@ import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.
9
18
  * @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
10
19
  */
11
20
  export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
21
+ let tokenPromise: Promise<string | undefined>;
22
+ if (options.superuserCredentials) {
23
+ if ("impersonateToken" in options.superuserCredentials) {
24
+ // Impersonate token provided, so use it directly.
25
+ tokenPromise = Promise.resolve(
26
+ options.superuserCredentials.impersonateToken
27
+ );
28
+ } else {
29
+ // Email and password provided, so get a temporary superuser token.
30
+ tokenPromise = getSuperuserToken(
31
+ options.url,
32
+ options.superuserCredentials
33
+ );
34
+ }
35
+ } else {
36
+ // No credentials provided, so no token can be used.
37
+ tokenPromise = Promise.resolve(undefined);
38
+ }
39
+
12
40
  return {
13
41
  name: "pocketbase-loader",
14
- // Load the entries from the collection
15
- load: async (context) => loader(context, options),
16
- // Generate the schema for the collection according to the API
17
- schema: async () => await generateSchema(options)
42
+ load: async (context): Promise<void> => {
43
+ if (options.experimental?.liveTypesOnly) {
44
+ context.logger.label = `pocketbase-loader:${options.collectionName}`;
45
+ context.logger.info(
46
+ "Experimental live types only mode enabled. No data will be loaded, only types will be generated."
47
+ );
48
+ return;
49
+ }
50
+
51
+ const token = await tokenPromise;
52
+
53
+ // Load the entries from the collection
54
+ await loader(context, options, token);
55
+ },
56
+ schema: async (): Promise<ZodSchema> => {
57
+ const token = await tokenPromise;
58
+
59
+ // Generate the schema for the collection according to the API
60
+ return await generateSchema(options, token);
61
+ }
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Live loader for collections stored in PocketBase.
67
+ * This loader is currently experimental and may change in any future release.
68
+ *
69
+ * @param options Options for the live loader. See {@link ExperimentalPocketBaseLiveLoaderOptions} for more details.
70
+ */
71
+ export function experimentalPocketbaseLiveLoader<
72
+ TEntry extends PocketBaseEntry
73
+ >(
74
+ options: ExperimentalPocketBaseLiveLoaderOptions
75
+ ): LiveLoader<TEntry, { id: string }> {
76
+ let tokenPromise: Promise<string | undefined>;
77
+ if (options.superuserCredentials) {
78
+ if ("impersonateToken" in options.superuserCredentials) {
79
+ // Impersonate token provided, so use it directly.
80
+ tokenPromise = Promise.resolve(
81
+ options.superuserCredentials.impersonateToken
82
+ );
83
+ } else {
84
+ // Email and password provided, so get a temporary superuser token.
85
+ tokenPromise = getSuperuserToken(
86
+ options.url,
87
+ options.superuserCredentials
88
+ );
89
+ }
90
+ } else {
91
+ // No credentials provided, so no token can be used.
92
+ tokenPromise = Promise.resolve(undefined);
93
+ }
94
+
95
+ return {
96
+ name: "pocketbase-live-loader",
97
+ loadCollection: async (): Promise<
98
+ LiveDataCollection<TEntry> | { error: Error }
99
+ > => {
100
+ const token = await tokenPromise;
101
+
102
+ // Load entries from the collection
103
+ return await liveCollectionLoader(options, token);
104
+ },
105
+ loadEntry: async ({
106
+ filter
107
+ }): Promise<LiveDataEntry<TEntry> | { error: Error }> => {
108
+ const token = await tokenPromise;
109
+
110
+ // Load a single entry from the collection
111
+ return await liveEntryLoader(filter.id, options, token);
112
+ }
18
113
  };
19
114
  }
@@ -28,14 +28,18 @@ const VALID_ID_TYPES = ["text", "number", "email", "url", "date"];
28
28
  * If a path to a local schema file is provided, the schema is read from the file.
29
29
  *
30
30
  * @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
31
+ * @param token The superuser token to authenticate the request.
31
32
  */
32
33
  export async function generateSchema(
33
- options: PocketBaseLoaderOptions
34
+ options: PocketBaseLoaderOptions,
35
+ token: string | undefined
34
36
  ): Promise<ZodSchema> {
35
37
  let collection: PocketBaseCollection | undefined;
36
38
 
37
- // Try to get the schema directly from the PocketBase instance
38
- collection = await getRemoteSchema(options);
39
+ if (token) {
40
+ // Try to get the schema directly from the PocketBase instance
41
+ collection = await getRemoteSchema(options, token);
42
+ }
39
43
 
40
44
  const hasSuperuserRights = !!collection || !!options.superuserCredentials;
41
45
 
@@ -61,7 +65,8 @@ export async function generateSchema(
61
65
  collection,
62
66
  options.jsonSchemas,
63
67
  hasSuperuserRights,
64
- options.improveTypes ?? false
68
+ options.improveTypes ?? false,
69
+ options.experimental?.liveTypesOnly ?? false
65
70
  );
66
71
 
67
72
  // Check if custom id field is present
@@ -1,30 +1,16 @@
1
1
  import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
2
2
  import type { PocketBaseCollection } from "../types/pocketbase-schema.type";
3
- import { getSuperuserToken } from "../utils/get-superuser-token";
4
3
 
5
4
  /**
6
5
  * Fetches the schema for the specified collection from the PocketBase instance.
7
6
  *
8
7
  * @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
8
+ * @param token The superuser token to authenticate the request.
9
9
  */
10
10
  export async function getRemoteSchema(
11
- options: PocketBaseLoaderOptions
11
+ options: PocketBaseLoaderOptions,
12
+ token: string
12
13
  ): Promise<PocketBaseCollection | undefined> {
13
- if (!options.superuserCredentials) {
14
- return undefined;
15
- }
16
-
17
- // Get a superuser token
18
- const token = await getSuperuserToken(
19
- options.url,
20
- options.superuserCredentials
21
- );
22
-
23
- // If the token is invalid try another method
24
- if (!token) {
25
- return undefined;
26
- }
27
-
28
14
  // Build URL and headers for the schema request
29
15
  const schemaUrl = new URL(
30
16
  `api/collections/${options.collectionName}`,
@@ -8,7 +8,8 @@ export function parseSchema(
8
8
  collection: PocketBaseCollection,
9
9
  customSchemas: Record<string, z.ZodType> | undefined,
10
10
  hasSuperuserRights: boolean,
11
- improveTypes: boolean
11
+ improveTypes: boolean,
12
+ experimentalLiveTypesOnly?: boolean
12
13
  ): Record<string, z.ZodType> {
13
14
  // Prepare the schemas fields
14
15
  const fields: Record<string, z.ZodType> = {};
@@ -32,6 +33,11 @@ export function parseSchema(
32
33
  break;
33
34
  case "date":
34
35
  case "autodate":
36
+ if (experimentalLiveTypesOnly) {
37
+ // If experimental live types only mode is enabled, treat dates as strings
38
+ fieldType = z.string();
39
+ break;
40
+ }
35
41
  // Coerce and parse the value as a date
36
42
  fieldType = z.coerce.date();
37
43
  break;
@@ -58,16 +58,24 @@ export interface PocketBaseLoaderOptions {
58
58
  * Credentials of a superuser to get full access to the PocketBase instance.
59
59
  * This is required to get automatic type generation without a local schema, to access all resources even if they are not public and to fetch content of hidden fields.
60
60
  */
61
- superuserCredentials?: {
62
- /**
63
- * Email of the superuser.
64
- */
65
- email: string;
66
- /**
67
- * Password of the superuser.
68
- */
69
- password: string;
70
- };
61
+ superuserCredentials?:
62
+ | {
63
+ /**
64
+ * Email of the superuser.
65
+ */
66
+ email: string;
67
+ /**
68
+ * Password of the superuser.
69
+ */
70
+ password: string;
71
+ }
72
+ | {
73
+ /**
74
+ * Impersonate auth token of the superuser.
75
+ * This token will take precedence over the email and password.
76
+ */
77
+ impersonateToken: string;
78
+ };
71
79
  /**
72
80
  * File path to the local schema file.
73
81
  * This file will be used to generate the schema for the collection.
@@ -90,4 +98,27 @@ export interface PocketBaseLoaderOptions {
90
98
  * The PocketBase API does always return at least `0` or `false` as the default values, even though the fields are not marked as required in the schema.
91
99
  */
92
100
  improveTypes?: boolean;
101
+ /**
102
+ * Experimental options for the loader.
103
+ */
104
+ experimental?: {
105
+ /**
106
+ * Whether to only create types for the live loader.
107
+ * This will not load any data, but only generate types that can be used with the live loader.
108
+ */
109
+ liveTypesOnly?: boolean;
110
+ };
93
111
  }
112
+
113
+ /**
114
+ * Options for the PocketBase live loader.
115
+ */
116
+ export type ExperimentalPocketBaseLiveLoaderOptions = Pick<
117
+ PocketBaseLoaderOptions,
118
+ | "url"
119
+ | "collectionName"
120
+ | "contentFields"
121
+ | "updatedField"
122
+ | "filter"
123
+ | "superuserCredentials"
124
+ >;