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