astro-loader-pocketbase 0.4.0-rc.2 → 0.5.0-rc.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
@@ -9,6 +9,18 @@
9
9
 
10
10
  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
11
 
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
+ ## Compatibility
18
+
19
+ | Loader version | Astro version | PocketBase version |
20
+ | ---------------------------------------------------------------------------- | ------------- | ------------------ |
21
+ | >= 0.5.0 | >= 5.0.0-beta | >= 0.23.0 |
22
+ | <= [0.4.0](https://github.com/pawcoding/astro-loader-pocketbase/tree/v0.4.0) | >= 5.0.0-beta | < 0.23.0 |
23
+
12
24
  ## Basic usage
13
25
 
14
26
  In your content configuration file, you can use the `pocketbaseLoader` function to use your PocketBase database as a data source.
@@ -27,12 +39,30 @@ const blog = defineCollection({
27
39
  export const collections = { blog };
28
40
  ```
29
41
 
30
- By default, the loader will only fetch entries that have been modified since the last build.
31
42
  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.
32
43
  If you want to update your deployed site with new entries, you need to rebuild it.
33
44
 
34
45
  <sub>When running the dev server, you can trigger a reload by using `s + enter`.</sub>
35
46
 
47
+ ## Incremental builds
48
+
49
+ Since PocketBase 0.23.0, the `updated` field is not mandatory anymore.
50
+ This means that the loader can't automatically detect when an entry has been modified.
51
+ 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.
52
+
53
+ ```ts
54
+ const blog = defineCollection({
55
+ loader: pocketbaseLoader({
56
+ ...options,
57
+ updatedField: "<field-in-collection>"
58
+ })
59
+ });
60
+ ```
61
+
62
+ When this field is provided, the loader will only fetch entries that have been modified since the last build.
63
+ Ideally, this field should be of type `autodate` and have the value "Update" or "Create/Update" in the PocketBase dashboard.
64
+ This ensures that the field is automatically updated when an entry is modified.
65
+
36
66
  ## Entries
37
67
 
38
68
  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.).
@@ -46,7 +76,7 @@ This content will then be used when calling the `render` function of [Astros con
46
76
  const blog = defineCollection({
47
77
  loader: pocketbaseLoader({
48
78
  ...options,
49
- content: "<field-in-collection>"
79
+ contentFields: "<field-in-collection>"
50
80
  })
51
81
  });
52
82
  ```
@@ -70,14 +100,16 @@ These types can be generated in two ways:
70
100
 
71
101
  ### Remote schema
72
102
 
73
- To use the lice remote schema, you need to provide the email and password of an admin of the PocketBase instance.
103
+ To use the lice remote schema, you need to provide superuser credentials for the PocketBase instance.
74
104
 
75
105
  ```ts
76
106
  const blog = defineCollection({
77
107
  loader: pocketbaseLoader({
78
108
  ...options,
79
- adminEmail: "<admin-email>",
80
- adminPassword: "<admin-password>"
109
+ superuserCredentials: {
110
+ email: "<superuser-email>",
111
+ password: "<superuser-password>"
112
+ }
81
113
  })
82
114
  });
83
115
  ```
@@ -86,7 +118,7 @@ Under the hood, the loader will use the [PocketBase API](https://pocketbase.io/d
86
118
 
87
119
  ### Local schema
88
120
 
89
- 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.
121
+ 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.
90
122
 
91
123
  ```ts
92
124
  const blog = defineCollection({
@@ -100,7 +132,7 @@ const blog = defineCollection({
100
132
  In PocketBase you can export the schema of the whole database to a `pb_schema.json` file.
101
133
  If you provide the path to this file, the loader will use this schema to generate the types locally.
102
134
 
103
- When admin credentials are provided, the loader will **always use the remote schema** since it's more up-to-date.
135
+ When superuser credentials are provided, the loader will **always use the remote schema** since it's more up-to-date.
104
136
 
105
137
  ### Manual schema
106
138
 
@@ -109,36 +141,25 @@ This manual schema will **always override the automatic type generation**.
109
141
 
110
142
  ## All options
111
143
 
112
- | Option | Type | Required | Description |
113
- | ---------------- | ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
114
- | `url` | `string` | x | The URL of your PocketBase instance. |
115
- | `collectionName` | `string` | x | The name of the collection in your PocketBase instance. |
116
- | `content` | `string \| Array<string>` | | The field in the collection to use as content. This can also be an array of fields. |
117
- | `adminEmail` | `string` | | The email of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
118
- | `adminPassword` | `string` | | The password of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
119
- | `localSchema` | `string` | | The path to a local schema file. This is used for automatic type generation. |
120
- | `jsonSchemas` | `Record<string, z.ZodSchema>` | | A record of Zod schemas to use for type generation of `json` fields. |
121
- | `forceUpdate` | `boolean` | | If set to `true`, the loader will fetch every entry instead of only the ones modified since the last build. |
144
+ | Option | Type | Required | Description |
145
+ | ---------------------- | ------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
146
+ | `url` | `string` | x | The URL of your PocketBase instance. |
147
+ | `collectionName` | `string` | x | The name of the collection in your PocketBase instance. |
148
+ | `contentFields` | `string \| Array<string>` | | The field in the collection to use as content. This can also be an array of fields. |
149
+ | `updatedField` | `string` | | The field in the collection that stores the last update date of an entry. This is used for incremental builds. |
150
+ | `superuserCredentials` | `{ email: string, password: string }` | | The email and password of the superuser of the PocketBase instance. This is used for automatic type generation. |
151
+ | `localSchema` | `string` | | The path to a local schema file. This is used for automatic type generation. |
152
+ | `jsonSchemas` | `Record<string, z.ZodSchema>` | | A record of Zod schemas to use for type generation of `json` fields. |
122
153
 
123
154
  ## Special cases
124
155
 
125
- ### Private collections
156
+ ### Private collections and hidden fields
126
157
 
127
- If you want to access a private collection, you also need to provide the admin credentials.
158
+ If you want to access a private collection or want to access hidden fields, you also need to provide superuser credentials.
128
159
  Otherwise, you need to make the collection public in the PocketBase dashboard.
129
160
 
130
161
  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.
131
162
 
132
- ### View collections
133
-
134
- Out of the box, the loader also supports collections with the type `view`, though with some limitations.
135
- To enable incremental builds, the loader needs to know when an entry has been modified.
136
- Normal `base` collections have a `updated` field that is automatically updated when an entry is modified.
137
- Thus, `view` collections that don't include this field can't be incrementally built but will be fetched every time.
138
-
139
- You can also alias another field as `updated` (as long as it's a date field) in your view.
140
- While this is possible, it's not recommended since it can lead to outdated data not being fetched.
141
-
142
163
  ### JSON fields
143
164
 
144
165
  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.4.0-rc.2",
3
+ "version": "0.5.0-rc.1",
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)",
@@ -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 adminToken Admin token to access all resources.
9
+ * @param superuserToken Superuser token to access all resources.
10
10
  */
11
11
  export async function cleanupEntries(
12
12
  options: PocketBaseLoaderOptions,
13
13
  context: LoaderContext,
14
- adminToken: string | undefined
14
+ superuserToken: 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 admin token (if available)
22
+ // Create the headers for the request to append the superuser token (if available)
23
23
  const collectionHeaders = new Headers();
24
- if (adminToken) {
25
- collectionHeaders.set("Authorization", adminToken);
24
+ if (superuserToken) {
25
+ collectionHeaders.set("Authorization", superuserToken);
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 admin token is required
45
+ // If the collection is locked, an superuser token is required
46
46
  if (collectionRequest.status === 403) {
47
47
  context.logger.error(
48
- `The collection ${options.collectionName} is not accessible without an admin rights. Please provide an admin email and password in the config.`
48
+ `(${options.collectionName}) 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 = `Fetching ids from ${options.collectionName} failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
56
+ const errorMessage = `(${options.collectionName}) Fetching ids 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
- `Cleaned up ${cleanedUp} old entries for ${context.collection}`
89
+ `(${options.collectionName}) Cleaned up ${cleanedUp} old entries.`
90
90
  );
91
91
  }
92
92
  }
@@ -11,28 +11,15 @@ import { transformFiles } from "./utils/transform-files";
11
11
  * Basic schema for every PocketBase collection.
12
12
  */
13
13
  const BASIC_SCHEMA = {
14
- id: z.string().length(15),
15
- collectionId: z.string().length(15),
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
14
  id: z.string(),
26
15
  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()))
16
+ collectionName: z.string()
30
17
  };
31
18
 
32
19
  /**
33
20
  * Generate a schema for the collection based on the collection's schema in PocketBase.
34
21
  * By default, a basic schema is returned if no other schema is available.
35
- * If admin credentials are provided, the schema is fetched from the PocketBase API.
22
+ * If superuser credentials are provided, the schema is fetched from the PocketBase API.
36
23
  * If a path to a local schema file is provided, the schema is read from the file.
37
24
  *
38
25
  * @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
@@ -45,6 +32,8 @@ export async function generateSchema(
45
32
  // Try to get the schema directly from the PocketBase instance
46
33
  collection = await getRemoteSchema(options);
47
34
 
35
+ const hasSuperuserRights = !!collection || !!options.superuserCredentials;
36
+
48
37
  // If the schema is not available, try to read it from a local schema file
49
38
  if (!collection && options.localSchema) {
50
39
  collection = await readLocalSchema(
@@ -56,22 +45,29 @@ export async function generateSchema(
56
45
  // If the schema is still not available, return the basic schema
57
46
  if (!collection) {
58
47
  console.error(
59
- `No schema available for ${options.collectionName}. Only basic types are available. Please check your configuration and provide a valid schema file or admin credentials.`
48
+ `No schema available for "${options.collectionName}". Only basic types are available. Please check your configuration and provide a valid schema file or superuser credentials.`
60
49
  );
61
- // Return the view schema since every collection has at least the view schema
62
- return z.object(VIEW_SCHEMA);
50
+ // Return the basic schema since every collection has at least these fields
51
+ return z.object(BASIC_SCHEMA);
63
52
  }
64
53
 
65
54
  // Parse the schema
66
- const fields = parseSchema(collection, options.jsonSchemas);
55
+ const fields = parseSchema(
56
+ collection,
57
+ options.jsonSchemas,
58
+ hasSuperuserRights
59
+ );
67
60
 
68
61
  // Check if the content field is present
69
- if (typeof options.content === "string" && !fields[options.content]) {
62
+ if (
63
+ typeof options.contentFields === "string" &&
64
+ !fields[options.contentFields]
65
+ ) {
70
66
  console.error(
71
- `The content field "${options.content}" is not present in the schema of the collection "${options.collectionName}".`
67
+ `The content field "${options.contentFields}" is not present in the schema of the collection "${options.collectionName}".`
72
68
  );
73
- } else if (Array.isArray(options.content)) {
74
- for (const field of options.content) {
69
+ } else if (Array.isArray(options.contentFields)) {
70
+ for (const field of options.contentFields) {
75
71
  if (!fields[field]) {
76
72
  console.error(
77
73
  `The content field "${field}" is not present in the schema of the collection "${options.collectionName}".`
@@ -80,18 +76,39 @@ export async function generateSchema(
80
76
  }
81
77
  }
82
78
 
83
- // Use the corresponding base schema for the type of collection
84
- // Auth collections are basically a superset of the basic schema.
85
- const base = collection.type === "view" ? VIEW_SCHEMA : BASIC_SCHEMA;
79
+ // Check if the updated field is present
80
+ if (options.updatedField) {
81
+ if (!fields[options.updatedField]) {
82
+ console.error(
83
+ `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.`
84
+ );
85
+ } else {
86
+ const updatedField = collection.fields.find(
87
+ (field) => field.name === options.updatedField
88
+ );
89
+ if (
90
+ !updatedField ||
91
+ updatedField.type !== "autodate" ||
92
+ !updatedField.onUpdate
93
+ ) {
94
+ console.warn(
95
+ `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!`
96
+ );
97
+ }
98
+ }
99
+ }
86
100
 
87
101
  // Combine the basic schema with the parsed fields
88
102
  const schema = z.object({
89
- ...base,
103
+ ...BASIC_SCHEMA,
90
104
  ...fields
91
105
  });
92
106
 
93
107
  // Get all file fields
94
- const fileFields = collection.schema.filter((field) => field.type === "file");
108
+ const fileFields = collection.fields
109
+ .filter((field) => field.type === "file")
110
+ // Only show hidden fields if the user has superuser rights
111
+ .filter((field) => !field.hidden || hasSuperuserRights);
95
112
 
96
113
  if (fileFields.length === 0) {
97
114
  return schema;
@@ -99,7 +116,6 @@ export async function generateSchema(
99
116
 
100
117
  // Transform file names to file urls
101
118
  return schema.transform((entry) =>
102
- // @ts-expect-error - `updated` and `created` are already transformed to dates
103
119
  transformFiles(options.url, fileFields, entry)
104
120
  );
105
121
  }
@@ -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 adminToken Admin token to access all resources.
10
+ * @param superuserToken Superuser 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,35 +15,34 @@ import { parseEntry } from "./utils/parse-entry";
15
15
  export async function loadEntries(
16
16
  options: PocketBaseLoaderOptions,
17
17
  context: LoaderContext,
18
- adminToken: string | undefined,
18
+ superuserToken: string | undefined,
19
19
  lastModified: string | undefined
20
- ): Promise<boolean> {
20
+ ): Promise<void> {
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 admin token (if available)
27
+ // Create the headers for the request to append the superuser token (if available)
28
28
  const collectionHeaders = new Headers();
29
- if (adminToken) {
30
- collectionHeaders.set("Authorization", adminToken);
29
+ if (superuserToken) {
30
+ collectionHeaders.set("Authorization", superuserToken);
31
31
  }
32
32
 
33
33
  // Log the fetching of the entries
34
34
  context.logger.info(
35
- `Fetching${lastModified ? " modified" : ""} data for ${
36
- context.collection
37
- } from ${collectionUrl}${
38
- lastModified ? ` starting at ${lastModified}` : ""
39
- }${adminToken ? " with admin token" : ""}`
35
+ `(${options.collectionName}) Fetching${
36
+ lastModified ? " modified" : ""
37
+ } data${lastModified ? ` starting at ${lastModified}` : ""}${
38
+ superuserToken ? " as superuser" : ""
39
+ }`
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;
47
46
 
48
47
  // Fetch all (modified) entries
49
48
  do {
@@ -51,8 +50,8 @@ export async function loadEntries(
51
50
  // If `lastModified` is set, only fetch entries that have been modified since the last fetch
52
51
  const collectionRequest = await fetch(
53
52
  `${collectionUrl}?page=${++page}&perPage=100${
54
- lastModified
55
- ? `&sort=-updated,id&filter=(updated>"${lastModified}")`
53
+ lastModified && options.updatedField
54
+ ? `&sort=-${options.updatedField},id&filter=(${options.updatedField}>"${lastModified}")`
56
55
  : ""
57
56
  }`,
58
57
  {
@@ -62,10 +61,10 @@ export async function loadEntries(
62
61
 
63
62
  // If the request was not successful, print the error message and return
64
63
  if (!collectionRequest.ok) {
65
- // If the collection is locked, an admin token is required
64
+ // If the collection is locked, an superuser token is required
66
65
  if (collectionRequest.status === 403) {
67
66
  throw new Error(
68
- `The collection ${options.collectionName} is not accessible without an admin rights. Please provide an admin email and password in the config.`
67
+ `(${options.collectionName}) The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
69
68
  );
70
69
  }
71
70
 
@@ -73,7 +72,7 @@ export async function loadEntries(
73
72
  const reason = await collectionRequest
74
73
  .json()
75
74
  .then((data) => data.message);
76
- const errorMessage = `Fetching data from ${options.collectionName} failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
75
+ const errorMessage = `(${options.collectionName}) Fetching data failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
77
76
  throw new Error(errorMessage);
78
77
  }
79
78
 
@@ -82,13 +81,12 @@ export async function loadEntries(
82
81
 
83
82
  // Parse and store the entries
84
83
  for (const entry of response.items) {
85
- await parseEntry(entry, context, 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
- }
84
+ await parseEntry(
85
+ entry,
86
+ context,
87
+ options.contentFields,
88
+ options.updatedField
89
+ );
92
90
  }
93
91
 
94
92
  // Update the page and total pages
@@ -99,11 +97,8 @@ export async function loadEntries(
99
97
 
100
98
  // Log the number of fetched entries
101
99
  context.logger.info(
102
- `Fetched ${entries}${lastModified ? " changed" : ""} entries for ${
103
- context.collection
104
- }`
100
+ `(${options.collectionName}) Fetched ${entries}${
101
+ lastModified ? " changed" : ""
102
+ } entries.`
105
103
  );
106
-
107
- // Return if the collection has an updated column
108
- return hasUpdatedColumn;
109
104
  }
@@ -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 { getAdminToken } from "./utils/get-admin-token";
7
+ import { getSuperuserToken } from "./utils/get-superuser-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
+
18
21
  // Check if the version has changed to force an update
19
22
  const lastVersion = context.meta.get("version");
20
23
  if (lastVersion !== packageJson.version) {
21
- context.logger.info(
22
- `PocketBase loader was updated from ${lastVersion} to ${packageJson.version}. All entries will be loaded again.`
23
- );
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
+ }
24
29
 
25
- options.forceUpdate = true;
30
+ // Disable incremental builds and clear the store
31
+ lastModified = undefined;
32
+ context.store.clear();
26
33
  }
27
34
 
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();
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;
40
41
  }
41
42
 
42
- // Try to get an admin token to access all resources.
43
+ // Try to get a superuser token to access all resources.
43
44
  let token: string | undefined;
44
- if (options.adminEmail && options.adminPassword) {
45
- token = await getAdminToken(
45
+ if (options.superuserCredentials) {
46
+ token = await getSuperuserToken(
46
47
  options.url,
47
- options.adminEmail,
48
- options.adminPassword,
48
+ options.superuserCredentials,
49
49
  context.logger
50
50
  );
51
51
  }
@@ -56,25 +56,7 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
56
56
  }
57
57
 
58
58
  // Load the (modified) entries
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}`);
59
+ await loadEntries(options, context, token, lastModified);
78
60
 
79
61
  // Set the last modified date to the current date
80
62
  context.meta.set("last-modified", new Date().toISOString());
@@ -14,14 +14,6 @@ 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;
25
17
  }
26
18
 
27
19
  /**
@@ -13,29 +13,39 @@ export interface PocketBaseLoaderOptions {
13
13
  */
14
14
  collectionName: string;
15
15
  /**
16
- * Content of the collection in PocketBase.
17
- * This must be the name of a field in the collection that contains the content.
16
+ * Name of the field(s) containing the content of an entry.
17
+ * This must be the name of a field in the PocketBase collection that contains the content.
18
18
  * The content will be parsed as HTML and rendered to the page.
19
19
  *
20
20
  * If you want to render multiple fields as main content, you can pass an array of field names.
21
21
  * The loader will concatenate the content of all fields in the order they are defined in the array.
22
22
  * Each block will be contained in a `<section>` element.
23
23
  */
24
- content?: string | Array<string>;
24
+ contentFields?: string | Array<string>;
25
25
  /**
26
- * Email of an admin to get full access to the PocketBase instance.
27
- * Together with `adminPassword` this is required to get automatic type generation and to access all resources even if they are not public.
26
+ * Name of the field containing the last update date of an entry.
27
+ * Ideally, this field should be of type `autodate` and have the value "Update" or "Create/Update".
28
+ * This field is used to only fetch entries that have been modified since the last build.
28
29
  */
29
- adminEmail?: string;
30
+ updatedField?: string;
30
31
  /**
31
- * Password of the admin to get full access to the PocketBase instance.
32
- * Together with `adminEmail` this is required to get automatic type generation and to access all resources even if they are not public.
32
+ * Credentials of a superuser to get full access to the PocketBase instance.
33
+ * This is required to get automatic type generation, to access all resources even if they are not public and fetch content of hidden fields.
33
34
  */
34
- adminPassword?: string;
35
+ superuserCredentials?: {
36
+ /**
37
+ * Email of the superuser.
38
+ */
39
+ email: string;
40
+ /**
41
+ * Password of the superuser.
42
+ */
43
+ password: string;
44
+ };
35
45
  /**
36
46
  * File path to the local schema file.
37
47
  * This file will be used to generate the schema for the collection.
38
- * If admin credentials are provided (see `adminEmail` and `adminPassword`), this option will be ignored.
48
+ * If `superuserCredentials` are provided, this option will be ignored.
39
49
  */
40
50
  localSchema?: string;
41
51
  /**
@@ -46,9 +56,4 @@ export interface PocketBaseLoaderOptions {
46
56
  * Note that this will only be used for fields of type `json`.
47
57
  */
48
58
  jsonSchemas?: Record<string, z.ZodSchema>;
49
- /**
50
- * By default, the loader will only fetch entries that have been modified since the last fetch.
51
- * If you want to fetch all entries, set this to `true`.
52
- */
53
- forceUpdate?: boolean;
54
59
  }
@@ -2,6 +2,11 @@
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;
5
10
  /**
6
11
  * Name of the field.
7
12
  */
@@ -15,20 +20,25 @@ export interface PocketBaseSchemaEntry {
15
20
  */
16
21
  required: boolean;
17
22
  /**
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
- };
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;
32
42
  }
33
43
 
34
44
  /**
@@ -42,9 +52,9 @@ export interface PocketBaseCollection {
42
52
  /**
43
53
  * Type of the collection.
44
54
  */
45
- type: 'base' | 'view' | 'auth';
55
+ type: "base" | "view" | "auth";
46
56
  /**
47
57
  * Schema of the collection.
48
58
  */
49
- schema: Array<PocketBaseSchemaEntry>;
59
+ fields: Array<PocketBaseSchemaEntry>;
50
60
  }
@@ -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 { getAdminToken } from "./get-admin-token";
3
+ import { getSuperuserToken } from "./get-superuser-token";
4
4
 
5
5
  /**
6
6
  * Fetches the schema for the specified collection from the PocketBase instance.
@@ -10,18 +10,17 @@ import { getAdminToken } from "./get-admin-token";
10
10
  export async function getRemoteSchema(
11
11
  options: PocketBaseLoaderOptions
12
12
  ): Promise<PocketBaseCollection | undefined> {
13
- if (!options.adminEmail || !options.adminPassword) {
13
+ if (!options.superuserCredentials) {
14
14
  return undefined;
15
15
  }
16
16
 
17
- // Get the admin token
18
- const token = await getAdminToken(
17
+ // Get a superuser token
18
+ const token = await getSuperuserToken(
19
19
  options.url,
20
- options.adminEmail,
21
- options.adminPassword
20
+ options.superuserCredentials
22
21
  );
23
22
 
24
- // If the token is invalid, return the basic schema
23
+ // If the token is invalid try another method
25
24
  if (!token) {
26
25
  return undefined;
27
26
  }
@@ -39,7 +38,7 @@ export async function getRemoteSchema(
39
38
  headers: schemaHeaders
40
39
  });
41
40
 
42
- // If the request was not successful, return the basic schema
41
+ // If the request was not successful, try another method
43
42
  if (!schemaRequest.ok) {
44
43
  const reason = await schemaRequest.json().then((data) => data.message);
45
44
  const errorMessage = `Fetching schema from ${options.collectionName} failed with status code ${schemaRequest.status}.\nReason: ${reason}`;
@@ -1,27 +1,31 @@
1
1
  import type { AstroIntegrationLogger } from "astro";
2
2
 
3
3
  /**
4
- * This function will get an admin token from the given PocketBase instance.
4
+ * This function will get a superuser token from the given PocketBase instance.
5
5
  *
6
6
  * @param url URL of the PocketBase instance
7
- * @param email Email of the admin
8
- * @param password Password of the admin
7
+ * @param superuserCredentials Credentials of the superuser
9
8
  *
10
- * @returns An admin token to access all resources of the PocketBase instance.
9
+ * @returns A superuser token to access all resources of the PocketBase instance.
11
10
  */
12
- export async function getAdminToken(
11
+ export async function getSuperuserToken(
13
12
  url: string,
14
- email: string,
15
- password: string,
13
+ superuserCredentials: {
14
+ email: string;
15
+ password: string;
16
+ },
16
17
  logger?: AstroIntegrationLogger
17
18
  ): Promise<string | undefined> {
18
19
  // Build the URL for the login endpoint
19
- const loginUrl = new URL(`api/admins/auth-with-password`, url).href;
20
+ const loginUrl = new URL(
21
+ `api/collections/_superusers/auth-with-password`,
22
+ url
23
+ ).href;
20
24
 
21
25
  // Create a new FormData object to send the login data
22
26
  const loginData = new FormData();
23
- loginData.set("identity", email);
24
- loginData.set("password", password);
27
+ loginData.set("identity", superuserCredentials.email);
28
+ loginData.set("password", superuserCredentials.password);
25
29
 
26
30
  // Send the login request to get a token
27
31
  const loginRequest = await fetch(loginUrl, {
@@ -12,7 +12,8 @@ import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
12
12
  export async function parseEntry(
13
13
  entry: PocketBaseEntry,
14
14
  { generateDigest, parseData, store }: LoaderContext,
15
- contentFields?: string | Array<string>
15
+ contentFields: string | Array<string> | undefined,
16
+ updatedField: string | undefined
16
17
  ): Promise<void> {
17
18
  // Parse the data to match the schema
18
19
  // This will throw an error if the data does not match the schema
@@ -21,11 +22,15 @@ export async function parseEntry(
21
22
  data: entry
22
23
  });
23
24
 
25
+ // Get the updated date of the entry
26
+ let updated: string | undefined;
27
+ if (updatedField) {
28
+ updated = `${entry[updatedField]}`;
29
+ }
30
+
24
31
  // Generate a digest for the entry
25
- // Normal collections use the updated date that is always updated when the entry is updated.
26
- // If the entry was never updated, the created date can be used as a fallback.
27
- // View collections don't necessarily publish the updated date, so the whole entry is used for the digest.
28
- const digest = generateDigest(entry.updated ?? entry.created ?? entry);
32
+ // If no updated date is available, the digest will be generated from the whole entry
33
+ const digest = generateDigest(updated ?? entry);
29
34
 
30
35
  if (!contentFields) {
31
36
  // Store the entry
@@ -6,13 +6,19 @@ import type {
6
6
 
7
7
  export function parseSchema(
8
8
  collection: PocketBaseCollection,
9
- customSchemas: Record<string, z.ZodType> | undefined
9
+ customSchemas: Record<string, z.ZodType> | undefined,
10
+ hasSuperuserRights: boolean
10
11
  ): Record<string, z.ZodType> {
11
12
  // Prepare the schemas fields
12
13
  const fields: Record<string, z.ZodType> = {};
13
14
 
14
15
  // Parse every field in the schema
15
- for (const field of collection.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
+
16
22
  let fieldType;
17
23
 
18
24
  // Determine the field type and create the corresponding Zod type
@@ -26,11 +32,12 @@ export function parseSchema(
26
32
  fieldType = z.coerce.boolean();
27
33
  break;
28
34
  case "date":
35
+ case "autodate":
29
36
  // Coerce and parse the value as a date
30
37
  fieldType = z.coerce.date();
31
38
  break;
32
39
  case "select":
33
- if (!field.options.values) {
40
+ if (!field.values) {
34
41
  throw new Error(
35
42
  `Field ${field.name} is of type "select" but has no values defined.`
36
43
  );
@@ -38,7 +45,7 @@ export function parseSchema(
38
45
 
39
46
  // Create an enum for the select values
40
47
  // @ts-expect-error - Zod complains because the values are not known at compile time and thus the array is not static.
41
- const values = z.enum(field.options.values);
48
+ const values = z.enum(field.values);
42
49
 
43
50
  // Parse the field type based on the number of values it can have
44
51
  fieldType = parseSingleOrMultipleValues(field, values);
@@ -66,8 +73,12 @@ export function parseSchema(
66
73
  break;
67
74
  }
68
75
 
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
+
69
80
  // If the field is not required, mark it as optional
70
- if (!field.required) {
81
+ if (!isRequired) {
71
82
  fieldType = z.preprocess(
72
83
  (val) => val || undefined,
73
84
  z.optional(fieldType)
@@ -94,7 +105,7 @@ function parseSingleOrMultipleValues(
94
105
  type: z.ZodType
95
106
  ) {
96
107
  // If the select allows multiple values, create an array of the enum
97
- if (field.options.maxSelect === undefined || field.options.maxSelect === 1) {
108
+ if (field.maxSelect === undefined || field.maxSelect === 1) {
98
109
  return type;
99
110
  } else {
100
111
  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.options.maxSelect === 1) {
20
+ if (field.maxSelect === 1) {
21
21
  const fileName = entry[fieldName] as string | undefined;
22
22
  // Check if a file name is present
23
23
  if (!fileName) {