astro-loader-pocketbase 2.9.0 → 2.10.0-live-expand.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 CHANGED
@@ -309,6 +309,50 @@ const blogLive = defineLiveCollection({
309
309
  });
310
310
  ```
311
311
 
312
+ ### Expanding relations
313
+
314
+ The live loader supports expanding relation fields directly in the API request, which will include the related records in the response.
315
+ This is useful when you need to access related data without making additional requests.
316
+
317
+ ```ts
318
+ const blogLive = defineLiveCollection({
319
+ loader: experimentalPocketbaseLiveLoader({
320
+ ...options,
321
+ experimental: {
322
+ // Expand single relation field
323
+ expand: ["author"]
324
+
325
+ // Expand multiple relation fields
326
+ expand: ["author", "category"]
327
+
328
+ // Expand nested relations (up to 6 levels deep)
329
+ expand: ["author.profile", "category.parent"]
330
+ }
331
+ })
332
+ });
333
+ ```
334
+
335
+ When you fetch entries with expanded relations, the related records will be available in the `expand` property:
336
+
337
+ ```ts
338
+ const entry = await getLiveEntry("blogLive", { id: "<entry-id>" });
339
+
340
+ // Access expanded relation data
341
+ console.log(entry.expand.author.name);
342
+ console.log(entry.expand.category.name);
343
+
344
+ // Access nested expanded relations
345
+ console.log(entry.expand.author.expand.profile.bio);
346
+ ```
347
+
348
+ > [!NOTE]
349
+ > The expand parameter:
350
+ >
351
+ > - Supports up to 6 levels of nested relations (enforced by PocketBase)
352
+ > - Must use separate array entries for each field (e.g., `["author", "category"]` not `["author,category"]`)
353
+ > - Is not compatible with the `fields` option (yet)
354
+ > - Does not support schema generation (yet) - expanded data will have `unknown | undefined` types
355
+
312
356
  ### Error handling
313
357
 
314
358
  The live content loader follows Astro's standard error handling conventions for live collections. For more information on how to handle errors in your components, see the [Astro documentation on error handling](https://docs.astro.build/en/reference/experimental-flags/live-content-collections/#error-handling).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-loader-pocketbase",
3
- "version": "2.9.0",
3
+ "version": "2.10.0-live-expand.1",
4
4
  "description": "A content loader for Astro that uses the PocketBase API",
5
5
  "keywords": [
6
6
  "astro",
@@ -9,8 +9,12 @@ import {
9
9
  } from "../types/pocketbase-api-response.type";
10
10
  import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
11
11
  import type { ExperimentalPocketBaseLiveLoaderCollectionFilter } from "../types/pocketbase-live-loader-filter.type";
12
- import type { PocketBaseLoaderBaseOptions } from "../types/pocketbase-loader-options.type";
12
+ import type {
13
+ ExperimentalPocketBaseLiveLoaderOptions,
14
+ PocketBaseLoaderOptions
15
+ } from "../types/pocketbase-loader-options.type";
13
16
  import { combineFieldsForRequest } from "../utils/combine-fields-for-request";
17
+ import { formatExpand } from "../utils/format-expand";
14
18
  import { formatFields } from "../utils/format-fields";
15
19
 
16
20
  /**
@@ -28,7 +32,7 @@ export type CollectionFilter = {
28
32
  * Fetches entries from a PocketBase collection, optionally filtering by modification date and supporting pagination.
29
33
  */
30
34
  export async function fetchCollection<TEntry extends PocketBaseEntry>(
31
- options: PocketBaseLoaderBaseOptions,
35
+ options: PocketBaseLoaderOptions | ExperimentalPocketBaseLiveLoaderOptions,
32
36
  chunkLoaded: (entries: Array<TEntry>) => Promise<void>,
33
37
  token: string | undefined,
34
38
  collectionFilter: CollectionFilter | undefined
@@ -124,7 +128,9 @@ export async function fetchCollection<TEntry extends PocketBaseEntry>(
124
128
  * Build search parameters for the PocketBase collection request.
125
129
  */
126
130
  function buildSearchParams(
127
- loaderOptions: PocketBaseLoaderBaseOptions,
131
+ loaderOptions:
132
+ | PocketBaseLoaderOptions
133
+ | ExperimentalPocketBaseLiveLoaderOptions,
128
134
  combinedFields: Array<string> | undefined,
129
135
  collectionFilter: CollectionFilter
130
136
  ): URLSearchParams {
@@ -173,5 +179,15 @@ function buildSearchParams(
173
179
  searchParams.set("fields", combinedFields.join(","));
174
180
  }
175
181
 
182
+ if (loaderOptions.experimental && "expand" in loaderOptions.experimental) {
183
+ const expandString = formatExpand(
184
+ loaderOptions.experimental.expand,
185
+ loaderOptions.collectionName
186
+ );
187
+ if (expandString) {
188
+ searchParams.set("expand", expandString);
189
+ }
190
+ }
191
+
176
192
  return searchParams;
177
193
  }
@@ -6,8 +6,12 @@ import { PocketBaseAuthenticationError } from "../types/errors";
6
6
  import { pocketBaseErrorResponse } from "../types/pocketbase-api-response.type";
7
7
  import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
8
8
  import { pocketBaseEntry } from "../types/pocketbase-entry.type";
9
- import type { ExperimentalPocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
9
+ import type {
10
+ ExperimentalPocketBaseLiveLoaderOptions,
11
+ PocketBaseLoaderOptions
12
+ } from "../types/pocketbase-loader-options.type";
10
13
  import { combineFieldsForRequest } from "../utils/combine-fields-for-request";
14
+ import { formatExpand } from "../utils/format-expand";
11
15
  import { formatFields } from "../utils/format-fields";
12
16
 
13
17
  /**
@@ -15,7 +19,7 @@ import { formatFields } from "../utils/format-fields";
15
19
  */
16
20
  export async function fetchEntry<TEntry extends PocketBaseEntry>(
17
21
  id: string,
18
- options: ExperimentalPocketBaseLiveLoaderOptions,
22
+ options: PocketBaseLoaderOptions | ExperimentalPocketBaseLiveLoaderOptions,
19
23
  token: string | undefined
20
24
  ): Promise<TEntry> {
21
25
  // Build the URL for the entry endpoint
@@ -31,6 +35,17 @@ export async function fetchEntry<TEntry extends PocketBaseEntry>(
31
35
  entryUrl.searchParams.set("fields", combinedFields.join(","));
32
36
  }
33
37
 
38
+ // Add expand parameter if specified in experimental options
39
+ if (options.experimental && "expand" in options.experimental) {
40
+ const expandString = formatExpand(
41
+ options.experimental.expand,
42
+ options.collectionName
43
+ );
44
+ if (expandString) {
45
+ entryUrl.searchParams.set("expand", expandString);
46
+ }
47
+ }
48
+
34
49
  // Create the headers for the request to append the token (if available)
35
50
  const entryHeaders = new Headers();
36
51
  if (token) {
@@ -84,7 +84,11 @@ export async function generateSchema(
84
84
  // Combine the basic schema with the parsed fields
85
85
  const schema = z.object({
86
86
  ...BASIC_SCHEMA,
87
- ...fields
87
+ ...fields,
88
+ // Add expand field for live types only mode to support expanded relations
89
+ ...(options.experimental?.liveTypesOnly && {
90
+ expand: z.optional(z.unknown())
91
+ })
88
92
  });
89
93
 
90
94
  // Get all file fields
@@ -17,3 +17,21 @@ export class PocketBaseAuthenticationError extends LiveCollectionError {
17
17
  );
18
18
  }
19
19
  }
20
+
21
+ /**
22
+ * Error thrown when there is a configuration issue with the loader.
23
+ */
24
+ export class PocketBaseConfigurationError extends LiveCollectionError {
25
+ constructor(collection: string, message: string) {
26
+ super(collection, message);
27
+ this.name = "PocketBaseConfigurationError";
28
+ }
29
+
30
+ static is(error: unknown): error is PocketBaseConfigurationError {
31
+ // This is similar to the original implementation in Astro itself.
32
+ return (
33
+ // oxlint-disable-next-line no-unsafe-type-assertion
34
+ !!error && (error as Error)?.name === "PocketBaseConfigurationError"
35
+ );
36
+ }
37
+ }
@@ -156,4 +156,33 @@ export type PocketBaseLoaderOptions = PocketBaseLoaderBaseOptions & {
156
156
  * @experimental Live content collections are still experimental
157
157
  */
158
158
  export type ExperimentalPocketBaseLiveLoaderOptions =
159
- PocketBaseLoaderBaseOptions;
159
+ PocketBaseLoaderBaseOptions & {
160
+ /**
161
+ * Experimental options for the live loader.
162
+ *
163
+ * @experimental All of these options are experimental and may change in the future.
164
+ */
165
+ experimental?: {
166
+ /**
167
+ * Specify relations to auto expand in the API response.
168
+ * This can be an array of relation field names to expand.
169
+ * Supports dot notation for nested relations up to 6 levels deep.
170
+ *
171
+ * Note: This option is not compatible with the `fields` option.
172
+ *
173
+ * Example:
174
+ * ```ts
175
+ * // Using array format:
176
+ * expand: ['author', 'category']
177
+ *
178
+ * // Nested relations:
179
+ * expand: ['author.profile', 'category.parent']
180
+ * ```
181
+ *
182
+ * @see {@link https://pocketbase.io/docs/working-with-relations/#expanding-relations PocketBase documentation} for valid syntax
183
+ *
184
+ * @experimental This feature is experimental and may change in the future
185
+ */
186
+ expand?: Array<string>;
187
+ };
188
+ };
@@ -55,7 +55,12 @@ export const pocketBaseSchemaEntry = z.object({
55
55
  * Whether the field is updated when the entry is updated.
56
56
  * This is only present on "autodate" fields.
57
57
  */
58
- onUpdate: z.optional(z.boolean())
58
+ onUpdate: z.optional(z.boolean()),
59
+ /**
60
+ * Id of the associated collection that the relation is referencing.
61
+ * This is only present on "relation" fields.
62
+ */
63
+ collectionId: z.optional(z.string())
59
64
  });
60
65
 
61
66
  /**
@@ -67,6 +72,10 @@ export type PocketBaseSchemaEntry = z.infer<typeof pocketBaseSchemaEntry>;
67
72
  * Schema for a PocketBase collection.
68
73
  */
69
74
  export const pocketBaseCollection = z.object({
75
+ /**
76
+ * Id of the collection.
77
+ */
78
+ id: z.string(),
70
79
  /**
71
80
  * Name of the collection.
72
81
  */
@@ -0,0 +1,41 @@
1
+ import { PocketBaseConfigurationError } from "../types/errors";
2
+
3
+ /**
4
+ * Maximum nesting depth for expand relations as enforced by PocketBase
5
+ */
6
+ const MAX_EXPAND_DEPTH = 6;
7
+
8
+ /**
9
+ * Format and validate expand option for PocketBase API requests.
10
+ * Validates nesting depth and returns formatted expand string.
11
+ */
12
+ export function formatExpand(
13
+ expand: Array<string> | undefined,
14
+ collectionName: string
15
+ ): string | undefined {
16
+ if (!expand || expand.length === 0) {
17
+ return undefined;
18
+ }
19
+
20
+ // Validate each expand field for maximum nesting depth and invalid characters
21
+ for (const field of expand) {
22
+ // Check for comma in field name
23
+ if (field.includes(",")) {
24
+ throw new PocketBaseConfigurationError(
25
+ collectionName,
26
+ `Expand field "${field}" contains a comma. Use separate array entries instead of comma-separated values.`
27
+ );
28
+ }
29
+
30
+ const depth = (field.match(/\./g) || []).length + 1;
31
+ if (depth > MAX_EXPAND_DEPTH) {
32
+ throw new PocketBaseConfigurationError(
33
+ collectionName,
34
+ `Expand field "${field}" exceeds maximum nesting depth of ${MAX_EXPAND_DEPTH} levels.`
35
+ );
36
+ }
37
+ }
38
+
39
+ // Join all expand fields with comma as required by PocketBase
40
+ return expand.join(",");
41
+ }