astro-loader-pocketbase 2.8.0-next.3 → 2.8.0-next.5
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 +18 -0
- package/package.json +1 -1
- package/src/loader/cleanup-entries.ts +2 -2
- package/src/loader/fetch-collection.ts +23 -19
- package/src/loader/fetch-entry.ts +11 -2
- package/src/schema/generate-schema.ts +106 -68
- package/src/schema/get-remote-schema.ts +1 -1
- package/src/schema/parse-schema.ts +26 -6
- package/src/types/pocketbase-loader-options.type.ts +52 -23
- package/src/utils/combine-fields-for-request.ts +55 -0
- package/src/utils/create-token-promise.ts +2 -2
- package/src/utils/extract-field-names.ts +15 -0
- package/src/utils/format-fields.ts +63 -0
package/README.md
CHANGED
|
@@ -130,6 +130,24 @@ This filter will be added to the PocketBase API request and will only fetch entr
|
|
|
130
130
|
This is in addition to the built-in filtering of the loader, which handles the incremental builds using the `updated` field.
|
|
131
131
|
For more information on how to use filters, check out the [PocketBase documentation](https://pocketbase.io/docs/api-records/#listsearch-records).
|
|
132
132
|
|
|
133
|
+
### Partial data loading
|
|
134
|
+
|
|
135
|
+
By default, the loader fetches all fields for each entry in your PocketBase collection.
|
|
136
|
+
If you want to optimize data loading or restrict the fields available in your content collection, you can use the `fields` option.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const blog = defineCollection({
|
|
140
|
+
loader: pocketbaseLoader({
|
|
141
|
+
...options,
|
|
142
|
+
fields: ["title", "summary", "coverImage"]
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This parameter will be added to the PocketBase API request and will only return these fields for each entry.
|
|
148
|
+
Additional system fields like `id`, `collectionName` and `collectionId`, as well as any fields specified for `idField`, `updatedField` or `contentFields` will be added automatically.
|
|
149
|
+
For further details on field selection, see the [PocketBase API documentation](https://pocketbase.io/docs/api-records/#listsearch-records).
|
|
150
|
+
|
|
133
151
|
## Type generation
|
|
134
152
|
|
|
135
153
|
The loader can automatically generate types for your collection.
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LoaderContext } from "astro/loaders";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PocketBaseLoaderBaseOptions } from "../types/pocketbase-loader-options.type";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Cleanup entries that are no longer in the collection.
|
|
@@ -9,7 +9,7 @@ import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options
|
|
|
9
9
|
* @param superuserToken Superuser token to access all resources.
|
|
10
10
|
*/
|
|
11
11
|
export async function cleanupEntries(
|
|
12
|
-
options:
|
|
12
|
+
options: PocketBaseLoaderBaseOptions,
|
|
13
13
|
context: LoaderContext,
|
|
14
14
|
superuserToken: string | undefined
|
|
15
15
|
): Promise<void> {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
|
|
2
2
|
import type { ExperimentalPocketBaseLiveLoaderCollectionFilter } from "../types/pocketbase-live-loader-filter.type";
|
|
3
|
-
import type {
|
|
3
|
+
import type { PocketBaseLoaderBaseOptions } from "../types/pocketbase-loader-options.type";
|
|
4
|
+
import { combineFieldsForRequest } from "../utils/combine-fields-for-request";
|
|
5
|
+
import { formatFields } from "../utils/format-fields";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Provides utilities to fetch entries from a PocketBase collection, supporting filtering and pagination.
|
|
@@ -17,14 +19,7 @@ export type CollectionFilter = {
|
|
|
17
19
|
* Fetches entries from a PocketBase collection, optionally filtering by modification date and supporting pagination.
|
|
18
20
|
*/
|
|
19
21
|
export async function fetchCollection<TEntry extends PocketBaseEntry>(
|
|
20
|
-
options:
|
|
21
|
-
PocketBaseLoaderOptions,
|
|
22
|
-
| "collectionName"
|
|
23
|
-
| "url"
|
|
24
|
-
| "updatedField"
|
|
25
|
-
| "filter"
|
|
26
|
-
| "superuserCredentials"
|
|
27
|
-
>,
|
|
22
|
+
options: PocketBaseLoaderBaseOptions,
|
|
28
23
|
chunkLoaded: (entries: Array<TEntry>) => Promise<void>,
|
|
29
24
|
token: string | undefined,
|
|
30
25
|
collectionFilter: CollectionFilter | undefined
|
|
@@ -33,7 +28,7 @@ export async function fetchCollection<TEntry extends PocketBaseEntry>(
|
|
|
33
28
|
const collectionUrl = new URL(
|
|
34
29
|
`api/collections/${options.collectionName}/records`,
|
|
35
30
|
options.url
|
|
36
|
-
)
|
|
31
|
+
);
|
|
37
32
|
|
|
38
33
|
// Create the headers for the request to append the token (if available)
|
|
39
34
|
const collectionHeaders = new Headers();
|
|
@@ -41,25 +36,29 @@ export async function fetchCollection<TEntry extends PocketBaseEntry>(
|
|
|
41
36
|
collectionHeaders.set("Authorization", token);
|
|
42
37
|
}
|
|
43
38
|
|
|
39
|
+
// Cache fields computation outside the loop
|
|
40
|
+
const fieldsArray = formatFields(options.fields);
|
|
41
|
+
const combinedFields = combineFieldsForRequest(fieldsArray, options);
|
|
42
|
+
|
|
44
43
|
// Prepare pagination variables
|
|
45
44
|
let page = 0;
|
|
46
45
|
let totalPages = 0;
|
|
47
46
|
|
|
48
47
|
// Fetch all (modified) entries
|
|
49
48
|
do {
|
|
50
|
-
const searchParams = buildSearchParams(options, {
|
|
49
|
+
const searchParams = buildSearchParams(options, combinedFields, {
|
|
51
50
|
...collectionFilter,
|
|
52
51
|
page: collectionFilter?.page ?? ++page,
|
|
53
52
|
perPage: collectionFilter?.perPage ?? 100
|
|
54
53
|
});
|
|
55
54
|
|
|
55
|
+
// Apply search parameters to URL
|
|
56
|
+
collectionUrl.search = searchParams.toString();
|
|
57
|
+
|
|
56
58
|
// Fetch entries from the collection
|
|
57
|
-
const collectionRequest = await fetch(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
headers: collectionHeaders
|
|
61
|
-
}
|
|
62
|
-
);
|
|
59
|
+
const collectionRequest = await fetch(collectionUrl.href, {
|
|
60
|
+
headers: collectionHeaders
|
|
61
|
+
});
|
|
63
62
|
|
|
64
63
|
// If the request was not successful, print the error message and return
|
|
65
64
|
if (!collectionRequest.ok) {
|
|
@@ -101,10 +100,10 @@ export async function fetchCollection<TEntry extends PocketBaseEntry>(
|
|
|
101
100
|
* Build search parameters for the PocketBase collection request.
|
|
102
101
|
*/
|
|
103
102
|
function buildSearchParams(
|
|
104
|
-
loaderOptions:
|
|
103
|
+
loaderOptions: PocketBaseLoaderBaseOptions,
|
|
104
|
+
combinedFields: Array<string> | undefined,
|
|
105
105
|
collectionFilter: CollectionFilter
|
|
106
106
|
): URLSearchParams {
|
|
107
|
-
// Build search parameters
|
|
108
107
|
const searchParams = new URLSearchParams();
|
|
109
108
|
|
|
110
109
|
if (collectionFilter.page) {
|
|
@@ -145,5 +144,10 @@ function buildSearchParams(
|
|
|
145
144
|
searchParams.set("sort", collectionFilter.sort);
|
|
146
145
|
}
|
|
147
146
|
|
|
147
|
+
// Add fields parameter if specified
|
|
148
|
+
if (combinedFields) {
|
|
149
|
+
searchParams.set("fields", combinedFields.join(","));
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
return searchParams;
|
|
149
153
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
|
|
2
2
|
import type { ExperimentalPocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
|
|
3
|
+
import { combineFieldsForRequest } from "../utils/combine-fields-for-request";
|
|
4
|
+
import { formatFields } from "../utils/format-fields";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Retrieves a specific entry from a PocketBase collection using its ID and loader options.
|
|
@@ -13,7 +15,14 @@ export async function fetchEntry<TEntry extends PocketBaseEntry>(
|
|
|
13
15
|
const entryUrl = new URL(
|
|
14
16
|
`api/collections/${options.collectionName}/records/${id}`,
|
|
15
17
|
options.url
|
|
16
|
-
)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Add fields parameter if specified
|
|
21
|
+
const fieldsArray = formatFields(options.fields);
|
|
22
|
+
const combinedFields = combineFieldsForRequest(fieldsArray, options);
|
|
23
|
+
if (combinedFields) {
|
|
24
|
+
entryUrl.searchParams.set("fields", combinedFields.join(","));
|
|
25
|
+
}
|
|
17
26
|
|
|
18
27
|
// Create the headers for the request to append the token (if available)
|
|
19
28
|
const entryHeaders = new Headers();
|
|
@@ -22,7 +31,7 @@ export async function fetchEntry<TEntry extends PocketBaseEntry>(
|
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
// Fetch the entry from the collection
|
|
25
|
-
const entryRequest = await fetch(entryUrl, {
|
|
34
|
+
const entryRequest = await fetch(entryUrl.href, {
|
|
26
35
|
headers: entryHeaders
|
|
27
36
|
});
|
|
28
37
|
|
|
@@ -2,6 +2,9 @@ import type { ZodSchema } from "astro/zod";
|
|
|
2
2
|
import { z } from "astro/zod";
|
|
3
3
|
import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
|
|
4
4
|
import type { PocketBaseCollection } from "../types/pocketbase-schema.type";
|
|
5
|
+
import { combineFieldsForRequest } from "../utils/combine-fields-for-request";
|
|
6
|
+
import { extractFieldNames } from "../utils/extract-field-names";
|
|
7
|
+
import { formatFields } from "../utils/format-fields";
|
|
5
8
|
import { getRemoteSchema } from "./get-remote-schema";
|
|
6
9
|
import { parseSchema } from "./parse-schema";
|
|
7
10
|
import { readLocalSchema } from "./read-local-schema";
|
|
@@ -60,41 +63,87 @@ export async function generateSchema(
|
|
|
60
63
|
return z.object(BASIC_SCHEMA);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// Get fields to include from options
|
|
67
|
+
const formattedFields = formatFields(options.fields);
|
|
68
|
+
const fieldNames = extractFieldNames(formattedFields);
|
|
69
|
+
const fieldsToInclude = combineFieldsForRequest(fieldNames, options);
|
|
70
|
+
|
|
71
|
+
// Parse the schema with optional field filtering
|
|
72
|
+
const fields = parseSchema(collection, options.jsonSchemas, {
|
|
67
73
|
hasSuperuserRights,
|
|
68
|
-
options.improveTypes
|
|
69
|
-
|
|
74
|
+
improveTypes: options.improveTypes,
|
|
75
|
+
fieldsToInclude,
|
|
76
|
+
experimentalLiveTypesOnly: options.experimental?.liveTypesOnly
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Do some sanity checks on the provided options
|
|
80
|
+
checkCustomIdField(collection, options);
|
|
81
|
+
checkContentField(fields, options);
|
|
82
|
+
checkUpdatedField(fields, collection, options);
|
|
83
|
+
|
|
84
|
+
// Combine the basic schema with the parsed fields
|
|
85
|
+
const schema = z.object({
|
|
86
|
+
...BASIC_SCHEMA,
|
|
87
|
+
...fields
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Get all file fields
|
|
91
|
+
const fileFields = collection.fields
|
|
92
|
+
.filter((field) => field.type === "file")
|
|
93
|
+
// Only show hidden fields if the user has superuser rights
|
|
94
|
+
.filter((field) => !field.hidden || hasSuperuserRights);
|
|
95
|
+
|
|
96
|
+
if (fileFields.length === 0) {
|
|
97
|
+
return schema;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Transform file names to file urls
|
|
101
|
+
return schema.transform((entry) =>
|
|
102
|
+
transformFiles(options.url, fileFields, entry)
|
|
70
103
|
);
|
|
104
|
+
}
|
|
71
105
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Check if the custom id field is present
|
|
108
|
+
*/
|
|
109
|
+
function checkCustomIdField(
|
|
110
|
+
collection: PocketBaseCollection,
|
|
111
|
+
options: PocketBaseLoaderOptions
|
|
112
|
+
): void {
|
|
113
|
+
if (!options.idField) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
78
116
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
117
|
+
// Find the id field in the schema
|
|
118
|
+
const idField = collection.fields.find(
|
|
119
|
+
(field) => field.name === options.idField
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Check if the id field is present and of a valid type
|
|
123
|
+
if (!idField) {
|
|
124
|
+
console.error(
|
|
125
|
+
`The id field "${options.idField}" is not present in the schema of the collection "${options.collectionName}".`
|
|
126
|
+
);
|
|
127
|
+
} else if (!VALID_ID_TYPES.includes(idField.type)) {
|
|
128
|
+
console.error(
|
|
129
|
+
`The id field "${options.idField}" for collection "${
|
|
130
|
+
options.collectionName
|
|
131
|
+
}" is of type "${
|
|
132
|
+
idField.type
|
|
133
|
+
}" which is not recommended. Please use one of the following types: ${VALID_ID_TYPES.join(
|
|
134
|
+
", "
|
|
135
|
+
)}.`
|
|
136
|
+
);
|
|
95
137
|
}
|
|
138
|
+
}
|
|
96
139
|
|
|
97
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Check if the content field(s) are present
|
|
142
|
+
*/
|
|
143
|
+
function checkContentField(
|
|
144
|
+
fields: Record<string, z.ZodType>,
|
|
145
|
+
options: PocketBaseLoaderOptions
|
|
146
|
+
): void {
|
|
98
147
|
if (
|
|
99
148
|
typeof options.contentFields === "string" &&
|
|
100
149
|
!fields[options.contentFields]
|
|
@@ -111,47 +160,36 @@ export async function generateSchema(
|
|
|
111
160
|
}
|
|
112
161
|
}
|
|
113
162
|
}
|
|
163
|
+
}
|
|
114
164
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
!updatedField ||
|
|
127
|
-
updatedField.type !== "autodate" ||
|
|
128
|
-
!updatedField.onUpdate
|
|
129
|
-
) {
|
|
130
|
-
console.warn(
|
|
131
|
-
`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!`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if the updated field is present
|
|
167
|
+
*/
|
|
168
|
+
function checkUpdatedField(
|
|
169
|
+
fields: Record<string, z.ZodType>,
|
|
170
|
+
collection: PocketBaseCollection,
|
|
171
|
+
options: PocketBaseLoaderOptions
|
|
172
|
+
): void {
|
|
173
|
+
if (!options.updatedField) {
|
|
174
|
+
return;
|
|
135
175
|
}
|
|
136
176
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
if (!fields[options.updatedField]) {
|
|
178
|
+
console.error(
|
|
179
|
+
`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.`
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
const updatedField = collection.fields.find(
|
|
183
|
+
(field) => field.name === options.updatedField
|
|
184
|
+
);
|
|
185
|
+
if (
|
|
186
|
+
!updatedField ||
|
|
187
|
+
updatedField.type !== "autodate" ||
|
|
188
|
+
!updatedField.onUpdate
|
|
189
|
+
) {
|
|
190
|
+
console.warn(
|
|
191
|
+
`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!`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
151
194
|
}
|
|
152
|
-
|
|
153
|
-
// Transform file names to file urls
|
|
154
|
-
return schema.transform((entry) =>
|
|
155
|
-
transformFiles(options.url, fileFields, entry)
|
|
156
|
-
);
|
|
157
195
|
}
|
|
@@ -8,7 +8,7 @@ import type { PocketBaseCollection } from "../types/pocketbase-schema.type";
|
|
|
8
8
|
* @param token The superuser token to authenticate the request.
|
|
9
9
|
*/
|
|
10
10
|
export async function getRemoteSchema(
|
|
11
|
-
options: PocketBaseLoaderOptions,
|
|
11
|
+
options: Pick<PocketBaseLoaderOptions, "collectionName" | "url">,
|
|
12
12
|
token: string
|
|
13
13
|
): Promise<PocketBaseCollection | undefined> {
|
|
14
14
|
// Build URL and headers for the schema request
|
|
@@ -4,23 +4,42 @@ import type {
|
|
|
4
4
|
PocketBaseSchemaEntry
|
|
5
5
|
} from "../types/pocketbase-schema.type";
|
|
6
6
|
|
|
7
|
+
export interface ParseSchemaOptions {
|
|
8
|
+
hasSuperuserRights: boolean;
|
|
9
|
+
improveTypes?: boolean;
|
|
10
|
+
fieldsToInclude?: Array<string>;
|
|
11
|
+
experimentalLiveTypesOnly?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Converts PocketBase collection fields into Zod types, handling field types, required status, and custom schemas.
|
|
9
16
|
*/
|
|
10
17
|
export function parseSchema(
|
|
11
18
|
collection: PocketBaseCollection,
|
|
12
19
|
customSchemas: Record<string, z.ZodType> | undefined,
|
|
13
|
-
|
|
14
|
-
improveTypes: boolean,
|
|
15
|
-
experimentalLiveTypesOnly?: boolean
|
|
20
|
+
options: ParseSchemaOptions
|
|
16
21
|
): Record<string, z.ZodType> {
|
|
17
22
|
// Prepare the schemas fields
|
|
18
23
|
const fields: Record<string, z.ZodType> = {};
|
|
19
24
|
|
|
20
25
|
// Parse every field in the schema
|
|
21
26
|
for (const field of collection.fields) {
|
|
27
|
+
// If fieldsToInclude is specified, only include fields that are in the list
|
|
28
|
+
if (
|
|
29
|
+
options.fieldsToInclude &&
|
|
30
|
+
!options.fieldsToInclude.includes(field.name)
|
|
31
|
+
) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
// Skip hidden fields if the user does not have superuser rights
|
|
23
|
-
if (field.hidden && !hasSuperuserRights) {
|
|
36
|
+
if (field.hidden && !options.hasSuperuserRights) {
|
|
37
|
+
if (options.fieldsToInclude) {
|
|
38
|
+
console.warn(
|
|
39
|
+
`"${field.name}" is requested but hidden. Provide superuser credentials to include this field.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
continue;
|
|
25
44
|
}
|
|
26
45
|
|
|
@@ -36,7 +55,7 @@ export function parseSchema(
|
|
|
36
55
|
break;
|
|
37
56
|
case "date":
|
|
38
57
|
case "autodate":
|
|
39
|
-
if (experimentalLiveTypesOnly) {
|
|
58
|
+
if (options.experimentalLiveTypesOnly) {
|
|
40
59
|
// If experimental live types only mode is enabled, treat dates as strings
|
|
41
60
|
fieldType = z.string();
|
|
42
61
|
break;
|
|
@@ -94,7 +113,8 @@ export function parseSchema(
|
|
|
94
113
|
// `onCreate autodate` fields are always set
|
|
95
114
|
(field.type === "autodate" && field.onCreate) ||
|
|
96
115
|
// Improve number and boolean types by providing default values
|
|
97
|
-
(improveTypes &&
|
|
116
|
+
(options.improveTypes &&
|
|
117
|
+
(field.type === "number" || field.type === "bool"));
|
|
98
118
|
|
|
99
119
|
// If the field is not required, mark it as optional
|
|
100
120
|
if (!isRequired) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { z } from "astro/zod";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Options for
|
|
4
|
+
* Options for both build time and live collection loader
|
|
5
5
|
*/
|
|
6
|
-
export interface
|
|
6
|
+
export interface PocketBaseLoaderBaseOptions {
|
|
7
7
|
/**
|
|
8
8
|
* URL of the PocketBase instance.
|
|
9
9
|
*/
|
|
@@ -12,15 +12,6 @@ export interface PocketBaseLoaderOptions {
|
|
|
12
12
|
* Name of the collection in PocketBase.
|
|
13
13
|
*/
|
|
14
14
|
collectionName: string;
|
|
15
|
-
/**
|
|
16
|
-
* Field that should be used as the unique identifier for the collection.
|
|
17
|
-
* This must be the name of a field in the collection that contains unique values.
|
|
18
|
-
* If not provided, the `id` field will be used.
|
|
19
|
-
* The value of this field will be used in `getEntry` and `getEntries` to load the entry or entries.
|
|
20
|
-
*
|
|
21
|
-
* If the field is a string, it will be slugified to be used in the URL.
|
|
22
|
-
*/
|
|
23
|
-
idField?: string;
|
|
24
15
|
/**
|
|
25
16
|
* Name of the field(s) containing the content of an entry.
|
|
26
17
|
* This must be the name of a field in the PocketBase collection that contains the content.
|
|
@@ -34,12 +25,12 @@ export interface PocketBaseLoaderOptions {
|
|
|
34
25
|
/**
|
|
35
26
|
* Name of the field containing the last update date of an entry.
|
|
36
27
|
* Ideally, this field should be of type `autodate` and have the value "Update" or "Create/Update".
|
|
37
|
-
*
|
|
28
|
+
* For the build time loader, this field is used to only fetch entries that have been modified since the last build.
|
|
29
|
+
* For the live collection loader, this field is used to set the `lastModified` cache hint.
|
|
38
30
|
*/
|
|
39
31
|
updatedField?: string;
|
|
40
32
|
/**
|
|
41
33
|
* Custom filter that is applied when loading data from PocketBase.
|
|
42
|
-
* Valid syntax can be found in the [PocketBase documentation](https://pocketbase.io/docs/api-records/#listsearch-records)
|
|
43
34
|
*
|
|
44
35
|
* The loader will also add it's own filters for incremental builds.
|
|
45
36
|
* These will be added to your custom filter query.
|
|
@@ -52,8 +43,38 @@ export interface PocketBaseLoaderOptions {
|
|
|
52
43
|
* // request
|
|
53
44
|
* `?filter=(${loaderFilter})&&(release >= @now && deleted = false)`
|
|
54
45
|
* ```
|
|
46
|
+
*
|
|
47
|
+
* @see {@link https://pocketbase.io/docs/api-records/#listsearch-records PocketBase documentation} for valid syntax
|
|
55
48
|
*/
|
|
56
49
|
filter?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Specify which fields to return for each record.
|
|
52
|
+
* This can be either a comma-separated string of field names or an array of field names.
|
|
53
|
+
* Only the specified fields will be included in the response and schema.
|
|
54
|
+
*
|
|
55
|
+
* Use "*" to include all fields (same as not specifying the fields option).
|
|
56
|
+
*
|
|
57
|
+
* Note: The basic fields (`id`, `collectionId`, `collectionName`) are automatically included
|
|
58
|
+
* in API requests when using field filtering. Additionally, any custom fields specified in the
|
|
59
|
+
* loader options (`idField`, `updatedField`, `contentFields`) are also automatically included.
|
|
60
|
+
*
|
|
61
|
+
* Warning: Expand fields are not currently supported by this loader.
|
|
62
|
+
*
|
|
63
|
+
* Example:
|
|
64
|
+
* ```ts
|
|
65
|
+
* // Using string format:
|
|
66
|
+
* fields: 'title,content,author'
|
|
67
|
+
*
|
|
68
|
+
* // Using array format:
|
|
69
|
+
* fields: ['title', 'content', 'author']
|
|
70
|
+
*
|
|
71
|
+
* // Include all fields:
|
|
72
|
+
* fields: '*'
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @see {@link https://pocketbase.io/docs/api-records/#listsearch-records PocketBase documentation} for valid syntax
|
|
76
|
+
*/
|
|
77
|
+
fields?: string | Array<string>;
|
|
57
78
|
/**
|
|
58
79
|
* Credentials of a superuser to get full access to the PocketBase instance.
|
|
59
80
|
* 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.
|
|
@@ -76,6 +97,21 @@ export interface PocketBaseLoaderOptions {
|
|
|
76
97
|
*/
|
|
77
98
|
impersonateToken: string;
|
|
78
99
|
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Options for the PocketBase loader.
|
|
104
|
+
*/
|
|
105
|
+
export type PocketBaseLoaderOptions = PocketBaseLoaderBaseOptions & {
|
|
106
|
+
/**
|
|
107
|
+
* Field that should be used as the unique identifier for the collection.
|
|
108
|
+
* This must be the name of a field in the collection that contains unique values.
|
|
109
|
+
* If not provided, the `id` field will be used.
|
|
110
|
+
* The value of this field will be used in `getEntry` and `getEntries` to load the entry or entries.
|
|
111
|
+
*
|
|
112
|
+
* If the field is a string, it will be slugified to be used in the URL.
|
|
113
|
+
*/
|
|
114
|
+
idField?: string;
|
|
79
115
|
/**
|
|
80
116
|
* File path to the local schema file.
|
|
81
117
|
* This file will be used to generate the schema for the collection.
|
|
@@ -112,19 +148,12 @@ export interface PocketBaseLoaderOptions {
|
|
|
112
148
|
*/
|
|
113
149
|
liveTypesOnly?: boolean;
|
|
114
150
|
};
|
|
115
|
-
}
|
|
151
|
+
};
|
|
116
152
|
|
|
117
153
|
/**
|
|
118
154
|
* Options for the PocketBase live loader.
|
|
119
155
|
*
|
|
120
156
|
* @experimental Live content collections are still experimental
|
|
121
157
|
*/
|
|
122
|
-
export type ExperimentalPocketBaseLiveLoaderOptions =
|
|
123
|
-
|
|
124
|
-
| "url"
|
|
125
|
-
| "collectionName"
|
|
126
|
-
| "contentFields"
|
|
127
|
-
| "updatedField"
|
|
128
|
-
| "filter"
|
|
129
|
-
| "superuserCredentials"
|
|
130
|
-
>;
|
|
158
|
+
export type ExperimentalPocketBaseLiveLoaderOptions =
|
|
159
|
+
PocketBaseLoaderBaseOptions;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PocketBaseLoaderBaseOptions,
|
|
3
|
+
PocketBaseLoaderOptions
|
|
4
|
+
} from "../types/pocketbase-loader-options.type";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Combine basic, special, and user-specified fields for PocketBase API requests.
|
|
8
|
+
* This utility ensures that required system fields are always included in API requests.
|
|
9
|
+
*
|
|
10
|
+
* @param userFields Array of fields specified by the user, or undefined for all fields
|
|
11
|
+
* @param options PocketBase loader options containing custom field configurations
|
|
12
|
+
* @returns Combined array of fields to include in the API request, or undefined for all fields
|
|
13
|
+
*/
|
|
14
|
+
export function combineFieldsForRequest(
|
|
15
|
+
userFields: Array<string> | undefined,
|
|
16
|
+
options: Pick<PocketBaseLoaderBaseOptions, "updatedField" | "contentFields"> &
|
|
17
|
+
Pick<PocketBaseLoaderOptions, "idField">
|
|
18
|
+
): Array<string> | undefined {
|
|
19
|
+
// If no fields specified, return undefined to get all fields
|
|
20
|
+
if (!userFields) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Basic fields that are always required by the loader
|
|
25
|
+
const basicFields = ["id", "collectionId", "collectionName"];
|
|
26
|
+
|
|
27
|
+
// Special fields that are configured in options
|
|
28
|
+
const specialFields: Array<string> = [];
|
|
29
|
+
|
|
30
|
+
// Add custom id field if specified
|
|
31
|
+
if (options.idField && options.idField !== "id") {
|
|
32
|
+
specialFields.push(options.idField);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add updated field if specified
|
|
36
|
+
if (options.updatedField) {
|
|
37
|
+
specialFields.push(options.updatedField);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add content fields if specified
|
|
41
|
+
if (options.contentFields) {
|
|
42
|
+
if (Array.isArray(options.contentFields)) {
|
|
43
|
+
specialFields.push(...options.contentFields);
|
|
44
|
+
} else {
|
|
45
|
+
specialFields.push(options.contentFields);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Combine all field sets, removing duplicates
|
|
50
|
+
const allFields = [
|
|
51
|
+
...new Set([...basicFields, ...specialFields, ...userFields])
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
return allFields;
|
|
55
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PocketBaseLoaderBaseOptions } from "../types/pocketbase-loader-options.type";
|
|
2
2
|
import { getSuperuserToken } from "./get-superuser-token";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates a promise that resolves to a superuser token or undefined.
|
|
6
6
|
*/
|
|
7
7
|
export async function createTokenPromise(
|
|
8
|
-
options: Pick<
|
|
8
|
+
options: Pick<PocketBaseLoaderBaseOptions, "superuserCredentials" | "url">
|
|
9
9
|
): Promise<string | undefined> {
|
|
10
10
|
if (options.superuserCredentials) {
|
|
11
11
|
if ("impersonateToken" in options.superuserCredentials) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract field names from fields that may contain modifiers like :excerpt().
|
|
3
|
+
*
|
|
4
|
+
* @param fields Array of field specifications that may contain modifiers
|
|
5
|
+
* @returns Array of clean field names suitable for schema parsing
|
|
6
|
+
*/
|
|
7
|
+
export function extractFieldNames(
|
|
8
|
+
fields: Array<string> | undefined
|
|
9
|
+
): Array<string> | undefined {
|
|
10
|
+
if (!fields) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return fields.map((field) => field.split(":")[0]);
|
|
15
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { PocketBaseLoaderBaseOptions } from "../types/pocketbase-loader-options.type";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format fields option into an array and validate for expand usage.
|
|
5
|
+
* Handles wildcard "*" and preserves excerpt field modifiers.
|
|
6
|
+
*
|
|
7
|
+
* @param fields The fields option (string or array)
|
|
8
|
+
* @returns Formatted fields array, or undefined if no fields specified or "*" wildcard is used
|
|
9
|
+
*/
|
|
10
|
+
export function formatFields(
|
|
11
|
+
fields: PocketBaseLoaderBaseOptions["fields"]
|
|
12
|
+
): Array<string> | undefined {
|
|
13
|
+
if (!fields || fields.length === 0) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let fieldList: Array<string>;
|
|
18
|
+
if (Array.isArray(fields)) {
|
|
19
|
+
fieldList = fields.map((f) => f.trim());
|
|
20
|
+
} else {
|
|
21
|
+
// Split carefully, respecting parentheses in excerpt syntax
|
|
22
|
+
fieldList = splitFieldsString(fields).map((f) => f.trim());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Warn if expand is used since it's not currently supported
|
|
26
|
+
const hasExpand = fieldList.some((field) => field.includes("expand"));
|
|
27
|
+
if (hasExpand) {
|
|
28
|
+
console.warn(
|
|
29
|
+
'The "expand" parameter is not currently supported by astro-loader-pocketbase and will be filtered out.'
|
|
30
|
+
);
|
|
31
|
+
fieldList = fieldList.filter((field) => !field.includes("expand"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for "*" wildcard - if found anywhere, include all fields
|
|
35
|
+
const hasWildcard = fieldList.some((field) => field === "*");
|
|
36
|
+
if (hasWildcard) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return fieldList;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Splits the fields string at `,` but respects the `:excerpt(number, boolean)` option
|
|
45
|
+
*/
|
|
46
|
+
function splitFieldsString(fieldsString: string): Array<string> {
|
|
47
|
+
// First, split by comma
|
|
48
|
+
const initialSplit = fieldsString.split(",");
|
|
49
|
+
|
|
50
|
+
const fields: Array<string> = [];
|
|
51
|
+
for (let i = 0; i < initialSplit.length; i++) {
|
|
52
|
+
const part = initialSplit[i];
|
|
53
|
+
|
|
54
|
+
if (part.includes("(") && !part.includes(")")) {
|
|
55
|
+
fields.push(`${part},${initialSplit[++i]}`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fields.push(part);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return fields;
|
|
63
|
+
}
|