Skip to content

Commit a98f1b4

Browse files
committed
feat(*): add support for PocketBase 0.23.0
BREAKING CHANGE: This also removes support for PocketBase 0.22. There are a lot of breaking changes in this new version of PocketBase, e.g. new endpoint for login, new collection schema format, etc. Since this version already brings a lot of changes, I used this chance to refactor some of the internals and configuration options. Please refer to the new README for more details.
1 parent 7dc5dde commit a98f1b4

13 files changed

+247
-207
lines changed

README.md

+50-29
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@
99

1010
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.
1111

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+
1224
## Basic usage
1325

1426
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({
2739
export const collections = { blog };
2840
```
2941

30-
By default, the loader will only fetch entries that have been modified since the last build.
3142
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.
3243
If you want to update your deployed site with new entries, you need to rebuild it.
3344

3445
<sub>When running the dev server, you can trigger a reload by using `s + enter`.</sub>
3546

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+
3666
## Entries
3767

3868
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
4676
const blog = defineCollection({
4777
loader: pocketbaseLoader({
4878
...options,
49-
content: "<field-in-collection>"
79+
contentFields: "<field-in-collection>"
5080
})
5181
});
5282
```
@@ -70,14 +100,16 @@ These types can be generated in two ways:
70100

71101
### Remote schema
72102

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.
74104

75105
```ts
76106
const blog = defineCollection({
77107
loader: pocketbaseLoader({
78108
...options,
79-
adminEmail: "<admin-email>",
80-
adminPassword: "<admin-password>"
109+
superuserCredentials: {
110+
email: "<superuser-email>",
111+
password: "<superuser-password>"
112+
}
81113
})
82114
});
83115
```
@@ -86,7 +118,7 @@ Under the hood, the loader will use the [PocketBase API](https://pocketbase.io/d
86118

87119
### Local schema
88120

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.
90122

91123
```ts
92124
const blog = defineCollection({
@@ -100,7 +132,7 @@ const blog = defineCollection({
100132
In PocketBase you can export the schema of the whole database to a `pb_schema.json` file.
101133
If you provide the path to this file, the loader will use this schema to generate the types locally.
102134

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.
104136

105137
### Manual schema
106138

@@ -109,36 +141,25 @@ This manual schema will **always override the automatic type generation**.
109141

110142
## All options
111143

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. |
122153

123154
## Special cases
124155

125-
### Private collections
156+
### Private collections and hidden fields
126157

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.
128159
Otherwise, you need to make the collection public in the PocketBase dashboard.
129160

130161
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.
131162

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-
142163
### JSON fields
143164

144165
PocketBase can store arbitrary JSON data in a `json` field.

src/cleanup-entries.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.
66
*
77
* @param options Options for the loader.
88
* @param context Context of the loader.
9-
* @param adminToken Admin token to access all resources.
9+
* @param superuserToken Superuser token to access all resources.
1010
*/
1111
export async function cleanupEntries(
1212
options: PocketBaseLoaderOptions,
1313
context: LoaderContext,
14-
adminToken: string | undefined
14+
superuserToken: string | undefined
1515
): Promise<void> {
1616
// Build the URL for the collections endpoint
1717
const collectionUrl = new URL(
1818
`api/collections/${options.collectionName}/records`,
1919
options.url
2020
).href;
2121

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)
2323
const collectionHeaders = new Headers();
24-
if (adminToken) {
25-
collectionHeaders.set("Authorization", adminToken);
24+
if (superuserToken) {
25+
collectionHeaders.set("Authorization", superuserToken);
2626
}
2727

2828
// Prepare pagination variables
@@ -42,18 +42,18 @@ export async function cleanupEntries(
4242

4343
// If the request was not successful, print the error message and return
4444
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
4646
if (collectionRequest.status === 403) {
4747
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.`
4949
);
5050
return;
5151
}
5252

5353
const reason = await collectionRequest
5454
.json()
5555
.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}`;
5757
context.logger.error(errorMessage);
5858
return;
5959
}
@@ -86,7 +86,7 @@ export async function cleanupEntries(
8686
if (cleanedUp > 0) {
8787
// Log the number of cleaned up entries
8888
context.logger.info(
89-
`Cleaned up ${cleanedUp} old entries for ${context.collection}`
89+
`(${options.collectionName}) Cleaned up ${cleanedUp} old entries.`
9090
);
9191
}
9292
}

src/generate-schema.ts

+45-29
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,15 @@ import { transformFiles } from "./utils/transform-files";
1111
* Basic schema for every PocketBase collection.
1212
*/
1313
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 = {
2514
id: z.string(),
2615
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()
3017
};
3118

3219
/**
3320
* Generate a schema for the collection based on the collection's schema in PocketBase.
3421
* 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.
3623
* If a path to a local schema file is provided, the schema is read from the file.
3724
*
3825
* @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
@@ -45,6 +32,8 @@ export async function generateSchema(
4532
// Try to get the schema directly from the PocketBase instance
4633
collection = await getRemoteSchema(options);
4734

35+
const hasSuperuserRights = !!collection || !!options.superuserCredentials;
36+
4837
// If the schema is not available, try to read it from a local schema file
4938
if (!collection && options.localSchema) {
5039
collection = await readLocalSchema(
@@ -56,22 +45,29 @@ export async function generateSchema(
5645
// If the schema is still not available, return the basic schema
5746
if (!collection) {
5847
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.`
6049
);
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);
6352
}
6453

6554
// Parse the schema
66-
const fields = parseSchema(collection, options.jsonSchemas);
55+
const fields = parseSchema(
56+
collection,
57+
options.jsonSchemas,
58+
hasSuperuserRights
59+
);
6760

6861
// 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+
) {
7066
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}".`
7268
);
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) {
7571
if (!fields[field]) {
7672
console.error(
7773
`The content field "${field}" is not present in the schema of the collection "${options.collectionName}".`
@@ -80,26 +76,46 @@ export async function generateSchema(
8076
}
8177
}
8278

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+
}
86100

87101
// Combine the basic schema with the parsed fields
88102
const schema = z.object({
89-
...base,
103+
...BASIC_SCHEMA,
90104
...fields
91105
});
92106

93107
// 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);
95112

96113
if (fileFields.length === 0) {
97114
return schema;
98115
}
99116

100117
// Transform file names to file urls
101118
return schema.transform((entry) =>
102-
// @ts-expect-error - `updated` and `created` are already transformed to dates
103119
transformFiles(options.url, fileFields, entry)
104120
);
105121
}

0 commit comments

Comments
 (0)