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 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.5",
4
4
  "description": "A content loader for Astro that uses the PocketBase API",
5
5
  "keywords": [
6
6
  "astro",
@@ -1,5 +1,5 @@
1
1
  import type { LoaderContext } from "astro/loaders";
2
- import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.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: PocketBaseLoaderOptions,
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 { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.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: Pick<
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
- ).href;
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
- `${collectionUrl}?${searchParams.toString()}`,
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: Pick<PocketBaseLoaderOptions, "updatedField" | "filter">,
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
- ).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
  }
@@ -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
- 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) {
@@ -1,9 +1,9 @@
1
1
  import type { z } from "astro/zod";
2
2
 
3
3
  /**
4
- * Options for the PocketBase loader.
4
+ * Options for both build time and live collection loader
5
5
  */
6
- export interface PocketBaseLoaderOptions {
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
- * This field is used to only fetch entries that have been modified since the last build.
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 = Pick<
123
- PocketBaseLoaderOptions,
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 { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.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<PocketBaseLoaderOptions, "url" | "superuserCredentials">
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
+ }