astro-loader-pocketbase 0.1.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 +42 -0
- package/index.ts +5 -0
- package/package.json +38 -0
- package/src/cleanup-entries.ts +92 -0
- package/src/generate-schema.ts +99 -0
- package/src/load-entries.ts +97 -0
- package/src/pocketbase-loader.ts +55 -0
- package/src/types/pocketbase-loader-options.type.ts +38 -0
- package/src/utils/get-admin-token.ts +46 -0
- package/src/utils/parse-entry.ts +49 -0
- package/src/utils/parse-schema.ts +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# astro-pocketbase-loader
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
In your content configuration file, you can use the `pocketbaseLoader` function to use your PocketBase database as a data source.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { pocketbaseLoader } from "astro-pocketbase-loader";
|
|
11
|
+
import { defineCollection } from "astro:content";
|
|
12
|
+
|
|
13
|
+
const blog = defineCollection({
|
|
14
|
+
loader: pocketbaseLoader({
|
|
15
|
+
url: "https://<your-pocketbase-url>",
|
|
16
|
+
collectionName: "<collection-in-pocketbase>",
|
|
17
|
+
content: "<field-in-collection>"
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Type Generation
|
|
23
|
+
|
|
24
|
+
The loader can automatically generate types for your collection.
|
|
25
|
+
To do this, you need to provide the email and password of an admin of the PocketBase instance.
|
|
26
|
+
Under the hood, the loader will use the [PocketBase API](https://pocketbase.io/docs/api-collections/#view-collection) to fetch the schema of your collection and generate types with Zod based on that schema.
|
|
27
|
+
|
|
28
|
+
### Private Collections
|
|
29
|
+
|
|
30
|
+
If you want to access a private collection, you also need to provide the admin credentials.
|
|
31
|
+
Otherwise, you need to make the collection public in the PocketBase dashboard.
|
|
32
|
+
|
|
33
|
+
### Options
|
|
34
|
+
|
|
35
|
+
| Option | Type | Required | Description |
|
|
36
|
+
| ---------------- | ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `url` | `string` | x | The URL of your PocketBase instance. |
|
|
38
|
+
| `collectionName` | `string` | x | The name of the collection in your PocketBase instance. |
|
|
39
|
+
| `content` | `string \| Array<string>` | x | The field in the collection to use as content. This can also be an array of fields. |
|
|
40
|
+
| `adminEmail` | `string` | | The email of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
|
|
41
|
+
| `adminPassword` | `string` | | The password of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
|
|
42
|
+
| `forceUpdate` | `boolean` | | If set to `true`, the loader will fetch every entry instead of only the ones modified since the last build. |
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astro-loader-pocketbase",
|
|
3
|
+
"description": "A content loader for Astro that uses the PocketBase API",
|
|
4
|
+
"homepage": "https://github.com/pawcoding/astro-loader-pocketbase",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"index.ts"
|
|
13
|
+
],
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Luis Wolf",
|
|
16
|
+
"email": "development@pawcode.de",
|
|
17
|
+
"url": "https://pawcode.de"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"withastro",
|
|
21
|
+
"astro",
|
|
22
|
+
"astro-loader",
|
|
23
|
+
"astro-content-loader",
|
|
24
|
+
"pocketbase"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"astro": "^5.0.0-beta.1"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"astro": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@astrojs/check": "^0.9.3",
|
|
35
|
+
"typescript": "^5.6.2"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { LoaderContext } from "astro/loaders";
|
|
2
|
+
import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cleanup entries that are no longer in the collection.
|
|
6
|
+
*
|
|
7
|
+
* @param options Options for the loader.
|
|
8
|
+
* @param context Context of the loader.
|
|
9
|
+
* @param adminToken Admin token to access all resources.
|
|
10
|
+
*/
|
|
11
|
+
export async function cleanupEntries(
|
|
12
|
+
options: PocketBaseLoaderOptions,
|
|
13
|
+
context: LoaderContext,
|
|
14
|
+
adminToken: string | undefined
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
// Build the URL for the collections endpoint
|
|
17
|
+
const collectionUrl = new URL(
|
|
18
|
+
`api/collections/${options.collectionName}/records`,
|
|
19
|
+
options.url
|
|
20
|
+
).href;
|
|
21
|
+
|
|
22
|
+
// Create the headers for the request to append the admin token (if available)
|
|
23
|
+
const collectionHeaders = new Headers();
|
|
24
|
+
if (adminToken) {
|
|
25
|
+
collectionHeaders.set("Authorization", adminToken);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Prepare pagination variables
|
|
29
|
+
let page = 1;
|
|
30
|
+
let totalPages = 0;
|
|
31
|
+
const entries = new Set<string>();
|
|
32
|
+
|
|
33
|
+
// Fetch all ids of the collection
|
|
34
|
+
do {
|
|
35
|
+
// Fetch ids from the collection
|
|
36
|
+
const collectionRequest = await fetch(
|
|
37
|
+
`${collectionUrl}?page=${page}&perPage=1000&fields=id`,
|
|
38
|
+
{
|
|
39
|
+
headers: collectionHeaders
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// If the request was not successful, print the error message and return
|
|
44
|
+
if (!collectionRequest.ok) {
|
|
45
|
+
// If the collection is locked, an admin token is required
|
|
46
|
+
if (collectionRequest.status === 403) {
|
|
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.`
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const reason = await collectionRequest
|
|
54
|
+
.json()
|
|
55
|
+
.then((data) => data.message);
|
|
56
|
+
const errorMessage = `Fetching ids from ${options.collectionName} failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
|
|
57
|
+
context.logger.error(errorMessage);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get the data from the response
|
|
62
|
+
const response = await collectionRequest.json();
|
|
63
|
+
|
|
64
|
+
// Add the ids to the set
|
|
65
|
+
for (const item of response.items) {
|
|
66
|
+
entries.add(item.id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update the page and total pages
|
|
70
|
+
page = response.page;
|
|
71
|
+
totalPages = response.totalPages;
|
|
72
|
+
} while (page < totalPages);
|
|
73
|
+
|
|
74
|
+
let cleanedUp = 0;
|
|
75
|
+
|
|
76
|
+
// Get all ids of the entries in the store
|
|
77
|
+
const storedIds = context.store.keys();
|
|
78
|
+
for (const id of storedIds) {
|
|
79
|
+
// If the id is not in the entries set, remove the entry from the store
|
|
80
|
+
if (!entries.has(id)) {
|
|
81
|
+
context.store.delete(id);
|
|
82
|
+
cleanedUp++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cleanedUp > 0) {
|
|
87
|
+
// Log the number of cleaned up entries
|
|
88
|
+
context.logger.info(
|
|
89
|
+
`Cleaned up ${cleanedUp} old entries for ${context.collection}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ZodSchema } from "astro/zod";
|
|
2
|
+
import { z } from "astro/zod";
|
|
3
|
+
import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
|
|
4
|
+
import { getAdminToken } from "./utils/get-admin-token";
|
|
5
|
+
import { parseSchema } from "./utils/parse-schema";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Basic schema for every PocketBase collection.
|
|
9
|
+
*/
|
|
10
|
+
const BASIC_SCHEMA = {
|
|
11
|
+
id: z.string().length(15),
|
|
12
|
+
collectionId: z.string().length(15),
|
|
13
|
+
collectionName: z.string(),
|
|
14
|
+
created: z.date({ coerce: true }),
|
|
15
|
+
updated: z.date({ coerce: true })
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a schema for the collection based on the collection's schema in PocketBase.
|
|
20
|
+
* If no login credentials are provided, a {@link BASIC_SCHEMA} for every PocketBase collection is returned.
|
|
21
|
+
*
|
|
22
|
+
* @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
|
|
23
|
+
*/
|
|
24
|
+
export async function generateSchema(
|
|
25
|
+
options: PocketBaseLoaderOptions
|
|
26
|
+
): Promise<ZodSchema> {
|
|
27
|
+
// TODO: Add support for local schema files
|
|
28
|
+
|
|
29
|
+
// If no admin email and password are provided, we can't get the schema from the API
|
|
30
|
+
if (!options.adminEmail || !options.adminPassword) {
|
|
31
|
+
console.warn(
|
|
32
|
+
"Make sure to add an admin email and password to the config to get automatic type generation."
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Return the basic schema
|
|
36
|
+
return z.object(BASIC_SCHEMA);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get the admin token
|
|
40
|
+
const token = await getAdminToken(
|
|
41
|
+
options.url,
|
|
42
|
+
options.adminEmail,
|
|
43
|
+
options.adminPassword
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// If the token is invalid, return the basic schema
|
|
47
|
+
if (!token) {
|
|
48
|
+
return z.object(BASIC_SCHEMA);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Build URL and headers for the schema request
|
|
52
|
+
const schemaUrl = new URL(
|
|
53
|
+
`api/collections/${options.collectionName}`,
|
|
54
|
+
options.url
|
|
55
|
+
).href;
|
|
56
|
+
const schemaHeaders = new Headers();
|
|
57
|
+
schemaHeaders.set("Authorization", token);
|
|
58
|
+
|
|
59
|
+
// Fetch the schema
|
|
60
|
+
const schemaRequest = await fetch(schemaUrl, {
|
|
61
|
+
headers: schemaHeaders
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// If the request was not successful, return the basic schema
|
|
65
|
+
if (!schemaRequest.ok) {
|
|
66
|
+
const reason = await schemaRequest.json().then((data) => data.message);
|
|
67
|
+
const errorMessage = `Fetching schema from ${options.collectionName} failed with status code ${schemaRequest.status}.\nReason: ${reason}`;
|
|
68
|
+
console.error(errorMessage);
|
|
69
|
+
|
|
70
|
+
return z.object(BASIC_SCHEMA);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get the schema from the response
|
|
74
|
+
const schema = await schemaRequest.json();
|
|
75
|
+
|
|
76
|
+
// Parse the schema
|
|
77
|
+
const fields = parseSchema(schema.schema);
|
|
78
|
+
|
|
79
|
+
// Check if the content field is present
|
|
80
|
+
if (typeof options.content === "string" && !fields[options.content]) {
|
|
81
|
+
console.error(
|
|
82
|
+
`The content field "${options.content}" is not present in the schema of the collection "${options.collectionName}".`
|
|
83
|
+
);
|
|
84
|
+
} else if (Array.isArray(options.content)) {
|
|
85
|
+
for (const field of options.content) {
|
|
86
|
+
if (!fields[field]) {
|
|
87
|
+
console.error(
|
|
88
|
+
`The content field "${field}" is not present in the schema of the collection "${options.collectionName}".`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Combine the basic schema with the parsed fields
|
|
95
|
+
return z.object({
|
|
96
|
+
...BASIC_SCHEMA,
|
|
97
|
+
...fields
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { LoaderContext } from "astro/loaders";
|
|
2
|
+
import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
|
|
3
|
+
import { parseEntry } from "./utils/parse-entry";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load (modified) entries from a PocketBase collection.
|
|
7
|
+
*
|
|
8
|
+
* @param options Options for the loader.
|
|
9
|
+
* @param context Context of the loader.
|
|
10
|
+
* @param adminToken Admin token to access all resources.
|
|
11
|
+
* @param lastModified Date of the last fetch to only update changed entries.
|
|
12
|
+
*/
|
|
13
|
+
export async function loadEntries(
|
|
14
|
+
options: PocketBaseLoaderOptions,
|
|
15
|
+
context: LoaderContext,
|
|
16
|
+
adminToken: string | undefined,
|
|
17
|
+
lastModified: string | undefined
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
// Build the URL for the collections endpoint
|
|
20
|
+
const collectionUrl = new URL(
|
|
21
|
+
`api/collections/${options.collectionName}/records`,
|
|
22
|
+
options.url
|
|
23
|
+
).href;
|
|
24
|
+
|
|
25
|
+
// Create the headers for the request to append the admin token (if available)
|
|
26
|
+
const collectionHeaders = new Headers();
|
|
27
|
+
if (adminToken) {
|
|
28
|
+
collectionHeaders.set("Authorization", adminToken);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Log the fetching of the entries
|
|
32
|
+
context.logger.info(
|
|
33
|
+
`Fetching${lastModified ? " modified" : ""} data for ${
|
|
34
|
+
context.collection
|
|
35
|
+
} from ${collectionUrl}${
|
|
36
|
+
lastModified ? ` starting at ${lastModified}` : ""
|
|
37
|
+
}${adminToken ? " with admin token" : ""}`
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Prepare pagination variables
|
|
41
|
+
let page = 1;
|
|
42
|
+
let totalPages = 0;
|
|
43
|
+
let entries = 0;
|
|
44
|
+
|
|
45
|
+
// Fetch all (modified) entries
|
|
46
|
+
do {
|
|
47
|
+
// Fetch entries from the collection
|
|
48
|
+
// If `lastModified` is set, only fetch entries that have been modified since the last fetch
|
|
49
|
+
const collectionRequest = await fetch(
|
|
50
|
+
`${collectionUrl}?page=${page}&perPage=100&sort=-updated,id${
|
|
51
|
+
lastModified ? `&filter=(updated>"${lastModified}")` : ""
|
|
52
|
+
}`,
|
|
53
|
+
{
|
|
54
|
+
headers: collectionHeaders
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// If the request was not successful, print the error message and return
|
|
59
|
+
if (!collectionRequest.ok) {
|
|
60
|
+
// If the collection is locked, an admin token is required
|
|
61
|
+
if (collectionRequest.status === 403) {
|
|
62
|
+
context.logger.error(
|
|
63
|
+
`The collection ${options.collectionName} is not accessible without an admin rights. Please provide an admin email and password in the config.`
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Get the reason for the error
|
|
69
|
+
const reason = await collectionRequest
|
|
70
|
+
.json()
|
|
71
|
+
.then((data) => data.message);
|
|
72
|
+
const errorMessage = `Fetching data from ${options.collectionName} failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
|
|
73
|
+
context.logger.error(errorMessage);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get the data from the response
|
|
78
|
+
const response = await collectionRequest.json();
|
|
79
|
+
|
|
80
|
+
// Parse and store the entries
|
|
81
|
+
for (const entry of response.items) {
|
|
82
|
+
await parseEntry(entry, context, options.content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Update the page and total pages
|
|
86
|
+
page = response.page;
|
|
87
|
+
totalPages = response.totalPages;
|
|
88
|
+
entries += response.items.length;
|
|
89
|
+
} while (page < totalPages);
|
|
90
|
+
|
|
91
|
+
// Log the number of fetched entries
|
|
92
|
+
context.logger.info(
|
|
93
|
+
`Fetched ${entries}${lastModified ? " changed" : ""} entries for ${
|
|
94
|
+
context.collection
|
|
95
|
+
}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Loader, LoaderContext } from "astro/loaders";
|
|
2
|
+
import { cleanupEntries } from "./cleanup-entries";
|
|
3
|
+
import { generateSchema } from "./generate-schema";
|
|
4
|
+
import { loadEntries } from "./load-entries";
|
|
5
|
+
import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
|
|
6
|
+
import { getAdminToken } from "./utils/get-admin-token";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Loader for collections stored in PocketBase.
|
|
10
|
+
*
|
|
11
|
+
* @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
|
|
12
|
+
*/
|
|
13
|
+
export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
|
|
14
|
+
return {
|
|
15
|
+
name: "pocketbase-loader",
|
|
16
|
+
load: async (context: LoaderContext): Promise<void> => {
|
|
17
|
+
// Get the date of the last fetch to only update changed entries.
|
|
18
|
+
// If `forceUpdate` is set to `true`, this will be `undefined` to fetch all entries again.
|
|
19
|
+
const lastModified = options.forceUpdate
|
|
20
|
+
? undefined
|
|
21
|
+
: context.meta.get("last-modified");
|
|
22
|
+
|
|
23
|
+
// Clear the store if we want to fetch all entries again
|
|
24
|
+
if (options.forceUpdate) {
|
|
25
|
+
context.store.clear();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Try to get an admin token to access all resources.
|
|
29
|
+
let token: string | undefined;
|
|
30
|
+
if (options.adminEmail && options.adminPassword) {
|
|
31
|
+
token = await getAdminToken(
|
|
32
|
+
options.url,
|
|
33
|
+
options.adminEmail,
|
|
34
|
+
options.adminPassword,
|
|
35
|
+
context.logger
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (context.store.keys().length > 0) {
|
|
40
|
+
// Cleanup entries that are no longer in the collection
|
|
41
|
+
await cleanupEntries(options, context, token);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Load the (modified) entries
|
|
45
|
+
await loadEntries(options, context, token, lastModified);
|
|
46
|
+
|
|
47
|
+
// Set the last modified date to the current date
|
|
48
|
+
context.meta.set("last-modified", new Date().toISOString());
|
|
49
|
+
},
|
|
50
|
+
schema: async () => {
|
|
51
|
+
// Generate the schema for the collection according to the API
|
|
52
|
+
return await generateSchema(options);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the PocketBase loader.
|
|
3
|
+
*/
|
|
4
|
+
export type PocketBaseLoaderOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* URL of the PocketBase instance.
|
|
7
|
+
*/
|
|
8
|
+
url: string;
|
|
9
|
+
/**
|
|
10
|
+
* Name of the collection in PocketBase.
|
|
11
|
+
*/
|
|
12
|
+
collectionName: string;
|
|
13
|
+
/**
|
|
14
|
+
* Content of the collection in PocketBase.
|
|
15
|
+
* This must be the name of a field in the collection that contains the content.
|
|
16
|
+
* The content will be parsed as HTML and rendered to the page.
|
|
17
|
+
*
|
|
18
|
+
* If you want to render multiple fields as main content, you can pass an array of field names.
|
|
19
|
+
* The loader will concatenate the content of all fields in the order they are defined in the array.
|
|
20
|
+
* Each block will be contained in a `<section>` element.
|
|
21
|
+
*/
|
|
22
|
+
content: string | Array<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Email of an admin to get full access to the PocketBase instance.
|
|
25
|
+
* Together with `adminPassword` this is required to get automatic type generation and to access all resources even if they are not public.
|
|
26
|
+
*/
|
|
27
|
+
adminEmail?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Password of the admin to get full access to the PocketBase instance.
|
|
30
|
+
* Together with `adminEmail` this is required to get automatic type generation and to access all resources even if they are not public.
|
|
31
|
+
*/
|
|
32
|
+
adminPassword?: string;
|
|
33
|
+
/**
|
|
34
|
+
* By default, the loader will only fetch entries that have been modified since the last fetch.
|
|
35
|
+
* If you want to fetch all entries, set this to `true`.
|
|
36
|
+
*/
|
|
37
|
+
forceUpdate?: boolean;
|
|
38
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AstroIntegrationLogger } from "astro";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This function will get an admin token from the given PocketBase instance.
|
|
5
|
+
*
|
|
6
|
+
* @param url URL of the PocketBase instance
|
|
7
|
+
* @param email Email of the admin
|
|
8
|
+
* @param password Password of the admin
|
|
9
|
+
*
|
|
10
|
+
* @returns An admin token to access all resources of the PocketBase instance.
|
|
11
|
+
*/
|
|
12
|
+
export async function getAdminToken(
|
|
13
|
+
url: string,
|
|
14
|
+
email: string,
|
|
15
|
+
password: string,
|
|
16
|
+
logger?: AstroIntegrationLogger
|
|
17
|
+
): Promise<string | undefined> {
|
|
18
|
+
// Build the URL for the login endpoint
|
|
19
|
+
const loginUrl = new URL(`api/admins/auth-with-password`, url).href;
|
|
20
|
+
|
|
21
|
+
// Create a new FormData object to send the login data
|
|
22
|
+
const loginData = new FormData();
|
|
23
|
+
loginData.set("identity", email);
|
|
24
|
+
loginData.set("password", password);
|
|
25
|
+
|
|
26
|
+
// Send the login request to get a token
|
|
27
|
+
const loginRequest = await fetch(loginUrl, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
body: loginData
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// If the login request was not successful, print the error message and return undefined
|
|
33
|
+
if (!loginRequest.ok) {
|
|
34
|
+
const reason = await loginRequest.json().then((data) => data.message);
|
|
35
|
+
const errorMessage = `The given email / password for ${url} was not correct. Astro can't generate type definitions automatically and may not have access to all resources.\nReason: ${reason}`;
|
|
36
|
+
if (logger) {
|
|
37
|
+
logger.error(errorMessage);
|
|
38
|
+
} else {
|
|
39
|
+
console.error(errorMessage);
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Return the token
|
|
45
|
+
return await loginRequest.json().then((data) => data.token);
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { LoaderContext } from "astro/loaders";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse an entry from PocketBase to match the schema and store it in the store.
|
|
5
|
+
*
|
|
6
|
+
* @param entry Entry to parse.
|
|
7
|
+
* @param context Context of the loader.
|
|
8
|
+
* @param contentFields Field(s) to use as content for the entry.
|
|
9
|
+
* If multiple fields are used, they will be concatenated and wrapped in `<section>` elements.
|
|
10
|
+
*/
|
|
11
|
+
export async function parseEntry(
|
|
12
|
+
entry: Record<string, any>,
|
|
13
|
+
{ generateDigest, parseData, store }: LoaderContext,
|
|
14
|
+
contentFields: string | Array<string>
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
// Parse the data to match the schema
|
|
17
|
+
// This will throw an error if the data does not match the schema
|
|
18
|
+
const data = await parseData({
|
|
19
|
+
id: entry.id,
|
|
20
|
+
data: entry
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Generate a digest for the entry
|
|
24
|
+
// We can use the updated data as an identifier if the entry has changed since PocketBase automatically updates this value on every change
|
|
25
|
+
const digest = generateDigest(entry.updated);
|
|
26
|
+
|
|
27
|
+
// Generate the content for the entry
|
|
28
|
+
let content: string;
|
|
29
|
+
if (typeof contentFields === "string") {
|
|
30
|
+
// Only one field is used as content
|
|
31
|
+
content = entry[contentFields];
|
|
32
|
+
} else {
|
|
33
|
+
// Multiple fields are used as content, wrap each block in a section and concatenate them
|
|
34
|
+
content = contentFields
|
|
35
|
+
.map((field) => entry[field])
|
|
36
|
+
.map((block) => `<section>${block}</section>`)
|
|
37
|
+
.join("");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Store the entry
|
|
41
|
+
store.set({
|
|
42
|
+
id: entry.id,
|
|
43
|
+
data,
|
|
44
|
+
digest,
|
|
45
|
+
rendered: {
|
|
46
|
+
html: content
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "astro/zod";
|
|
2
|
+
|
|
3
|
+
export function parseSchema(
|
|
4
|
+
schema: Array<Record<string, any>>
|
|
5
|
+
): Record<string, z.ZodType> {
|
|
6
|
+
// Prepare the schemas fields
|
|
7
|
+
const fields: Record<string, z.ZodType> = {};
|
|
8
|
+
|
|
9
|
+
// Parse every field in the schema
|
|
10
|
+
for (const field of schema) {
|
|
11
|
+
let fieldType;
|
|
12
|
+
|
|
13
|
+
// Determine the field type and create the corresponding Zod type
|
|
14
|
+
switch (field.type) {
|
|
15
|
+
case "number":
|
|
16
|
+
// Coerce the value to a number
|
|
17
|
+
fieldType = z.number({ coerce: true });
|
|
18
|
+
break;
|
|
19
|
+
case "bool":
|
|
20
|
+
// Coerce the value to a boolean
|
|
21
|
+
fieldType = z.boolean({ coerce: true });
|
|
22
|
+
break;
|
|
23
|
+
case "date":
|
|
24
|
+
// Coerce and parse the value as a date
|
|
25
|
+
fieldType = z.date({ coerce: true });
|
|
26
|
+
break;
|
|
27
|
+
case "select":
|
|
28
|
+
// Create an enum for the select values
|
|
29
|
+
const values = z.enum(field.options.values);
|
|
30
|
+
|
|
31
|
+
// Parse the field type based on the number of values it can have
|
|
32
|
+
fieldType = parseSingleOrMultipleValues(field as any, values);
|
|
33
|
+
break;
|
|
34
|
+
case "relation":
|
|
35
|
+
case "file":
|
|
36
|
+
// NOTE: Relations and files are currently not supported and are treated as strings
|
|
37
|
+
|
|
38
|
+
// Parse the field type based on the number of values it can have
|
|
39
|
+
fieldType = parseSingleOrMultipleValues(field as any, z.string());
|
|
40
|
+
break;
|
|
41
|
+
case "json":
|
|
42
|
+
// Parse the field as an object
|
|
43
|
+
fieldType = z.object({});
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
// Default to a string
|
|
47
|
+
fieldType = z.string();
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If the field is not required, mark it as optional
|
|
52
|
+
if (!field.required) {
|
|
53
|
+
fieldType.isOptional();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Add the field to the fields object
|
|
57
|
+
fields[field.name] = fieldType;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return fields;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse the field type based on the number of values it can have
|
|
65
|
+
*
|
|
66
|
+
* @param field Field to parse
|
|
67
|
+
* @param type Type of each value
|
|
68
|
+
*
|
|
69
|
+
* @returns The parsed field type
|
|
70
|
+
*/
|
|
71
|
+
function parseSingleOrMultipleValues(
|
|
72
|
+
field: { options: { maxSelect: number } },
|
|
73
|
+
type: z.ZodType
|
|
74
|
+
) {
|
|
75
|
+
// If the select allows multiple values, create an array of the enum
|
|
76
|
+
if (field.options.maxSelect === 1) {
|
|
77
|
+
return type;
|
|
78
|
+
} else {
|
|
79
|
+
return z.array(type);
|
|
80
|
+
}
|
|
81
|
+
}
|