astro-loader-pocketbase 2.8.0-next.3 → 2.8.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-loader-pocketbase",
3
- "version": "2.8.0-next.3",
3
+ "version": "2.8.0-next.4",
4
4
  "description": "A content loader for Astro that uses the PocketBase API",
5
5
  "keywords": [
6
6
  "astro",
@@ -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
3
  import type { PocketBaseLoaderOptions } 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.
@@ -23,6 +25,7 @@ export async function fetchCollection<TEntry extends PocketBaseEntry>(
23
25
  | "url"
24
26
  | "updatedField"
25
27
  | "filter"
28
+ | "fields"
26
29
  | "superuserCredentials"
27
30
  >,
28
31
  chunkLoaded: (entries: Array<TEntry>) => Promise<void>,
@@ -33,7 +36,7 @@ export async function fetchCollection<TEntry extends PocketBaseEntry>(
33
36
  const collectionUrl = new URL(
34
37
  `api/collections/${options.collectionName}/records`,
35
38
  options.url
36
- ).href;
39
+ );
37
40
 
38
41
  // Create the headers for the request to append the token (if available)
39
42
  const collectionHeaders = new Headers();
@@ -41,25 +44,29 @@ export async function fetchCollection<TEntry extends PocketBaseEntry>(
41
44
  collectionHeaders.set("Authorization", token);
42
45
  }
43
46
 
47
+ // Cache fields computation outside the loop
48
+ const fieldsArray = formatFields(options.fields);
49
+ const combinedFields = combineFieldsForRequest(fieldsArray, options);
50
+
44
51
  // Prepare pagination variables
45
52
  let page = 0;
46
53
  let totalPages = 0;
47
54
 
48
55
  // Fetch all (modified) entries
49
56
  do {
50
- const searchParams = buildSearchParams(options, {
57
+ const searchParams = buildSearchParams(options, combinedFields, {
51
58
  ...collectionFilter,
52
59
  page: collectionFilter?.page ?? ++page,
53
60
  perPage: collectionFilter?.perPage ?? 100
54
61
  });
55
62
 
63
+ // Apply search parameters to URL
64
+ collectionUrl.search = searchParams.toString();
65
+
56
66
  // Fetch entries from the collection
57
- const collectionRequest = await fetch(
58
- `${collectionUrl}?${searchParams.toString()}`,
59
- {
60
- headers: collectionHeaders
61
- }
62
- );
67
+ const collectionRequest = await fetch(collectionUrl.href, {
68
+ headers: collectionHeaders
69
+ });
63
70
 
64
71
  // If the request was not successful, print the error message and return
65
72
  if (!collectionRequest.ok) {
@@ -102,9 +109,9 @@ export async function fetchCollection<TEntry extends PocketBaseEntry>(
102
109
  */
103
110
  function buildSearchParams(
104
111
  loaderOptions: Pick<PocketBaseLoaderOptions, "updatedField" | "filter">,
112
+ combinedFields: Array<string> | undefined,
105
113
  collectionFilter: CollectionFilter
106
114
  ): URLSearchParams {
107
- // Build search parameters
108
115
  const searchParams = new URLSearchParams();
109
116
 
110
117
  if (collectionFilter.page) {
@@ -145,5 +152,10 @@ function buildSearchParams(
145
152
  searchParams.set("sort", collectionFilter.sort);
146
153
  }
147
154
 
155
+ // Add fields parameter if specified
156
+ if (combinedFields) {
157
+ searchParams.set("fields", combinedFields.join(","));
158
+ }
159
+
148
160
  return searchParams;
149
161
  }
@@ -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
- ).href;
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
- // Parse the schema
64
- const fields = parseSchema(
65
- collection,
66
- options.jsonSchemas,
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 ?? false,
69
- options.experimental?.liveTypesOnly ?? false
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
- // Check if custom id field is present
73
- if (options.idField) {
74
- // Find the id field in the schema
75
- const idField = collection.fields.find(
76
- (field) => field.name === options.idField
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
- // Check if the id field is present and of a valid type
80
- if (!idField) {
81
- console.error(
82
- `The id field "${options.idField}" is not present in the schema of the collection "${options.collectionName}".`
83
- );
84
- } else if (!VALID_ID_TYPES.includes(idField.type)) {
85
- console.error(
86
- `The id field "${options.idField}" for collection "${
87
- options.collectionName
88
- }" is of type "${
89
- idField.type
90
- }" which is not recommended. Please use one of the following types: ${VALID_ID_TYPES.join(
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
- // Check if the content field is present
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
- // Check if the updated field is present
116
- if (options.updatedField) {
117
- if (!fields[options.updatedField]) {
118
- console.error(
119
- `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.`
120
- );
121
- } else {
122
- const updatedField = collection.fields.find(
123
- (field) => field.name === options.updatedField
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
- // Combine the basic schema with the parsed fields
138
- const schema = z.object({
139
- ...BASIC_SCHEMA,
140
- ...fields
141
- });
142
-
143
- // Get all file fields
144
- const fileFields = collection.fields
145
- .filter((field) => field.type === "file")
146
- // Only show hidden fields if the user has superuser rights
147
- .filter((field) => !field.hidden || hasSuperuserRights);
148
-
149
- if (fileFields.length === 0) {
150
- return schema;
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
  }
@@ -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
- hasSuperuserRights: boolean,
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 && (field.type === "number" || field.type === "bool"));
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) {
@@ -39,7 +39,6 @@ export interface PocketBaseLoaderOptions {
39
39
  updatedField?: string;
40
40
  /**
41
41
  * 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
42
  *
44
43
  * The loader will also add it's own filters for incremental builds.
45
44
  * These will be added to your custom filter query.
@@ -52,8 +51,38 @@ export interface PocketBaseLoaderOptions {
52
51
  * // request
53
52
  * `?filter=(${loaderFilter})&&(release >= @now && deleted = false)`
54
53
  * ```
54
+ *
55
+ * @see {@link https://pocketbase.io/docs/api-records/#listsearch-records PocketBase documentation} for valid syntax
55
56
  */
56
57
  filter?: string;
58
+ /**
59
+ * Specify which fields to return for each record.
60
+ * This can be either a comma-separated string of field names or an array of field names.
61
+ * Only the specified fields will be included in the response and schema.
62
+ *
63
+ * Use "*" to include all fields (same as not specifying the fields option).
64
+ *
65
+ * Note: The basic fields (`id`, `collectionId`, `collectionName`) are automatically included
66
+ * in API requests when using field filtering. Additionally, any custom fields specified in the
67
+ * loader options (`idField`, `updatedField`, `contentFields`) are also automatically included.
68
+ *
69
+ * Warning: Expand fields are not currently supported by this loader.
70
+ *
71
+ * Example:
72
+ * ```ts
73
+ * // Using string format:
74
+ * fields: 'title,content,author'
75
+ *
76
+ * // Using array format:
77
+ * fields: ['title', 'content', 'author']
78
+ *
79
+ * // Include all fields:
80
+ * fields: '*'
81
+ * ```
82
+ *
83
+ * @see {@link https://pocketbase.io/docs/api-records/#listsearch-records PocketBase documentation} for valid syntax
84
+ */
85
+ fields?: string | Array<string>;
57
86
  /**
58
87
  * Credentials of a superuser to get full access to the PocketBase instance.
59
88
  * 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.
@@ -126,5 +155,6 @@ export type ExperimentalPocketBaseLiveLoaderOptions = Pick<
126
155
  | "contentFields"
127
156
  | "updatedField"
128
157
  | "filter"
158
+ | "fields"
129
159
  | "superuserCredentials"
130
160
  >;
@@ -0,0 +1,54 @@
1
+ import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
2
+
3
+ /**
4
+ * Combine basic, special, and user-specified fields for PocketBase API requests.
5
+ * This utility ensures that required system fields are always included in API requests.
6
+ *
7
+ * @param userFields Array of fields specified by the user, or undefined for all fields
8
+ * @param options PocketBase loader options containing custom field configurations
9
+ * @returns Combined array of fields to include in the API request, or undefined for all fields
10
+ */
11
+ export function combineFieldsForRequest(
12
+ userFields: Array<string> | undefined,
13
+ options: Pick<
14
+ PocketBaseLoaderOptions,
15
+ "idField" | "updatedField" | "contentFields"
16
+ >
17
+ ): Array<string> | undefined {
18
+ // If no fields specified, return undefined to get all fields
19
+ if (!userFields) {
20
+ return undefined;
21
+ }
22
+
23
+ // Basic fields that are always required by the loader
24
+ const basicFields = ["id", "collectionId", "collectionName"];
25
+
26
+ // Special fields that are configured in options
27
+ const specialFields: Array<string> = [];
28
+
29
+ // Add custom id field if specified
30
+ if (options.idField && options.idField !== "id") {
31
+ specialFields.push(options.idField);
32
+ }
33
+
34
+ // Add updated field if specified
35
+ if (options.updatedField) {
36
+ specialFields.push(options.updatedField);
37
+ }
38
+
39
+ // Add content fields if specified
40
+ if (options.contentFields) {
41
+ if (Array.isArray(options.contentFields)) {
42
+ specialFields.push(...options.contentFields);
43
+ } else {
44
+ specialFields.push(options.contentFields);
45
+ }
46
+ }
47
+
48
+ // Combine all field sets, removing duplicates
49
+ const allFields = [
50
+ ...new Set([...basicFields, ...specialFields, ...userFields])
51
+ ];
52
+
53
+ return allFields;
54
+ }
@@ -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 { PocketBaseLoaderOptions } 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: PocketBaseLoaderOptions["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
+ }