Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/loader/fetch-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,10 @@ function buildSearchParams(
searchParams.set("fields", combinedFields.join(","));
}

// Add expand parameter if specified
if (loaderOptions.experimental?.expand) {
searchParams.set("expand", loaderOptions.experimental.expand.join(","));
}

return searchParams;
}
5 changes: 5 additions & 0 deletions src/loader/fetch-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export async function fetchEntry<TEntry extends PocketBaseEntry>(
entryUrl.searchParams.set("fields", combinedFields.join(","));
}

// Include expanded fields if option is set
if (options.experimental?.expand) {
entryUrl.searchParams.set("expand", options.experimental.expand.join(","));
}

// Create the headers for the request to append the token (if available)
const entryHeaders = new Headers();
if (token) {
Expand Down
8 changes: 8 additions & 0 deletions src/loader/handle-realtime-updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export async function handleRealtimeUpdates(
return false;
}

// Check if a expand is set
if (options.experimental?.expand) {
// Updating an entry directly via realtime updates is not supported when using expand.
// This is because updates to a related entry cannot be tracked here.
// So all (updated) entries must be refreshed when a update is received.
return false;
}

// Check if data was provided via the refresh context
if (!context.refreshContextData?.data) {
return false;
Expand Down
5 changes: 1 addition & 4 deletions src/loader/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ export async function loader(
context.logger.label = `pocketbase-loader:${options.collectionName}`;

// Check if the collection should be refreshed.
const refresh = shouldRefresh(
context.refreshContextData,
options.collectionName
);
const refresh = shouldRefresh(context.refreshContextData, options);
if (refresh === "skip") {
return;
}
Expand Down
85 changes: 82 additions & 3 deletions src/schema/generate-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { combineFieldsForRequest } from "../utils/combine-fields-for-request";
import { extractFieldNames } from "../utils/extract-field-names";
import { formatFields } from "../utils/format-fields";
import { getRemoteSchema } from "./get-remote-schema";
import { parseSchema } from "./parse-schema";
import { parseSchema, parseSingleOrMultipleValues } from "./parse-schema";
import { readLocalSchema } from "./read-local-schema";
import { transformFiles } from "./transform-files";

Expand Down Expand Up @@ -82,10 +82,19 @@ export async function generateSchema(
checkUpdatedField(fields, collection, options);

// Combine the basic schema with the parsed fields
const schema = z.object({
const schemaShape = {
...BASIC_SCHEMA,
...fields
});
};

// Generate schema for expanded fields
const expandSchema = await generateExpandSchema(collection, options, token);
if (expandSchema) {
// @ts-expect-error - "expand" is not known yet
schemaShape["expand"] = z.optional(z.object(expandSchema));
}

const schema = z.object(schemaShape);

// Get all file fields
const fileFields = collection.fields
Expand All @@ -103,6 +112,76 @@ export async function generateSchema(
);
}

/**
* Generate schema for expanded fields
*/
async function generateExpandSchema(
collection: PocketBaseCollection,
options: PocketBaseLoaderOptions,
token: string | undefined
): Promise<Record<string, z.ZodType> | undefined> {
if (
!options.experimental?.expand ||
options.experimental.expand.length === 0
) {
return undefined;
}

const expandedFields: Record<string, z.ZodType> = {};

for (const field of options.experimental.expand) {
const fields = field.split(".");
if (fields.length > 6) {
throw new Error(
`Expand value ${field} is not valid, since it exceeds 6 levels of depth. This is not supported by PocketBase.`
);
}

const currentField = fields.at(0);
if (!currentField) {
throw new Error(`Expand value ${field} contains an empty block`);
}

const fieldDefinition = collection.fields.find(
(field) => field.name === currentField
);
if (
!fieldDefinition ||
fieldDefinition.type !== "relation" ||
!fieldDefinition.collectionId
) {
throw new Error(
`The provided field ${currentField} in ${field} does not exist or has no associated collection. Thus the field cannot be expanded.`
);
}

const deeperFields =
fields.length > 1 ? [fields.slice(1).join(".")] : undefined;
const schema = await generateSchema(
{
...options,
collectionName: fieldDefinition.collectionId,
experimental: {
...options.experimental,
expand: deeperFields
}
},
token
);

let fieldType = parseSingleOrMultipleValues(fieldDefinition, schema);
if (!fieldDefinition.required) {
fieldType = z.preprocess(
(val) => val || undefined,
z.optional(fieldType)
);
}
expandedFields[currentField] = fieldType;
}

return expandedFields;
}

/**
* Check if the custom id field is present
*/
Expand Down
4 changes: 2 additions & 2 deletions src/schema/parse-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,12 @@ export function parseSchema(
*
* @returns The parsed field type
*/
function parseSingleOrMultipleValues(
export function parseSingleOrMultipleValues(
field: PocketBaseSchemaEntry,
type: z.ZodType
): z.ZodType {
// If the select allows multiple values, create an array of the enum
if (field.maxSelect === undefined || field.maxSelect === 1) {
if (field.maxSelect === undefined || field.maxSelect <= 1) {
return type;
}

Expand Down
3 changes: 2 additions & 1 deletion src/schema/read-local-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export async function readLocalSchema(

// Find and return the schema for the collection
const schema = fileContent.data.find(
(collection) => collection.name === collectionName
(collection) =>
collection.name === collectionName || collection.id === collectionName
);

if (!schema) {
Expand Down
27 changes: 27 additions & 0 deletions src/types/pocketbase-loader-options.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,33 @@ export interface PocketBaseLoaderBaseOptions {
*/
impersonateToken: string;
};
/**
* Experimental options for the loader.
*
* @experimental All of these options are experimental and may change in the future.
*/
experimental?: {
/**
* Array of relation field names to include (expand) when loading data from PocketBase.
*
* This is only reccomended to use with the live-loader.
* For the default build time loader using a separate collection instead is more efficient.
*
* Example:
* ```ts
* // config:
* expand: ['relatedField1', 'relatedField2']
*
* // request
* `?expand=relatedField1,relatedField2`
* ```
*
* @see {@link https://pocketbase.io/docs/api-records/#listsearch-records PocketBase documentation} for valid syntax
*
* @experimental Expand has many edge cases to consinder, especially regarding the schema generation and build cache. So this will be experimental for now.
*/
expand?: Array<string>;
};
}

/**
Expand Down
11 changes: 10 additions & 1 deletion src/types/pocketbase-schema.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ export const pocketBaseSchemaEntry = z.object({
* Whether the field is updated when the entry is updated.
* This is only present on "autodate" fields.
*/
onUpdate: z.optional(z.boolean())
onUpdate: z.optional(z.boolean()),
/**
* Id of the associated collection that the relation is referencing.
* This is only present on "relation" fields.
*/
collectionId: z.optional(z.string())
});

/**
Expand All @@ -67,6 +72,10 @@ export type PocketBaseSchemaEntry = z.infer<typeof pocketBaseSchemaEntry>;
* Schema for a PocketBase collection.
*/
export const pocketBaseCollection = z.object({
/**
* Id of the collection.
*/
id: z.string(),
/**
* Name of the collection.
*/
Expand Down
11 changes: 2 additions & 9 deletions src/utils/format-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import type { PocketBaseLoaderBaseOptions } from "../types/pocketbase-loader-opt

/**
* Format fields option into an array and validate for expand usage.
* Handles wildcard "*" and preserves excerpt field modifiers.
*
* @param fields The fields option (string or array)
* @returns Formatted fields array, or undefined if no fields specified or "*" wildcard is used
* @returns Formatted fields array, or undefined if no fields specified
*/
export function formatFields(
fields: PocketBaseLoaderBaseOptions["fields"]
Expand All @@ -26,17 +25,11 @@ export function formatFields(
const hasExpand = fieldList.some((field) => field.includes("expand"));
if (hasExpand) {
console.warn(
'The "expand" parameter is not currently supported by astro-loader-pocketbase and will be filtered out.'
'The "expand" parameter is currently experimental in astro-loader-pocketbase.'
);
fieldList = fieldList.filter((field) => !field.includes("expand"));
}

// Check for "*" wildcard - if found anywhere, include all fields
const hasWildcard = fieldList.some((field) => field === "*");
if (hasWildcard) {
return undefined;
}

return fieldList;
}

Expand Down
15 changes: 12 additions & 3 deletions src/utils/should-refresh.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { LoaderContext } from "astro/loaders";
import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";

/**
* Checks if the collection should be refreshed.
*/
export function shouldRefresh(
context: LoaderContext["refreshContextData"],
collectionName: string
options: Pick<PocketBaseLoaderOptions, "collectionName" | "experimental">
): "refresh" | "skip" | "force" {
// Check if the refresh was triggered by the `astro-integration-pocketbase`
// and the correct metadata is provided.
Expand All @@ -18,18 +19,26 @@ export function shouldRefresh(
return "force";
}

// If no collection is was provided refresh just in case
if (!context.collection) {
return "refresh";
}

// Must refresh all collections when expand is set
if (options.experimental?.expand) {
return "refresh";
}

// Check if the collection name matches the current collection.
if (typeof context.collection === "string") {
return context.collection === collectionName ? "refresh" : "skip";
return context.collection === options.collectionName ? "refresh" : "skip";
}

// Check if the collection is included in the list of collections.
if (Array.isArray(context.collection)) {
return context.collection.includes(collectionName) ? "refresh" : "skip";
return context.collection.includes(options.collectionName)
? "refresh"
: "skip";
}

// Should not happen but return true to be safe.
Expand Down
Loading