astro-loader-pocketbase 0.6.0-pocketbase-rc.3 → 1.0.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/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # astro-loader-pocketbase
2
2
 
3
- <!-- ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/pawcoding/astro-loader-pocketbase/release.yaml?style=flat-square) -->
4
-
3
+ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/pawcoding/astro-loader-pocketbase/release.yaml?style=flat-square)
5
4
  [![NPM Version](https://img.shields.io/npm/v/astro-loader-pocketbase?style=flat-square)](https://www.npmjs.com/package/astro-loader-pocketbase)
6
5
  [![NPM Downloads](https://img.shields.io/npm/dw/astro-loader-pocketbase?style=flat-square)](https://www.npmjs.com/package/astro-loader-pocketbase)
7
6
  [![GitHub License](https://img.shields.io/github/license/pawcoding/astro-loader-pocketbase?style=flat-square)](https://github.com/pawcoding/astro-loader-pocketbase/blob/master/LICENSE)
@@ -9,20 +8,14 @@
9
8
 
10
9
  This package is a simple loader to load data from a PocketBase database into Astro using the [Astro Loader API](https://5-0-0-beta.docs.astro.build/en/reference/loader-reference/) introduced in Astro 5.
11
10
 
12
- > [!WARNING]
13
- > This package is still under development.
14
- > It will have a first stable release when Astro 5 is released.
15
- > Until then, **breaking changes can occur at any time**.
16
-
17
11
  > [!TIP]
18
12
  > If you want to see the PocketBase data directly in your Astro toolbar, try the [`astro-integration-pocketbase`](https://github.com/pawcoding/astro-integration-pocketbase).
19
13
 
20
14
  ## Compatibility
21
15
 
22
- | Loader version | Astro version | PocketBase version |
23
- | ---------------------------------------------------------------------------- | ------------- | ------------------ |
24
- | >= 0.6.0 | >= 5.0.0-beta | >= 0.23.0 |
25
- | <= [0.5.0](https://github.com/pawcoding/astro-loader-pocketbase/tree/v0.5.0) | >= 5.0.0-beta | < 0.23.0 |
16
+ | Loader | Astro | PocketBase |
17
+ | ------ | ----- | ---------- |
18
+ | 1.0.0 | 5.0.0 | <= 0.22.0 |
26
19
 
27
20
  ## Basic usage
28
21
 
@@ -42,30 +35,12 @@ const blog = defineCollection({
42
35
  export const collections = { blog };
43
36
  ```
44
37
 
38
+ By default, the loader will only fetch entries that have been modified since the last build.
45
39
  Remember that due to the nature [Astros Content Layer lifecycle](https://astro.build/blog/content-layer-deep-dive#content-layer-lifecycle), the loader will **only fetch entries at build time**, even when using on-demand rendering.
46
40
  If you want to update your deployed site with new entries, you need to rebuild it.
47
41
 
48
42
  <sub>When running the dev server, you can trigger a reload by using `s + enter`.</sub>
49
43
 
50
- ## Incremental builds
51
-
52
- Since PocketBase 0.23.0, the `updated` field is not mandatory anymore.
53
- This means that the loader can't automatically detect when an entry has been modified.
54
- To enable incremental builds, you need to provide the name of a field in your collection that stores the last update date of an entry.
55
-
56
- ```ts
57
- const blog = defineCollection({
58
- loader: pocketbaseLoader({
59
- ...options,
60
- updatedField: "<field-in-collection>"
61
- })
62
- });
63
- ```
64
-
65
- When this field is provided, the loader will only fetch entries that have been modified since the last build.
66
- Ideally, this field should be of type `autodate` and have the value "Update" or "Create/Update" in the PocketBase dashboard.
67
- This ensures that the field is automatically updated when an entry is modified.
68
-
69
44
  ## Entries
70
45
 
71
46
  After generating the schema (see below), the loader will automatically parse the content of the entries (e.g. transform ISO dates to `Date` objects, coerce numbers, etc.).
@@ -79,7 +54,7 @@ This content will then be used when calling the `render` function of [Astros con
79
54
  const blog = defineCollection({
80
55
  loader: pocketbaseLoader({
81
56
  ...options,
82
- contentFields: "<field-in-collection>"
57
+ content: "<field-in-collection>"
83
58
  })
84
59
  });
85
60
  ```
@@ -122,16 +97,14 @@ These types can be generated in two ways:
122
97
 
123
98
  ### Remote schema
124
99
 
125
- To use the live remote schema, you need to provide superuser credentials for the PocketBase instance.
100
+ To use the lice remote schema, you need to provide the email and password of an admin of the PocketBase instance.
126
101
 
127
102
  ```ts
128
103
  const blog = defineCollection({
129
104
  loader: pocketbaseLoader({
130
105
  ...options,
131
- superuserCredentials: {
132
- email: "<superuser-email>",
133
- password: "<superuser-password>"
134
- }
106
+ adminEmail: "<admin-email>",
107
+ adminPassword: "<admin-password>"
135
108
  })
136
109
  });
137
110
  ```
@@ -140,7 +113,7 @@ Under the hood, the loader will use the [PocketBase API](https://pocketbase.io/d
140
113
 
141
114
  ### Local schema
142
115
 
143
- If you don't want to provide superuser credentials (e.g. in a public repository), you can also provide the schema manually via a JSON file.
116
+ If you don't want to provide the admin credentials (e.g. in a public repository), you can also provide the schema manually via a JSON file.
144
117
 
145
118
  ```ts
146
119
  const blog = defineCollection({
@@ -154,7 +127,7 @@ const blog = defineCollection({
154
127
  In PocketBase you can export the schema of the whole database to a `pb_schema.json` file.
155
128
  If you provide the path to this file, the loader will use this schema to generate the types locally.
156
129
 
157
- When superuser credentials are provided, the loader will **always use the remote schema** since it's more up-to-date.
130
+ When admin credentials are provided, the loader will **always use the remote schema** since it's more up-to-date.
158
131
 
159
132
  ### Manual schema
160
133
 
@@ -163,26 +136,37 @@ This manual schema will **always override the automatic type generation**.
163
136
 
164
137
  ## All options
165
138
 
166
- | Option | Type | Required | Description |
167
- | ---------------------- | ------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
168
- | `url` | `string` | x | The URL of your PocketBase instance. |
169
- | `collectionName` | `string` | x | The name of the collection in your PocketBase instance. |
170
- | `idField` | `string` | | The field in the collection to use as unique id. Defaults to `id`. |
171
- | `contentFields` | `string \| Array<string>` | | The field in the collection to use as content. This can also be an array of fields. |
172
- | `updatedField` | `string` | | The field in the collection that stores the last update date of an entry. This is used for incremental builds. |
173
- | `superuserCredentials` | `{ email: string, password: string }` | | The email and password of the superuser of the PocketBase instance. This is used for automatic type generation. |
174
- | `localSchema` | `string` | | The path to a local schema file. This is used for automatic type generation. |
175
- | `jsonSchemas` | `Record<string, z.ZodSchema>` | | A record of Zod schemas to use for type generation of `json` fields. |
139
+ | Option | Type | Required | Description |
140
+ | ---------------- | ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `url` | `string` | x | The URL of your PocketBase instance. |
142
+ | `collectionName` | `string` | x | The name of the collection in your PocketBase instance. |
143
+ | `content` | `string \| Array<string>` | | The field in the collection to use as content. This can also be an array of fields. |
144
+ | `adminEmail` | `string` | | The email of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
145
+ | `adminPassword` | `string` | | The password of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
146
+ | `id` | `string` | | The field in the collection to use as unique id. Defaults to `id`. |
147
+ | `localSchema` | `string` | | The path to a local schema file. This is used for automatic type generation. |
148
+ | `jsonSchemas` | `Record<string, z.ZodSchema>` | | A record of Zod schemas to use for type generation of `json` fields. |
149
+ | `forceUpdate` | `boolean` | | If set to `true`, the loader will fetch every entry instead of only the ones modified since the last build. |
176
150
 
177
151
  ## Special cases
178
152
 
179
- ### Private collections and hidden fields
153
+ ### Private collections
180
154
 
181
- If you want to access a private collection or hidden fields, you also need to provide superuser credentials.
155
+ If you want to access a private collection, you also need to provide the admin credentials.
182
156
  Otherwise, you need to make the collection public in the PocketBase dashboard.
183
157
 
184
158
  Generally, it's not recommended to use private collections, especially when users should be able to see images or other files stored in the collection.
185
159
 
160
+ ### View collections
161
+
162
+ Out of the box, the loader also supports collections with the type `view`, though with some limitations.
163
+ To enable incremental builds, the loader needs to know when an entry has been modified.
164
+ Normal `base` collections have a `updated` field that is automatically updated when an entry is modified.
165
+ Thus, `view` collections that don't include this field can't be incrementally built but will be fetched every time.
166
+
167
+ You can also alias another field as `updated` (as long as it's a date field) in your view.
168
+ While this is possible, it's not recommended since it can lead to outdated data not being fetched.
169
+
186
170
  ### JSON fields
187
171
 
188
172
  PocketBase can store arbitrary JSON data in a `json` field.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-loader-pocketbase",
3
- "version": "0.6.0-pocketbase-rc.3",
3
+ "version": "1.0.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)",
@@ -24,12 +24,12 @@
24
24
  "@eslint/js": "^9.16.0",
25
25
  "@stylistic/eslint-plugin": "^2.11.0",
26
26
  "@types/node": "^22.10.1",
27
- "astro": "^5.0.0-beta.12",
27
+ "astro": "^5.0.3",
28
28
  "eslint": "^9.16.0",
29
- "globals": "^15.12.0",
29
+ "globals": "^15.13.0",
30
30
  "husky": "^9.1.7",
31
31
  "typescript": "^5.7.2",
32
- "typescript-eslint": "^8.16.0"
32
+ "typescript-eslint": "^8.17.0"
33
33
  },
34
34
  "keywords": [
35
35
  "astro",
@@ -6,12 +6,12 @@ import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.
6
6
  *
7
7
  * @param options Options for the loader.
8
8
  * @param context Context of the loader.
9
- * @param superuserToken Superuser token to access all resources.
9
+ * @param adminToken Admin token to access all resources.
10
10
  */
11
11
  export async function cleanupEntries(
12
12
  options: PocketBaseLoaderOptions,
13
13
  context: LoaderContext,
14
- superuserToken: string | undefined
14
+ adminToken: string | undefined
15
15
  ): Promise<void> {
16
16
  // Build the URL for the collections endpoint
17
17
  const collectionUrl = new URL(
@@ -19,10 +19,10 @@ export async function cleanupEntries(
19
19
  options.url
20
20
  ).href;
21
21
 
22
- // Create the headers for the request to append the superuser token (if available)
22
+ // Create the headers for the request to append the admin token (if available)
23
23
  const collectionHeaders = new Headers();
24
- if (superuserToken) {
25
- collectionHeaders.set("Authorization", superuserToken);
24
+ if (adminToken) {
25
+ collectionHeaders.set("Authorization", adminToken);
26
26
  }
27
27
 
28
28
  // Prepare pagination variables
@@ -42,10 +42,10 @@ export async function cleanupEntries(
42
42
 
43
43
  // If the request was not successful, print the error message and return
44
44
  if (!collectionRequest.ok) {
45
- // If the collection is locked, an superuser token is required
45
+ // If the collection is locked, an admin 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 ${options.collectionName} is not accessible without an admin rights. Please provide an admin email and password 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 from ${options.collectionName} failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
57
57
  context.logger.error(errorMessage);
58
58
  return;
59
59
  }
@@ -86,7 +86,7 @@ export async function cleanupEntries(
86
86
  if (cleanedUp > 0) {
87
87
  // Log the number of cleaned up entries
88
88
  context.logger.info(
89
- `(${options.collectionName}) Cleaned up ${cleanedUp} old entries.`
89
+ `Cleaned up ${cleanedUp} old entries for ${context.collection}`
90
90
  );
91
91
  }
92
92
  }
@@ -13,7 +13,20 @@ import { transformFiles } from "./utils/transform-files";
13
13
  const BASIC_SCHEMA = {
14
14
  id: z.string(),
15
15
  collectionId: z.string().length(15),
16
- collectionName: z.string()
16
+ collectionName: z.string(),
17
+ created: z.coerce.date(),
18
+ updated: z.coerce.date()
19
+ };
20
+
21
+ /**
22
+ * Basic schema for a view in PocketBase.
23
+ */
24
+ const VIEW_SCHEMA = {
25
+ id: z.string(),
26
+ collectionId: z.string().length(15),
27
+ collectionName: z.string(),
28
+ created: z.preprocess((val) => val || undefined, z.optional(z.coerce.date())),
29
+ updated: z.preprocess((val) => val || undefined, z.optional(z.coerce.date()))
17
30
  };
18
31
 
19
32
  /**
@@ -24,7 +37,7 @@ const VALID_ID_TYPES = ["text", "number", "email", "url", "date"];
24
37
  /**
25
38
  * Generate a schema for the collection based on the collection's schema in PocketBase.
26
39
  * By default, a basic schema is returned if no other schema is available.
27
- * If superuser credentials are provided, the schema is fetched from the PocketBase API.
40
+ * If admin credentials are provided, the schema is fetched from the PocketBase API.
28
41
  * If a path to a local schema file is provided, the schema is read from the file.
29
42
  *
30
43
  * @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
@@ -37,8 +50,6 @@ export async function generateSchema(
37
50
  // Try to get the schema directly from the PocketBase instance
38
51
  collection = await getRemoteSchema(options);
39
52
 
40
- const hasSuperuserRights = !!collection || !!options.superuserCredentials;
41
-
42
53
  // If the schema is not available, try to read it from a local schema file
43
54
  if (!collection && options.localSchema) {
44
55
  collection = await readLocalSchema(
@@ -50,34 +61,30 @@ export async function generateSchema(
50
61
  // If the schema is still not available, return the basic schema
51
62
  if (!collection) {
52
63
  console.error(
53
- `No schema available for "${options.collectionName}". Only basic types are available. Please check your configuration and provide a valid schema file or superuser credentials.`
64
+ `No schema available for ${options.collectionName}. Only basic types are available. Please check your configuration and provide a valid schema file or admin credentials.`
54
65
  );
55
- // Return the basic schema since every collection has at least these fields
56
- return z.object(BASIC_SCHEMA);
66
+ // Return the view schema since every collection has at least the view schema
67
+ return z.object(VIEW_SCHEMA);
57
68
  }
58
69
 
59
70
  // Parse the schema
60
- const fields = parseSchema(
61
- collection,
62
- options.jsonSchemas,
63
- hasSuperuserRights
64
- );
71
+ const fields = parseSchema(collection, options.jsonSchemas);
65
72
 
66
73
  // Check if custom id field is present
67
- if (options.idField) {
74
+ if (options.id) {
68
75
  // Find the id field in the schema
69
- const idField = collection.fields.find(
70
- (field) => field.name === options.idField
76
+ const idField = collection.schema.find(
77
+ (field) => field.name === options.id
71
78
  );
72
79
 
73
80
  // Check if the id field is present and of a valid type
74
81
  if (!idField) {
75
82
  console.error(
76
- `The id field "${options.idField}" is not present in the schema of the collection "${options.collectionName}".`
83
+ `The id field "${options.id}" is not present in the schema of the collection "${options.collectionName}".`
77
84
  );
78
85
  } else if (!VALID_ID_TYPES.includes(idField.type)) {
79
86
  console.error(
80
- `The id field "${options.idField}" for collection "${
87
+ `The id field "${options.id}" for collection "${
81
88
  options.collectionName
82
89
  }" is of type "${
83
90
  idField.type
@@ -89,15 +96,12 @@ export async function generateSchema(
89
96
  }
90
97
 
91
98
  // Check if the content field is present
92
- if (
93
- typeof options.contentFields === "string" &&
94
- !fields[options.contentFields]
95
- ) {
99
+ if (typeof options.content === "string" && !fields[options.content]) {
96
100
  console.error(
97
- `The content field "${options.contentFields}" is not present in the schema of the collection "${options.collectionName}".`
101
+ `The content field "${options.content}" is not present in the schema of the collection "${options.collectionName}".`
98
102
  );
99
- } else if (Array.isArray(options.contentFields)) {
100
- for (const field of options.contentFields) {
103
+ } else if (Array.isArray(options.content)) {
104
+ for (const field of options.content) {
101
105
  if (!fields[field]) {
102
106
  console.error(
103
107
  `The content field "${field}" is not present in the schema of the collection "${options.collectionName}".`
@@ -106,39 +110,18 @@ export async function generateSchema(
106
110
  }
107
111
  }
108
112
 
109
- // Check if the updated field is present
110
- if (options.updatedField) {
111
- if (!fields[options.updatedField]) {
112
- console.error(
113
- `The field "${options.updatedField}" is not present in the schema of the collection "${options.collectionName}".\nThis will lead to errors when trying to fetch only updated entries.`
114
- );
115
- } else {
116
- const updatedField = collection.fields.find(
117
- (field) => field.name === options.updatedField
118
- );
119
- if (
120
- !updatedField ||
121
- updatedField.type !== "autodate" ||
122
- !updatedField.onUpdate
123
- ) {
124
- console.warn(
125
- `The field "${options.updatedField}" is not of type "autodate" with the value "Update" or "Create/Update".\nMake sure that the field is automatically updated when the entry is updated!`
126
- );
127
- }
128
- }
129
- }
113
+ // Use the corresponding base schema for the type of collection
114
+ // Auth collections are basically a superset of the basic schema.
115
+ const base = collection.type === "view" ? VIEW_SCHEMA : BASIC_SCHEMA;
130
116
 
131
117
  // Combine the basic schema with the parsed fields
132
118
  const schema = z.object({
133
- ...BASIC_SCHEMA,
119
+ ...base,
134
120
  ...fields
135
121
  });
136
122
 
137
123
  // Get all file fields
138
- const fileFields = collection.fields
139
- .filter((field) => field.type === "file")
140
- // Only show hidden fields if the user has superuser rights
141
- .filter((field) => !field.hidden || hasSuperuserRights);
124
+ const fileFields = collection.schema.filter((field) => field.type === "file");
142
125
 
143
126
  if (fileFields.length === 0) {
144
127
  return schema;
@@ -146,6 +129,7 @@ export async function generateSchema(
146
129
 
147
130
  // Transform file names to file urls
148
131
  return schema.transform((entry) =>
132
+ // @ts-expect-error - `updated` and `created` are already transformed to dates
149
133
  transformFiles(options.url, fileFields, entry)
150
134
  );
151
135
  }
@@ -7,7 +7,7 @@ import { parseEntry } from "./utils/parse-entry";
7
7
  *
8
8
  * @param options Options for the loader.
9
9
  * @param context Context of the loader.
10
- * @param superuserToken Superuser token to access all resources.
10
+ * @param adminToken Admin token to access all resources.
11
11
  * @param lastModified Date of the last fetch to only update changed entries.
12
12
  *
13
13
  * @returns `true` if the collection has an updated column, `false` otherwise.
@@ -15,34 +15,35 @@ import { parseEntry } from "./utils/parse-entry";
15
15
  export async function loadEntries(
16
16
  options: PocketBaseLoaderOptions,
17
17
  context: LoaderContext,
18
- superuserToken: string | undefined,
18
+ adminToken: string | undefined,
19
19
  lastModified: string | undefined
20
- ): Promise<void> {
20
+ ): Promise<boolean> {
21
21
  // Build the URL for the collections endpoint
22
22
  const collectionUrl = new URL(
23
23
  `api/collections/${options.collectionName}/records`,
24
24
  options.url
25
25
  ).href;
26
26
 
27
- // Create the headers for the request to append the superuser token (if available)
27
+ // Create the headers for the request to append the admin token (if available)
28
28
  const collectionHeaders = new Headers();
29
- if (superuserToken) {
30
- collectionHeaders.set("Authorization", superuserToken);
29
+ if (adminToken) {
30
+ collectionHeaders.set("Authorization", adminToken);
31
31
  }
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 for ${
36
+ context.collection
37
+ } from ${collectionUrl}${
38
+ lastModified ? ` starting at ${lastModified}` : ""
39
+ }${adminToken ? " with admin token" : ""}`
40
40
  );
41
41
 
42
42
  // Prepare pagination variables
43
43
  let page = 0;
44
44
  let totalPages = 0;
45
45
  let entries = 0;
46
+ let hasUpdatedColumn = !!lastModified;
46
47
 
47
48
  // Fetch all (modified) entries
48
49
  do {
@@ -50,8 +51,8 @@ export async function loadEntries(
50
51
  // If `lastModified` is set, only fetch entries that have been modified since the last fetch
51
52
  const collectionRequest = await fetch(
52
53
  `${collectionUrl}?page=${++page}&perPage=100${
53
- lastModified && options.updatedField
54
- ? `&sort=-${options.updatedField},id&filter=(${options.updatedField}>"${lastModified}")`
54
+ lastModified
55
+ ? `&sort=-updated,id&filter=(updated>"${lastModified}")`
55
56
  : ""
56
57
  }`,
57
58
  {
@@ -61,10 +62,10 @@ export async function loadEntries(
61
62
 
62
63
  // If the request was not successful, print the error message and return
63
64
  if (!collectionRequest.ok) {
64
- // If the collection is locked, an superuser token is required
65
+ // If the collection is locked, an admin token is required
65
66
  if (collectionRequest.status === 403) {
66
67
  throw new Error(
67
- `(${options.collectionName}) The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
68
+ `The collection ${options.collectionName} is not accessible without an admin rights. Please provide an admin email and password in the config.`
68
69
  );
69
70
  }
70
71
 
@@ -72,7 +73,7 @@ export async function loadEntries(
72
73
  const reason = await collectionRequest
73
74
  .json()
74
75
  .then((data) => data.message);
75
- const errorMessage = `(${options.collectionName}) Fetching data failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
76
+ const errorMessage = `Fetching data from ${options.collectionName} failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
76
77
  throw new Error(errorMessage);
77
78
  }
78
79
 
@@ -81,7 +82,13 @@ export async function loadEntries(
81
82
 
82
83
  // Parse and store the entries
83
84
  for (const entry of response.items) {
84
- await parseEntry(entry, context, options);
85
+ await parseEntry(entry, context, options.id, options.content);
86
+
87
+ // Check if the entry has an `updated` column
88
+ // This is used to enable the incremental fetching of entries
89
+ if (!hasUpdatedColumn && "updated" in entry) {
90
+ hasUpdatedColumn = true;
91
+ }
85
92
  }
86
93
 
87
94
  // Update the page and total pages
@@ -92,8 +99,11 @@ export async function loadEntries(
92
99
 
93
100
  // Log the number of fetched entries
94
101
  context.logger.info(
95
- `(${options.collectionName}) Fetched ${entries}${
96
- lastModified ? " changed" : ""
97
- } entries.`
102
+ `Fetched ${entries}${lastModified ? " changed" : ""} entries for ${
103
+ context.collection
104
+ }`
98
105
  );
106
+
107
+ // Return if the collection has an updated column
108
+ return hasUpdatedColumn;
99
109
  }
@@ -4,7 +4,7 @@ import { cleanupEntries } from "./cleanup-entries";
4
4
  import { generateSchema } from "./generate-schema";
5
5
  import { loadEntries } from "./load-entries";
6
6
  import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
7
- import { getSuperuserToken } from "./utils/get-superuser-token";
7
+ import { getAdminToken } from "./utils/get-admin-token";
8
8
 
9
9
  /**
10
10
  * Loader for collections stored in PocketBase.
@@ -15,37 +15,37 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
15
15
  return {
16
16
  name: "pocketbase-loader",
17
17
  load: async (context: LoaderContext): Promise<void> => {
18
- // Get the date of the last fetch to only update changed entries.
19
- let lastModified = context.meta.get("last-modified");
20
-
21
18
  // Check if the version has changed to force an update
22
19
  const lastVersion = context.meta.get("version");
23
20
  if (lastVersion !== packageJson.version) {
24
- if (lastVersion) {
25
- context.logger.info(
26
- `PocketBase loader was updated from ${lastVersion} to ${packageJson.version}. All entries will be loaded again.`
27
- );
28
- }
21
+ context.logger.info(
22
+ `PocketBase loader was updated from ${lastVersion} to ${packageJson.version}. All entries will be loaded again.`
23
+ );
29
24
 
30
- // Disable incremental builds and clear the store
31
- lastModified = undefined;
32
- context.store.clear();
25
+ options.forceUpdate = true;
33
26
  }
34
27
 
35
- // Disable incremental builds if no updated field is provided
36
- if (!options.updatedField) {
37
- context.logger.info(
38
- `(${options.collectionName}) No "updatedField" was provided. Incremental builds are disabled.`
39
- );
40
- lastModified = undefined;
28
+ // Get the date of the last fetch to only update changed entries.
29
+ // If `forceUpdate` is set to `true`, this will be `undefined` to fetch all entries again.
30
+ const lastModified = options.forceUpdate
31
+ ? undefined
32
+ : context.meta.get("last-modified");
33
+
34
+ // Get the `has-updated-column` meta to check if the collection has an updated column
35
+ let hasUpdatedColumn = context.meta.get("has-updated-column") === "true";
36
+
37
+ // Clear the store if we want to fetch all entries again
38
+ if (options.forceUpdate) {
39
+ context.store.clear();
41
40
  }
42
41
 
43
- // Try to get a superuser token to access all resources.
42
+ // Try to get an admin token to access all resources.
44
43
  let token: string | undefined;
45
- if (options.superuserCredentials) {
46
- token = await getSuperuserToken(
44
+ if (options.adminEmail && options.adminPassword) {
45
+ token = await getAdminToken(
47
46
  options.url,
48
- options.superuserCredentials,
47
+ options.adminEmail,
48
+ options.adminPassword,
49
49
  context.logger
50
50
  );
51
51
  }
@@ -56,7 +56,25 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
56
56
  }
57
57
 
58
58
  // Load the (modified) entries
59
- await loadEntries(options, context, token, lastModified);
59
+ try {
60
+ hasUpdatedColumn = await loadEntries(
61
+ options,
62
+ context,
63
+ token,
64
+ // Only fetch entries that have been modified since the last fetch
65
+ // If the collection does not have an updated column, all entries will be fetched
66
+ hasUpdatedColumn ? lastModified : undefined
67
+ );
68
+ } catch (error) {
69
+ // Set the `has-updated-column` meta to `false` if an error occurred
70
+ // This will force the loader to fetch all entries again in the next run
71
+ context.meta.set("has-updated-column", `${false}`);
72
+
73
+ throw error;
74
+ }
75
+
76
+ // Set the `has-updated-column` meta to `true` if the collection has an updated column
77
+ context.meta.set("has-updated-column", `${hasUpdatedColumn}`);
60
78
 
61
79
  // Set the last modified date to the current date
62
80
  context.meta.set("last-modified", new Date().toISOString());
@@ -14,6 +14,14 @@ interface PocketBaseBaseEntry {
14
14
  * Name of the collection the entry belongs to.
15
15
  */
16
16
  collectionName: string;
17
+ /**
18
+ * Date the entry was created.
19
+ */
20
+ created?: string | undefined;
21
+ /**
22
+ * Date the entry was last updated.
23
+ */
24
+ updated?: string | undefined;
17
25
  }
18
26
 
19
27
  /**
@@ -20,41 +20,31 @@ export interface PocketBaseLoaderOptions {
20
20
  *
21
21
  * If the field is a string, it will be slugified to be used in the URL.
22
22
  */
23
- idField?: string;
23
+ id?: string;
24
24
  /**
25
- * Name of the field(s) containing the content of an entry.
26
- * This must be the name of a field in the PocketBase collection that contains the content.
25
+ * Content of the collection in PocketBase.
26
+ * This must be the name of a field in the collection that contains the content.
27
27
  * The content will be parsed as HTML and rendered to the page.
28
28
  *
29
29
  * If you want to render multiple fields as main content, you can pass an array of field names.
30
30
  * The loader will concatenate the content of all fields in the order they are defined in the array.
31
31
  * Each block will be contained in a `<section>` element.
32
32
  */
33
- contentFields?: string | Array<string>;
33
+ content?: string | Array<string>;
34
34
  /**
35
- * Name of the field containing the last update date of an entry.
36
- * Ideally, this field should be of type `autodate` and have the value "Update" or "Create/Update".
37
- * This field is used to only fetch entries that have been modified since the last build.
35
+ * Email of an admin to get full access to the PocketBase instance.
36
+ * Together with `adminPassword` this is required to get automatic type generation and to access all resources even if they are not public.
38
37
  */
39
- updatedField?: string;
38
+ adminEmail?: string;
40
39
  /**
41
- * Credentials of a superuser to get full access to the PocketBase instance.
42
- * 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.
40
+ * Password of the admin to get full access to the PocketBase instance.
41
+ * Together with `adminEmail` this is required to get automatic type generation and to access all resources even if they are not public.
43
42
  */
44
- superuserCredentials?: {
45
- /**
46
- * Email of the superuser.
47
- */
48
- email: string;
49
- /**
50
- * Password of the superuser.
51
- */
52
- password: string;
53
- };
43
+ adminPassword?: string;
54
44
  /**
55
45
  * File path to the local schema file.
56
46
  * This file will be used to generate the schema for the collection.
57
- * If `superuserCredentials` are provided, this option will be ignored.
47
+ * If admin credentials are provided (see `adminEmail` and `adminPassword`), this option will be ignored.
58
48
  */
59
49
  localSchema?: string;
60
50
  /**
@@ -65,4 +55,9 @@ export interface PocketBaseLoaderOptions {
65
55
  * Note that this will only be used for fields of type `json`.
66
56
  */
67
57
  jsonSchemas?: Record<string, z.ZodSchema>;
58
+ /**
59
+ * By default, the loader will only fetch entries that have been modified since the last fetch.
60
+ * If you want to fetch all entries, set this to `true`.
61
+ */
62
+ forceUpdate?: boolean;
68
63
  }
@@ -2,11 +2,6 @@
2
2
  * Entry for a collections schema in PocketBase.
3
3
  */
4
4
  export interface PocketBaseSchemaEntry {
5
- /**
6
- * Flag to indicate if the field is hidden.
7
- * Hidden fields are not returned in the API response.
8
- */
9
- hidden: boolean;
10
5
  /**
11
6
  * Name of the field.
12
7
  */
@@ -20,25 +15,20 @@ export interface PocketBaseSchemaEntry {
20
15
  */
21
16
  required: boolean;
22
17
  /**
23
- * Values for a select field.
24
- * This is only present if the field type is "select".
25
- */
26
- values?: Array<string>;
27
- /**
28
- * Maximum number of values for a select field.
29
- * This is only present on "select", "relation", and "file" fields.
30
- */
31
- maxSelect?: number;
32
- /**
33
- * Whether the field is filled when the entry is created.
34
- * This is only present on "autodate" fields.
35
- */
36
- onCreate?: boolean;
37
- /**
38
- * Whether the field is updated when the entry is updated.
39
- * This is only present on "autodate" fields.
40
- */
41
- onUpdate?: boolean;
18
+ * Options for the field.
19
+ */
20
+ options: {
21
+ /**
22
+ * Values for a select field.
23
+ * This is only present if the field type is "select".
24
+ */
25
+ values?: Array<string>;
26
+ /**
27
+ * Maximum number of values for a select field.
28
+ * This is only present on "select", "relation", and "file" fields.
29
+ */
30
+ maxSelect?: number;
31
+ };
42
32
  }
43
33
 
44
34
  /**
@@ -52,9 +42,9 @@ export interface PocketBaseCollection {
52
42
  /**
53
43
  * Type of the collection.
54
44
  */
55
- type: "base" | "view" | "auth";
45
+ type: 'base' | 'view' | 'auth';
56
46
  /**
57
47
  * Schema of the collection.
58
48
  */
59
- fields: Array<PocketBaseSchemaEntry>;
49
+ schema: Array<PocketBaseSchemaEntry>;
60
50
  }
@@ -1,31 +1,27 @@
1
1
  import type { AstroIntegrationLogger } from "astro";
2
2
 
3
3
  /**
4
- * This function will get a superuser token from the given PocketBase instance.
4
+ * This function will get an admin token from the given PocketBase instance.
5
5
  *
6
6
  * @param url URL of the PocketBase instance
7
- * @param superuserCredentials Credentials of the superuser
7
+ * @param email Email of the admin
8
+ * @param password Password of the admin
8
9
  *
9
- * @returns A superuser token to access all resources of the PocketBase instance.
10
+ * @returns An admin token to access all resources of the PocketBase instance.
10
11
  */
11
- export async function getSuperuserToken(
12
+ export async function getAdminToken(
12
13
  url: string,
13
- superuserCredentials: {
14
- email: string;
15
- password: string;
16
- },
14
+ email: string,
15
+ password: string,
17
16
  logger?: AstroIntegrationLogger
18
17
  ): Promise<string | undefined> {
19
18
  // Build the URL for the login endpoint
20
- const loginUrl = new URL(
21
- `api/collections/_superusers/auth-with-password`,
22
- url
23
- ).href;
19
+ const loginUrl = new URL(`api/admins/auth-with-password`, url).href;
24
20
 
25
21
  // Create a new FormData object to send the login data
26
22
  const loginData = new FormData();
27
- loginData.set("identity", superuserCredentials.email);
28
- loginData.set("password", superuserCredentials.password);
23
+ loginData.set("identity", email);
24
+ loginData.set("password", password);
29
25
 
30
26
  // Send the login request to get a token
31
27
  const loginRequest = await fetch(loginUrl, {
@@ -1,6 +1,6 @@
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 "./get-superuser-token";
3
+ import { getAdminToken } from "./get-admin-token";
4
4
 
5
5
  /**
6
6
  * Fetches the schema for the specified collection from the PocketBase instance.
@@ -10,17 +10,18 @@ import { getSuperuserToken } from "./get-superuser-token";
10
10
  export async function getRemoteSchema(
11
11
  options: PocketBaseLoaderOptions
12
12
  ): Promise<PocketBaseCollection | undefined> {
13
- if (!options.superuserCredentials) {
13
+ if (!options.adminEmail || !options.adminPassword) {
14
14
  return undefined;
15
15
  }
16
16
 
17
- // Get a superuser token
18
- const token = await getSuperuserToken(
17
+ // Get the admin token
18
+ const token = await getAdminToken(
19
19
  options.url,
20
- options.superuserCredentials
20
+ options.adminEmail,
21
+ options.adminPassword
21
22
  );
22
23
 
23
- // If the token is invalid try another method
24
+ // If the token is invalid, return the basic schema
24
25
  if (!token) {
25
26
  return undefined;
26
27
  }
@@ -38,7 +39,7 @@ export async function getRemoteSchema(
38
39
  headers: schemaHeaders
39
40
  });
40
41
 
41
- // If the request was not successful, try another method
42
+ // If the request was not successful, return the basic schema
42
43
  if (!schemaRequest.ok) {
43
44
  const reason = await schemaRequest.json().then((data) => data.message);
44
45
  const errorMessage = `Fetching schema from ${options.collectionName} failed with status code ${schemaRequest.status}.\nReason: ${reason}`;
@@ -1,6 +1,5 @@
1
1
  import type { LoaderContext } from "astro/loaders";
2
2
  import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
3
- import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
4
3
  import { slugify } from "./slugify";
5
4
 
6
5
  /**
@@ -16,7 +15,8 @@ import { slugify } from "./slugify";
16
15
  export async function parseEntry(
17
16
  entry: PocketBaseEntry,
18
17
  { generateDigest, parseData, store, logger }: LoaderContext,
19
- { idField, contentFields, updatedField }: PocketBaseLoaderOptions
18
+ idField?: string,
19
+ contentFields?: string | Array<string>
20
20
  ): Promise<void> {
21
21
  let id = entry.id;
22
22
  if (idField) {
@@ -46,15 +46,11 @@ export async function parseEntry(
46
46
  data: entry
47
47
  });
48
48
 
49
- // Get the updated date of the entry
50
- let updated: string | undefined;
51
- if (updatedField) {
52
- updated = `${entry[updatedField]}`;
53
- }
54
-
55
49
  // Generate a digest for the entry
56
- // If no updated date is available, the digest will be generated from the whole entry
57
- const digest = generateDigest(updated ?? entry);
50
+ // Normal collections use the updated date that is always updated when the entry is updated.
51
+ // If the entry was never updated, the created date can be used as a fallback.
52
+ // View collections don't necessarily publish the updated date, so the whole entry is used for the digest.
53
+ const digest = generateDigest(entry.updated ?? entry.created ?? entry);
58
54
 
59
55
  if (!contentFields) {
60
56
  // Store the entry
@@ -6,19 +6,13 @@ import type {
6
6
 
7
7
  export function parseSchema(
8
8
  collection: PocketBaseCollection,
9
- customSchemas: Record<string, z.ZodType> | undefined,
10
- hasSuperuserRights: boolean
9
+ customSchemas: Record<string, z.ZodType> | undefined
11
10
  ): Record<string, z.ZodType> {
12
11
  // Prepare the schemas fields
13
12
  const fields: Record<string, z.ZodType> = {};
14
13
 
15
14
  // Parse every field in the schema
16
- for (const field of collection.fields) {
17
- // Skip hidden fields if the user does not have superuser rights
18
- if (field.hidden && !hasSuperuserRights) {
19
- continue;
20
- }
21
-
15
+ for (const field of collection.schema) {
22
16
  let fieldType;
23
17
 
24
18
  // Determine the field type and create the corresponding Zod type
@@ -32,12 +26,11 @@ export function parseSchema(
32
26
  fieldType = z.coerce.boolean();
33
27
  break;
34
28
  case "date":
35
- case "autodate":
36
29
  // Coerce and parse the value as a date
37
30
  fieldType = z.coerce.date();
38
31
  break;
39
32
  case "select":
40
- if (!field.values) {
33
+ if (!field.options.values) {
41
34
  throw new Error(
42
35
  `Field ${field.name} is of type "select" but has no values defined.`
43
36
  );
@@ -45,7 +38,7 @@ export function parseSchema(
45
38
 
46
39
  // Create an enum for the select values
47
40
  // @ts-expect-error - Zod complains because the values are not known at compile time and thus the array is not static.
48
- const values = z.enum(field.values);
41
+ const values = z.enum(field.options.values);
49
42
 
50
43
  // Parse the field type based on the number of values it can have
51
44
  fieldType = parseSingleOrMultipleValues(field, values);
@@ -73,12 +66,8 @@ export function parseSchema(
73
66
  break;
74
67
  }
75
68
 
76
- // Check if the field is required (onCreate autodate fields are always set)
77
- const isRequired =
78
- field.required || (field.type === "autodate" && field.onCreate);
79
-
80
69
  // If the field is not required, mark it as optional
81
- if (!isRequired) {
70
+ if (!field.required) {
82
71
  fieldType = z.preprocess(
83
72
  (val) => val || undefined,
84
73
  z.optional(fieldType)
@@ -105,7 +94,7 @@ function parseSingleOrMultipleValues(
105
94
  type: z.ZodType
106
95
  ) {
107
96
  // If the select allows multiple values, create an array of the enum
108
- if (field.maxSelect === undefined || field.maxSelect === 1) {
97
+ if (field.options.maxSelect === undefined || field.options.maxSelect === 1) {
109
98
  return type;
110
99
  } else {
111
100
  return z.array(type);
@@ -17,7 +17,7 @@ export function transformFiles(
17
17
  for (const field of fileFields) {
18
18
  const fieldName = field.name;
19
19
 
20
- if (field.maxSelect === 1) {
20
+ if (field.options.maxSelect === 1) {
21
21
  const fileName = entry[fieldName] as string | undefined;
22
22
  // Check if a file name is present
23
23
  if (!fileName) {