From 6b3284e2e453f0c12de6167ce6aca33d8bc3faa2 Mon Sep 17 00:00:00 2001 From: David Easton Date: Sun, 22 Jun 2025 20:26:21 +0100 Subject: [PATCH 1/9] feat(expand): added fetching and schema gen for expand property from pocketbase --- src/loader/load-entries.ts | 5 + src/schema/generate-schema.ts | 81 +++++++++++-- src/schema/parse-schema.ts | 6 +- src/schema/read-local-schema.ts | 2 +- src/types/pocketbase-collection.type.ts | 32 +++++ src/types/pocketbase-entry.type.ts | 4 + src/types/pocketbase-loader-options.type.ts | 13 +++ src/types/pocketbase-schema.type.ts | 18 +-- test/_mocks/insert-collection.ts | 8 +- test/loader/load-entries.e2e-spec.ts | 78 ++++++++++++- test/schema/generate-schema.e2e-spec.ts | 123 +++++++++++++++++++- 11 files changed, 337 insertions(+), 33 deletions(-) create mode 100644 src/types/pocketbase-collection.type.ts diff --git a/src/loader/load-entries.ts b/src/loader/load-entries.ts index 672a161..010292f 100644 --- a/src/loader/load-entries.ts +++ b/src/loader/load-entries.ts @@ -66,6 +66,11 @@ export async function loadEntries( searchParams.set("filter", filters.join("&&")); } + // Add filters to search parameters + if (options.expand) { + searchParams.set("expand", options.expand.join(",")); + } + // Fetch entries from the collection const collectionRequest = await fetch( `${collectionUrl}?${searchParams.toString()}`, diff --git a/src/schema/generate-schema.ts b/src/schema/generate-schema.ts index 58fb77a..580c191 100644 --- a/src/schema/generate-schema.ts +++ b/src/schema/generate-schema.ts @@ -1,7 +1,10 @@ -import type { ZodSchema } from "astro/zod"; +import type { ZodObject, ZodSchema } from "astro/zod"; import { z } from "astro/zod"; +import type { + ExpandedFields, + PocketBaseCollection +} from "../types/pocketbase-collection.type"; import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type"; -import type { PocketBaseCollection } from "../types/pocketbase-schema.type"; import { getRemoteSchema } from "./get-remote-schema"; import { parseSchema } from "./parse-schema"; import { readLocalSchema } from "./read-local-schema"; @@ -33,6 +36,7 @@ export async function generateSchema( options: PocketBaseLoaderOptions ): Promise { let collection: PocketBaseCollection | undefined; + const expandedFields: ExpandedFields = {}; // Try to get the schema directly from the PocketBase instance collection = await getRemoteSchema(options); @@ -129,10 +133,48 @@ export async function generateSchema( } } - // Combine the basic schema with the parsed fields + if (options.expand) { + for (const expandedFieldName of options.expand) { + const expandedFieldDefinition = collection.fields.find( + (field) => field.name === expandedFieldName + ); + + if (!expandedFieldDefinition) { + console.error( + `The provided field in the expand property "${expandedFieldName}" is not present in the schema of the collection "${options.collectionName}".\nThis will lead to use unable to provide a definition for this field.` + ); + } else { + if (!expandedFieldDefinition.collectionId) { + console.error( + `The provided field in the expand property "${expandedFieldName}" does not have an associated collection linked to it, we need this in order to know the shape of the related schema.` + ); + } else { + const expandedchema = await generateSchema({ + collectionName: expandedFieldDefinition.collectionId, + superuserCredentials: options.superuserCredentials, + localSchema: options.localSchema, + jsonSchemas: options.jsonSchemas, + improveTypes: options.improveTypes, + url: options.url + }); + + expandedFields[expandedFieldName] = z.union([ + expandedchema, + z.array(expandedchema) + ]); + } + } + } + } + + const expandSchema = { + expand: buildExpandSchema(expandedFields).optional() + }; + const schema = z.object({ ...BASIC_SCHEMA, - ...fields + ...fields, + ...expandSchema }); // Get all file fields @@ -146,7 +188,32 @@ export async function generateSchema( } // Transform file names to file urls - return schema.transform((entry) => - transformFiles(options.url, fileFields, entry) - ); + return schema.transform((entry) => { + if (Array.isArray(entry)) { + return entry.map((e) => transformFiles(options.url, fileFields, e)); + } + + return transformFiles(options.url, fileFields, entry); + }); +} + +/** + * Builds a Zod object schema from expandedFields, where each property is either a ZodSchema or an array of ZodSchema. + */ +export function buildExpandSchema( + expandedFields: Record> +): ZodObject>> { + const shape: Record> = {}; + + for (const key in expandedFields) { + const value = expandedFields[key]; + if (Array.isArray(value)) { + // If it's an array, wrap the first element as a ZodArray (assuming all elements are the same schema) + shape[key] = z.array(value[0]); + } else { + shape[key] = value; + } + } + + return z.object(shape); } diff --git a/src/schema/parse-schema.ts b/src/schema/parse-schema.ts index 2920869..20582bd 100644 --- a/src/schema/parse-schema.ts +++ b/src/schema/parse-schema.ts @@ -1,8 +1,6 @@ import { z } from "astro/zod"; -import type { - PocketBaseCollection, - PocketBaseSchemaEntry -} from "../types/pocketbase-schema.type"; +import type { PocketBaseCollection } from "../types/pocketbase-collection.type"; +import type { PocketBaseSchemaEntry } from "../types/pocketbase-schema.type"; export function parseSchema( collection: PocketBaseCollection, diff --git a/src/schema/read-local-schema.ts b/src/schema/read-local-schema.ts index 7a6caa3..9cf0177 100644 --- a/src/schema/read-local-schema.ts +++ b/src/schema/read-local-schema.ts @@ -1,6 +1,6 @@ import fs from "fs/promises"; import path from "path"; -import type { PocketBaseCollection } from "../types/pocketbase-schema.type"; +import type { PocketBaseCollection } from "../types/pocketbase-collection.type"; /** * Reads the local PocketBase schema file and returns the schema for the specified collection. diff --git a/src/types/pocketbase-collection.type.ts b/src/types/pocketbase-collection.type.ts new file mode 100644 index 0000000..eb942aa --- /dev/null +++ b/src/types/pocketbase-collection.type.ts @@ -0,0 +1,32 @@ +import type { ZodSchema } from "astro/zod"; +import type { PocketBaseSchemaEntry } from "./pocketbase-schema.type"; + +/** + * Base interface for all PocketBase entries. + */ +interface PocketBaseBaseCollection { + /** + * ID of the collection. + */ + id: string; + /** + * Name of the collection + */ + name: string; + /** + * Type of the collection. + */ + type: "base" | "view" | "auth"; + /** + * Schema of the collection. + */ + fields: Array; +} + +/** + * Type for a PocketBase entry. + */ +export type PocketBaseCollection = PocketBaseBaseCollection & + Record; + +export type ExpandedFields = Record>; diff --git a/src/types/pocketbase-entry.type.ts b/src/types/pocketbase-entry.type.ts index 7471d2f..efa250e 100644 --- a/src/types/pocketbase-entry.type.ts +++ b/src/types/pocketbase-entry.type.ts @@ -14,6 +14,10 @@ interface PocketBaseBaseEntry { * Name of the collection the entry belongs to. */ collectionName: string; + /** + * Optional property that contains all relational fields that have been expanded to contain their linked entry(s) + */ + expand?: Record>; } /** diff --git a/src/types/pocketbase-loader-options.type.ts b/src/types/pocketbase-loader-options.type.ts index 80fd331..37c68c2 100644 --- a/src/types/pocketbase-loader-options.type.ts +++ b/src/types/pocketbase-loader-options.type.ts @@ -54,6 +54,19 @@ export interface PocketBaseLoaderOptions { * ``` */ filter?: string; + /** + * array of relational field names to auto expand when loading data from PocketBase. + * Valid syntax can be found in the [PocketBase documentation](https://pocketbase.io/docs/api-records/#listsearch-records) + * Example: + * ```ts + * // config: + * expand: ['relatedField1', 'relatedField2'] + * + * // request + * `?expand=relatedField1,relatedField2` + * ``` + */ + expand?: Array; /** * Credentials of a superuser to get full access to the PocketBase instance. * 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. diff --git a/src/types/pocketbase-schema.type.ts b/src/types/pocketbase-schema.type.ts index ff218cf..6c0e07e 100644 --- a/src/types/pocketbase-schema.type.ts +++ b/src/types/pocketbase-schema.type.ts @@ -39,22 +39,10 @@ export interface PocketBaseSchemaEntry { * This is only present on "autodate" fields. */ onUpdate?: boolean; -} -/** - * Schema for a PocketBase collection. - */ -export interface PocketBaseCollection { - /** - * Name of the collection. - */ - name: string; - /** - * Type of the collection. - */ - type: "base" | "view" | "auth"; /** - * Schema of the collection. + * The associated collection id that the relation field is referencing + * This is only present on "relation"fields. */ - fields: Array; + collectionId?: string; } diff --git a/test/_mocks/insert-collection.ts b/test/_mocks/insert-collection.ts index 16f9918..0822447 100644 --- a/test/_mocks/insert-collection.ts +++ b/test/_mocks/insert-collection.ts @@ -1,11 +1,12 @@ import { assert } from "console"; +import type { PocketBaseCollection } from "../../src/types/pocketbase-collection.type"; import type { PocketBaseLoaderOptions } from "../../src/types/pocketbase-loader-options.type"; export async function insertCollection( fields: Array>, options: PocketBaseLoaderOptions, superuserToken: string -): Promise { +): Promise { const insertRequest = await fetch(new URL(`api/collections`, options.url), { method: "POST", headers: { @@ -19,4 +20,9 @@ export async function insertCollection( }); assert(insertRequest.status === 200, "Collection is not available."); + + const collection = await insertRequest.json(); + assert(collection.id, "Collection ID is not available."); + + return collection; } diff --git a/test/loader/load-entries.e2e-spec.ts b/test/loader/load-entries.e2e-spec.ts index 1966ecf..9b86c1b 100644 --- a/test/loader/load-entries.e2e-spec.ts +++ b/test/loader/load-entries.e2e-spec.ts @@ -8,17 +8,19 @@ import { describe, expect, test, - vi + vi, + type Mock } from "vitest"; import { loadEntries } from "../../src/loader/load-entries"; import { parseEntry } from "../../src/loader/parse-entry"; +import type { PocketBaseEntry } from "../../src/types/pocketbase-entry.type"; import { getSuperuserToken } from "../../src/utils/get-superuser-token"; import { checkE2eConnection } from "../_mocks/check-e2e-connection"; import { createLoaderContext } from "../_mocks/create-loader-context"; import { createLoaderOptions } from "../_mocks/create-loader-options"; import { deleteCollection } from "../_mocks/delete-collection"; import { insertCollection } from "../_mocks/insert-collection"; -import { insertEntries } from "../_mocks/insert-entry"; +import { insertEntries, insertEntry } from "../_mocks/insert-entry"; vi.mock("../../src/loader/parse-entry"); @@ -119,6 +121,78 @@ describe("loadEntries", () => { await deleteCollection(testOptions, superuserToken); }); + test("should expand related fields in pages", async () => { + const RELATION_FIELD_NAME = "related"; + const BLUE_ENTRY_NAME_FIELD_VALUE = "blue entry"; + + const redCollectionOptions = { + ...options, + collectionName: `red_${randomUUID().replace(/-/g, "")}` + }; + + const blueCollectionOptions = { + ...options, + collectionName: `blue_${randomUUID().replace(/-/g, "")}` + }; + + const testOptions = { + ...options, + collectionName: redCollectionOptions.collectionName, + expand: [RELATION_FIELD_NAME] + }; + + const blueCollection = await insertCollection( + [ + { + name: "name", + type: "text" + } + ], + blueCollectionOptions, + superuserToken + ); + + await insertCollection( + [ + { + name: RELATION_FIELD_NAME, + type: "relation", + collectionId: blueCollection.id + } + ], + redCollectionOptions, + superuserToken + ); + + const parsedEntries: Array = []; + + const blueEntry = await insertEntry( + { name: BLUE_ENTRY_NAME_FIELD_VALUE }, + blueCollectionOptions, + superuserToken + ); + + await insertEntry( + { [RELATION_FIELD_NAME]: blueEntry.id }, + redCollectionOptions, + superuserToken + ); + + (parseEntry as Mock).mockImplementation((entry: PocketBaseEntry) => { + parsedEntries.push(entry); + return entry; // or whatever parseEntry should return + }); + + await loadEntries(testOptions, context, superuserToken, undefined); + + expect(parsedEntries[0]?.expand?.related?.name).toBe( + BLUE_ENTRY_NAME_FIELD_VALUE + ); + + await deleteCollection(redCollectionOptions, superuserToken); + await deleteCollection(blueCollectionOptions, superuserToken); + }); + describe("incremental updates", () => { test("should fetch all entries when updatedField is missing", async () => { const lastModified = new Date(Date.now() - DAY).toISOString(); diff --git a/test/schema/generate-schema.e2e-spec.ts b/test/schema/generate-schema.e2e-spec.ts index cbac1ac..275bb36 100644 --- a/test/schema/generate-schema.e2e-spec.ts +++ b/test/schema/generate-schema.e2e-spec.ts @@ -1,17 +1,46 @@ +import type { LoaderContext } from "astro/loaders"; import type { ZodObject, ZodSchema } from "astro/zod"; -import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { randomUUID } from "crypto"; +import { + afterEach, + assert, + beforeAll, + beforeEach, + describe, + expect, + it, + vi +} from "vitest"; import { generateSchema } from "../../src/schema/generate-schema"; import { transformFileUrl } from "../../src/schema/transform-files"; +import { getSuperuserToken } from "../../src/utils/get-superuser-token"; import { checkE2eConnection } from "../_mocks/check-e2e-connection"; +import { createLoaderContext } from "../_mocks/create-loader-context"; import { createLoaderOptions } from "../_mocks/create-loader-options"; +import { deleteCollection } from "../_mocks/delete-collection"; +import { insertCollection } from "../_mocks/insert-collection"; describe("generateSchema", () => { const options = createLoaderOptions({ collectionName: "_superusers" }); + let context: LoaderContext; + let superuserToken: string; beforeAll(async () => { await checkE2eConnection(); }); + beforeEach(async () => { + context = createLoaderContext(); + + const token = await getSuperuserToken( + options.url, + options.superuserCredentials! + ); + + assert(token, "Superuser token is not available."); + superuserToken = token; + }); + afterEach(() => { vi.resetAllMocks(); }); @@ -44,7 +73,8 @@ describe("generateSchema", () => { "emailVisibility", "verified", "created", - "updated" + "updated", + "expand" ]); }); @@ -63,7 +93,8 @@ describe("generateSchema", () => { "emailVisibility", "verified", "created", - "updated" + "updated", + "expand" ]); }); }); @@ -199,4 +230,90 @@ describe("generateSchema", () => { ) }); }); + + describe("expand field", async () => { + it("the related fields schema is provided for expanded fields", async () => { + const RELATION_FIELD_NAME = "related"; + + const redCollectionOptions = { + ...options, + collectionName: `red_${randomUUID().replace(/-/g, "")}` + }; + + const blueCollectionOptions = { + ...options, + collectionName: `blue_${randomUUID().replace(/-/g, "")}` + }; + + const blueCollection = await insertCollection( + [ + { + name: "name", + type: "text" + } + ], + blueCollectionOptions, + superuserToken + ); + + await insertCollection( + [ + { + name: RELATION_FIELD_NAME, + type: "relation", + collectionId: blueCollection.id + } + ], + redCollectionOptions, + superuserToken + ); + + try { + const testOptions = { + ...options, + collectionName: redCollectionOptions.collectionName, + expand: [RELATION_FIELD_NAME] + }; + const schema = (await generateSchema(testOptions)) as ZodObject< + Record> + >; + + const expandSchema = schema.shape.expand; + const validExpand = { + related: { + collectionId: blueCollection.id, + collectionName: blueCollection.name, + id: "test", + name: "Blue Entry" + } + }; + + expect(() => expandSchema.parse(validExpand)).not.toThrow(); + + const validArrayExpand = { + related: [ + { + collectionId: blueCollection.id, + collectionName: blueCollection.name, + id: "test", + name: "Blue Entry" + }, + { + collectionId: blueCollection.id, + collectionName: blueCollection.name, + id: "test", + name: "Blue Entry" + } + ] + }; + + expect(() => expandSchema.parse(validArrayExpand)).not.toThrow(); + } catch (error) { + console.log(error); + } + + deleteCollection(redCollectionOptions, superuserToken); + deleteCollection(blueCollectionOptions, superuserToken); + }); + }); }); From 4134e779b7e160eb5b5e6dc0b08ef8799fc785a0 Mon Sep 17 00:00:00 2001 From: David Easton <119066700+cf-david-easton@users.noreply.github.com> Date: Sun, 29 Jun 2025 10:39:22 +0100 Subject: [PATCH 2/9] Apply suggestions from code review Co-authored-by: pawcode --- src/schema/generate-schema.ts | 4 ++-- src/types/pocketbase-schema.type.ts | 2 +- test/schema/generate-schema.e2e-spec.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/schema/generate-schema.ts b/src/schema/generate-schema.ts index 580c191..dd84a4f 100644 --- a/src/schema/generate-schema.ts +++ b/src/schema/generate-schema.ts @@ -149,7 +149,7 @@ export async function generateSchema( `The provided field in the expand property "${expandedFieldName}" does not have an associated collection linked to it, we need this in order to know the shape of the related schema.` ); } else { - const expandedchema = await generateSchema({ + const expandedSchema = await generateSchema({ collectionName: expandedFieldDefinition.collectionId, superuserCredentials: options.superuserCredentials, localSchema: options.localSchema, @@ -201,7 +201,7 @@ export async function generateSchema( * Builds a Zod object schema from expandedFields, where each property is either a ZodSchema or an array of ZodSchema. */ export function buildExpandSchema( - expandedFields: Record> + expandedFields: ExpandedFields ): ZodObject>> { const shape: Record> = {}; diff --git a/src/types/pocketbase-schema.type.ts b/src/types/pocketbase-schema.type.ts index 6c0e07e..c89ada0 100644 --- a/src/types/pocketbase-schema.type.ts +++ b/src/types/pocketbase-schema.type.ts @@ -42,7 +42,7 @@ export interface PocketBaseSchemaEntry { /** * The associated collection id that the relation field is referencing - * This is only present on "relation"fields. + * This is only present on "relation" fields. */ collectionId?: string; } diff --git a/test/schema/generate-schema.e2e-spec.ts b/test/schema/generate-schema.e2e-spec.ts index 275bb36..b9386a3 100644 --- a/test/schema/generate-schema.e2e-spec.ts +++ b/test/schema/generate-schema.e2e-spec.ts @@ -312,8 +312,8 @@ describe("generateSchema", () => { console.log(error); } - deleteCollection(redCollectionOptions, superuserToken); - deleteCollection(blueCollectionOptions, superuserToken); + await deleteCollection(redCollectionOptions, superuserToken); + await deleteCollection(blueCollectionOptions, superuserToken); }); }); }); From 75d2917176ca9db4c8ad15171663a314e922dc56 Mon Sep 17 00:00:00 2001 From: David Easton Date: Sun, 29 Jun 2025 11:23:16 +0100 Subject: [PATCH 3/9] feat(expand): added ability to call multiple layers of depth on expand with better error protection --- src/schema/generate-schema.ts | 60 +++++++++++++-------- test/schema/generate-schema.e2e-spec.ts | 72 ++++++++++++------------- 2 files changed, 72 insertions(+), 60 deletions(-) diff --git a/src/schema/generate-schema.ts b/src/schema/generate-schema.ts index dd84a4f..974ca41 100644 --- a/src/schema/generate-schema.ts +++ b/src/schema/generate-schema.ts @@ -135,35 +135,39 @@ export async function generateSchema( if (options.expand) { for (const expandedFieldName of options.expand) { + const [currentLevelFieldName, deeperExpandFields] = + getCurrentLevelExpandedFieldName(expandedFieldName); + const expandedFieldDefinition = collection.fields.find( - (field) => field.name === expandedFieldName + (field) => field.name === currentLevelFieldName ); if (!expandedFieldDefinition) { - console.error( + throw new Error( `The provided field in the expand property "${expandedFieldName}" is not present in the schema of the collection "${options.collectionName}".\nThis will lead to use unable to provide a definition for this field.` ); - } else { - if (!expandedFieldDefinition.collectionId) { - console.error( - `The provided field in the expand property "${expandedFieldName}" does not have an associated collection linked to it, we need this in order to know the shape of the related schema.` - ); - } else { - const expandedSchema = await generateSchema({ - collectionName: expandedFieldDefinition.collectionId, - superuserCredentials: options.superuserCredentials, - localSchema: options.localSchema, - jsonSchemas: options.jsonSchemas, - improveTypes: options.improveTypes, - url: options.url - }); - - expandedFields[expandedFieldName] = z.union([ - expandedchema, - z.array(expandedchema) - ]); - } } + + if (!expandedFieldDefinition.collectionId) { + throw new Error( + `The provided field in the expand property "${expandedFieldName}" does not have an associated collection linked to it, we need this in order to know the shape of the related schema.` + ); + } + + const expandedSchema = await generateSchema({ + collectionName: expandedFieldDefinition.collectionId, + superuserCredentials: options.superuserCredentials, + expand: [deeperExpandFields], + localSchema: options.localSchema, + jsonSchemas: options.jsonSchemas, + improveTypes: options.improveTypes, + url: options.url + }); + + expandedFields[expandedFieldName] = z.union([ + expandedSchema, + z.array(expandedSchema) + ]); } } @@ -217,3 +221,15 @@ export function buildExpandSchema( return z.object(shape); } + +function getCurrentLevelExpandedFieldName(s: string) { + const fields = s.split("."); + + if (fields.length <= 7) { + throw new Error( + `Expand value ${s} exceeds 6 levels of depth that Pocketbase allows` + ); + } + + return fields; +} diff --git a/test/schema/generate-schema.e2e-spec.ts b/test/schema/generate-schema.e2e-spec.ts index b9386a3..910d951 100644 --- a/test/schema/generate-schema.e2e-spec.ts +++ b/test/schema/generate-schema.e2e-spec.ts @@ -268,49 +268,45 @@ describe("generateSchema", () => { superuserToken ); - try { - const testOptions = { - ...options, - collectionName: redCollectionOptions.collectionName, - expand: [RELATION_FIELD_NAME] - }; - const schema = (await generateSchema(testOptions)) as ZodObject< - Record> - >; - - const expandSchema = schema.shape.expand; - const validExpand = { - related: { + const testOptions = { + ...options, + collectionName: redCollectionOptions.collectionName, + expand: [RELATION_FIELD_NAME] + }; + const schema = (await generateSchema(testOptions)) as ZodObject< + Record> + >; + + const expandSchema = schema.shape.expand; + const validExpand = { + related: { + collectionId: blueCollection.id, + collectionName: blueCollection.name, + id: "test", + name: "Blue Entry" + } + }; + + expect(() => expandSchema.parse(validExpand)).not.toThrow(); + + const validArrayExpand = { + related: [ + { + collectionId: blueCollection.id, + collectionName: blueCollection.name, + id: "test", + name: "Blue Entry" + }, + { collectionId: blueCollection.id, collectionName: blueCollection.name, id: "test", name: "Blue Entry" } - }; - - expect(() => expandSchema.parse(validExpand)).not.toThrow(); - - const validArrayExpand = { - related: [ - { - collectionId: blueCollection.id, - collectionName: blueCollection.name, - id: "test", - name: "Blue Entry" - }, - { - collectionId: blueCollection.id, - collectionName: blueCollection.name, - id: "test", - name: "Blue Entry" - } - ] - }; - - expect(() => expandSchema.parse(validArrayExpand)).not.toThrow(); - } catch (error) { - console.log(error); - } + ] + }; + + expect(() => expandSchema.parse(validArrayExpand)).not.toThrow(); await deleteCollection(redCollectionOptions, superuserToken); await deleteCollection(blueCollectionOptions, superuserToken); From 258f04da3c85cd7d147523f01052527b21809061 Mon Sep 17 00:00:00 2001 From: David Easton Date: Sun, 29 Jun 2025 13:29:06 +0100 Subject: [PATCH 4/9] feat(expand): added length guard for expand option --- src/schema/generate-schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema/generate-schema.ts b/src/schema/generate-schema.ts index 974ca41..ea8dc14 100644 --- a/src/schema/generate-schema.ts +++ b/src/schema/generate-schema.ts @@ -133,7 +133,7 @@ export async function generateSchema( } } - if (options.expand) { + if (options.expand && options.expand.length > 0) { for (const expandedFieldName of options.expand) { const [currentLevelFieldName, deeperExpandFields] = getCurrentLevelExpandedFieldName(expandedFieldName); From 9d20391af440a2f35591accabfacf93fa32bb4e0 Mon Sep 17 00:00:00 2001 From: David Easton Date: Sun, 29 Jun 2025 13:54:27 +0100 Subject: [PATCH 5/9] feat(expand): added guard for expand nesting when no more chained segments exist --- src/schema/generate-schema.ts | 8 ++++---- src/types/pocketbase-entry.type.ts | 2 +- test/loader/load-entries.e2e-spec.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/schema/generate-schema.ts b/src/schema/generate-schema.ts index ea8dc14..9453082 100644 --- a/src/schema/generate-schema.ts +++ b/src/schema/generate-schema.ts @@ -135,7 +135,7 @@ export async function generateSchema( if (options.expand && options.expand.length > 0) { for (const expandedFieldName of options.expand) { - const [currentLevelFieldName, deeperExpandFields] = + const [currentLevelFieldName, ...deeperExpandFields] = getCurrentLevelExpandedFieldName(expandedFieldName); const expandedFieldDefinition = collection.fields.find( @@ -157,7 +157,7 @@ export async function generateSchema( const expandedSchema = await generateSchema({ collectionName: expandedFieldDefinition.collectionId, superuserCredentials: options.superuserCredentials, - expand: [deeperExpandFields], + expand: deeperExpandFields.length ? deeperExpandFields : undefined, localSchema: options.localSchema, jsonSchemas: options.jsonSchemas, improveTypes: options.improveTypes, @@ -222,10 +222,10 @@ export function buildExpandSchema( return z.object(shape); } -function getCurrentLevelExpandedFieldName(s: string) { +function getCurrentLevelExpandedFieldName(s: string): Array { const fields = s.split("."); - if (fields.length <= 7) { + if (fields.length >= 7) { throw new Error( `Expand value ${s} exceeds 6 levels of depth that Pocketbase allows` ); diff --git a/src/types/pocketbase-entry.type.ts b/src/types/pocketbase-entry.type.ts index efa250e..4b1e934 100644 --- a/src/types/pocketbase-entry.type.ts +++ b/src/types/pocketbase-entry.type.ts @@ -17,7 +17,7 @@ interface PocketBaseBaseEntry { /** * Optional property that contains all relational fields that have been expanded to contain their linked entry(s) */ - expand?: Record>; + expand?: Record; } /** diff --git a/test/loader/load-entries.e2e-spec.ts b/test/loader/load-entries.e2e-spec.ts index 9b86c1b..b68e6b5 100644 --- a/test/loader/load-entries.e2e-spec.ts +++ b/test/loader/load-entries.e2e-spec.ts @@ -185,7 +185,7 @@ describe("loadEntries", () => { await loadEntries(testOptions, context, superuserToken, undefined); - expect(parsedEntries[0]?.expand?.related?.name).toBe( + expect(parsedEntries[0]?.expand?.related.name).toBe( BLUE_ENTRY_NAME_FIELD_VALUE ); From 3933c35ed44372a231b62c8f13c54c26e5277453 Mon Sep 17 00:00:00 2001 From: David Easton Date: Sat, 12 Jul 2025 15:59:03 +0100 Subject: [PATCH 6/9] feat(expand): updated expand schema parsing to more accurately represent the single or multi value --- src/schema/generate-schema.ts | 12 +++++++----- src/schema/parse-schema.ts | 23 +++++++++++++++++++---- test/schema/generate-schema.e2e-spec.ts | 13 ++----------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/schema/generate-schema.ts b/src/schema/generate-schema.ts index 9453082..9fdc282 100644 --- a/src/schema/generate-schema.ts +++ b/src/schema/generate-schema.ts @@ -6,7 +6,7 @@ import type { } from "../types/pocketbase-collection.type"; import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type"; import { getRemoteSchema } from "./get-remote-schema"; -import { parseSchema } from "./parse-schema"; +import { parseExpandedSchemaField, parseSchema } from "./parse-schema"; import { readLocalSchema } from "./read-local-schema"; import { transformFiles } from "./transform-files"; @@ -154,6 +154,8 @@ export async function generateSchema( ); } + const isRequired = expandedFieldDefinition.required; + const expandedSchema = await generateSchema({ collectionName: expandedFieldDefinition.collectionId, superuserCredentials: options.superuserCredentials, @@ -164,10 +166,10 @@ export async function generateSchema( url: options.url }); - expandedFields[expandedFieldName] = z.union([ - expandedSchema, - z.array(expandedSchema) - ]); + expandedFields[expandedFieldName] = parseExpandedSchemaField( + expandedFieldDefinition, + expandedSchema + ); } } diff --git a/src/schema/parse-schema.ts b/src/schema/parse-schema.ts index 20582bd..f4e464b 100644 --- a/src/schema/parse-schema.ts +++ b/src/schema/parse-schema.ts @@ -1,4 +1,4 @@ -import { z } from "astro/zod"; +import { z, ZodSchema } from "astro/zod"; import type { PocketBaseCollection } from "../types/pocketbase-collection.type"; import type { PocketBaseSchemaEntry } from "../types/pocketbase-schema.type"; @@ -99,6 +99,21 @@ export function parseSchema( return fields; } +export function parseExpandedSchemaField( + originalField: PocketBaseSchemaEntry, + expandedSchema: ZodSchema +): z.ZodType { + const isRequired = originalField.required; + let fieldType = parseSingleOrMultipleValues(originalField, expandedSchema); + + // If the field is not required, mark it as optional + if (!isRequired) { + fieldType = z.preprocess((val) => val || undefined, z.optional(fieldType)); + } + + return fieldType; +} + /** * Parse the field type based on the number of values it can have * @@ -112,9 +127,9 @@ function parseSingleOrMultipleValues( 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; - } else { - return z.array(type); } + + return z.array(type); } diff --git a/test/schema/generate-schema.e2e-spec.ts b/test/schema/generate-schema.e2e-spec.ts index 910d951..c0faa7a 100644 --- a/test/schema/generate-schema.e2e-spec.ts +++ b/test/schema/generate-schema.e2e-spec.ts @@ -261,7 +261,8 @@ describe("generateSchema", () => { { name: RELATION_FIELD_NAME, type: "relation", - collectionId: blueCollection.id + collectionId: blueCollection.id, + maxSelect: 999 } ], redCollectionOptions, @@ -278,16 +279,6 @@ describe("generateSchema", () => { >; const expandSchema = schema.shape.expand; - const validExpand = { - related: { - collectionId: blueCollection.id, - collectionName: blueCollection.name, - id: "test", - name: "Blue Entry" - } - }; - - expect(() => expandSchema.parse(validExpand)).not.toThrow(); const validArrayExpand = { related: [ From e6b541f06e8f72af4bae31ead3aa91d387058efb Mon Sep 17 00:00:00 2001 From: David Easton Date: Sat, 12 Jul 2025 18:45:18 +0100 Subject: [PATCH 7/9] feat(expand): implemented feedback and simplified expand typing --- src/loader/load-entries.ts | 2 +- src/schema/generate-schema.ts | 48 ++++--------------------- src/types/pocketbase-collection.type.ts | 13 ++----- src/types/pocketbase-entry.type.ts | 2 +- test/loader/load-entries.e2e-spec.ts | 34 +++++++++--------- 5 files changed, 28 insertions(+), 71 deletions(-) diff --git a/src/loader/load-entries.ts b/src/loader/load-entries.ts index 010292f..581c63b 100644 --- a/src/loader/load-entries.ts +++ b/src/loader/load-entries.ts @@ -66,7 +66,7 @@ export async function loadEntries( searchParams.set("filter", filters.join("&&")); } - // Add filters to search parameters + // Add expand to search parameters if (options.expand) { searchParams.set("expand", options.expand.join(",")); } diff --git a/src/schema/generate-schema.ts b/src/schema/generate-schema.ts index 9fdc282..8db6158 100644 --- a/src/schema/generate-schema.ts +++ b/src/schema/generate-schema.ts @@ -1,9 +1,6 @@ -import type { ZodObject, ZodSchema } from "astro/zod"; +import type { ZodSchema } from "astro/zod"; import { z } from "astro/zod"; -import type { - ExpandedFields, - PocketBaseCollection -} from "../types/pocketbase-collection.type"; +import type { PocketBaseCollection } from "../types/pocketbase-collection.type"; import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type"; import { getRemoteSchema } from "./get-remote-schema"; import { parseExpandedSchemaField, parseSchema } from "./parse-schema"; @@ -36,7 +33,7 @@ export async function generateSchema( options: PocketBaseLoaderOptions ): Promise { let collection: PocketBaseCollection | undefined; - const expandedFields: ExpandedFields = {}; + const expandedFields: Record = {}; // Try to get the schema directly from the PocketBase instance collection = await getRemoteSchema(options); @@ -154,8 +151,6 @@ export async function generateSchema( ); } - const isRequired = expandedFieldDefinition.required; - const expandedSchema = await generateSchema({ collectionName: expandedFieldDefinition.collectionId, superuserCredentials: options.superuserCredentials, @@ -173,14 +168,10 @@ export async function generateSchema( } } - const expandSchema = { - expand: buildExpandSchema(expandedFields).optional() - }; - const schema = z.object({ ...BASIC_SCHEMA, ...fields, - ...expandSchema + expand: z.optional(z.object(expandedFields)) }); // Get all file fields @@ -194,34 +185,9 @@ export async function generateSchema( } // Transform file names to file urls - return schema.transform((entry) => { - if (Array.isArray(entry)) { - return entry.map((e) => transformFiles(options.url, fileFields, e)); - } - - return transformFiles(options.url, fileFields, entry); - }); -} - -/** - * Builds a Zod object schema from expandedFields, where each property is either a ZodSchema or an array of ZodSchema. - */ -export function buildExpandSchema( - expandedFields: ExpandedFields -): ZodObject>> { - const shape: Record> = {}; - - for (const key in expandedFields) { - const value = expandedFields[key]; - if (Array.isArray(value)) { - // If it's an array, wrap the first element as a ZodArray (assuming all elements are the same schema) - shape[key] = z.array(value[0]); - } else { - shape[key] = value; - } - } - - return z.object(shape); + return schema.transform((entry) => + transformFiles(options.url, fileFields, entry) + ); } function getCurrentLevelExpandedFieldName(s: string): Array { diff --git a/src/types/pocketbase-collection.type.ts b/src/types/pocketbase-collection.type.ts index eb942aa..e6c9ed5 100644 --- a/src/types/pocketbase-collection.type.ts +++ b/src/types/pocketbase-collection.type.ts @@ -1,10 +1,9 @@ -import type { ZodSchema } from "astro/zod"; import type { PocketBaseSchemaEntry } from "./pocketbase-schema.type"; /** - * Base interface for all PocketBase entries. + * Base interface for all PocketBase collections. */ -interface PocketBaseBaseCollection { +export interface PocketBaseCollection { /** * ID of the collection. */ @@ -22,11 +21,3 @@ interface PocketBaseBaseCollection { */ fields: Array; } - -/** - * Type for a PocketBase entry. - */ -export type PocketBaseCollection = PocketBaseBaseCollection & - Record; - -export type ExpandedFields = Record>; diff --git a/src/types/pocketbase-entry.type.ts b/src/types/pocketbase-entry.type.ts index 4b1e934..47001d7 100644 --- a/src/types/pocketbase-entry.type.ts +++ b/src/types/pocketbase-entry.type.ts @@ -17,7 +17,7 @@ interface PocketBaseBaseEntry { /** * Optional property that contains all relational fields that have been expanded to contain their linked entry(s) */ - expand?: Record; + expand?: Record>; } /** diff --git a/test/loader/load-entries.e2e-spec.ts b/test/loader/load-entries.e2e-spec.ts index b68e6b5..701a744 100644 --- a/test/loader/load-entries.e2e-spec.ts +++ b/test/loader/load-entries.e2e-spec.ts @@ -12,7 +12,7 @@ import { type Mock } from "vitest"; import { loadEntries } from "../../src/loader/load-entries"; -import { parseEntry } from "../../src/loader/parse-entry"; +import * as parseEntry from "../../src/loader/parse-entry"; import type { PocketBaseEntry } from "../../src/types/pocketbase-entry.type"; import { getSuperuserToken } from "../../src/utils/get-superuser-token"; import { checkE2eConnection } from "../_mocks/check-e2e-connection"; @@ -30,6 +30,7 @@ describe("loadEntries", () => { const options = createLoaderOptions({ collectionName: "_superusers" }); let context: LoaderContext; let superuserToken: string; + let parsedEntrySpy: Mock; beforeAll(async () => { await checkE2eConnection(); @@ -37,6 +38,7 @@ describe("loadEntries", () => { beforeEach(async () => { context = createLoaderContext(); + parsedEntrySpy = vi.spyOn(parseEntry, "parseEntry") as Mock; const token = await getSuperuserToken( options.url, @@ -54,7 +56,7 @@ describe("loadEntries", () => { test("should fetch entries without errors", async () => { await loadEntries(options, context, superuserToken, undefined); - expect(parseEntry).toHaveBeenCalledOnce(); + expect(parsedEntrySpy).toHaveBeenCalledOnce(); }); test("should handle empty response gracefully", async () => { @@ -62,7 +64,7 @@ describe("loadEntries", () => { await loadEntries(testOptions, context, superuserToken, undefined); - expect(parseEntry).not.toHaveBeenCalled(); + expect(parsedEntrySpy).not.toHaveBeenCalled(); }); test("should load all pages", async () => { @@ -81,7 +83,7 @@ describe("loadEntries", () => { await loadEntries(testOptions, context, superuserToken, undefined); - expect(parseEntry).toHaveBeenCalledTimes(numberOfEntries); + expect(parsedEntrySpy).toHaveBeenCalledTimes(numberOfEntries); await deleteCollection(testOptions, superuserToken); }); @@ -116,7 +118,7 @@ describe("loadEntries", () => { ); await loadEntries(testOptions, context, superuserToken, undefined); - expect(parseEntry).toHaveBeenCalledTimes(numberOfEntries); + expect(parsedEntrySpy).toHaveBeenCalledTimes(numberOfEntries); await deleteCollection(testOptions, superuserToken); }); @@ -178,16 +180,14 @@ describe("loadEntries", () => { superuserToken ); - (parseEntry as Mock).mockImplementation((entry: PocketBaseEntry) => { - parsedEntries.push(entry); - return entry; // or whatever parseEntry should return - }); - await loadEntries(testOptions, context, superuserToken, undefined); - expect(parsedEntries[0]?.expand?.related.name).toBe( - BLUE_ENTRY_NAME_FIELD_VALUE - ); + const entryFromCall = parsedEntrySpy!.mock.calls[0][0]; + + const relatedEntry = entryFromCall.expand?.related as PocketBaseEntry; + + // Ensure expand and related exist and are of expected type + expect(relatedEntry?.name).toBe(BLUE_ENTRY_NAME_FIELD_VALUE); await deleteCollection(redCollectionOptions, superuserToken); await deleteCollection(blueCollectionOptions, superuserToken); @@ -198,7 +198,7 @@ describe("loadEntries", () => { const lastModified = new Date(Date.now() - DAY).toISOString(); await loadEntries(options, context, superuserToken, lastModified); - expect(parseEntry).toHaveBeenCalledOnce(); + expect(parsedEntrySpy).toHaveBeenCalledOnce(); }); test("should fetch updated entries", async () => { @@ -207,7 +207,7 @@ describe("loadEntries", () => { await loadEntries(testOptions, context, superuserToken, lastModified); - expect(parseEntry).toHaveBeenCalledOnce(); + expect(parsedEntrySpy).toHaveBeenCalledOnce(); }); test("should do nothing without updated entries", async () => { @@ -216,7 +216,7 @@ describe("loadEntries", () => { await loadEntries(testOptions, context, superuserToken, lastModified); - expect(parseEntry).not.toHaveBeenCalled(); + expect(parsedEntrySpy).not.toHaveBeenCalled(); }); test("should not fetch updated entries excluded from filter", async () => { @@ -229,7 +229,7 @@ describe("loadEntries", () => { await loadEntries(testOptions, context, superuserToken, lastModified); - expect(parseEntry).not.toHaveBeenCalled(); + expect(parsedEntrySpy).not.toHaveBeenCalled(); }); }); From bc1cca06411b6cc8b675845b00440df4ed05da8b Mon Sep 17 00:00:00 2001 From: David Easton Date: Sat, 12 Jul 2025 18:47:07 +0100 Subject: [PATCH 8/9] feat(expand): removed unused const --- test/loader/load-entries.e2e-spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/loader/load-entries.e2e-spec.ts b/test/loader/load-entries.e2e-spec.ts index 701a744..15bad16 100644 --- a/test/loader/load-entries.e2e-spec.ts +++ b/test/loader/load-entries.e2e-spec.ts @@ -166,8 +166,6 @@ describe("loadEntries", () => { superuserToken ); - const parsedEntries: Array = []; - const blueEntry = await insertEntry( { name: BLUE_ENTRY_NAME_FIELD_VALUE }, blueCollectionOptions, @@ -183,7 +181,6 @@ describe("loadEntries", () => { await loadEntries(testOptions, context, superuserToken, undefined); const entryFromCall = parsedEntrySpy!.mock.calls[0][0]; - const relatedEntry = entryFromCall.expand?.related as PocketBaseEntry; // Ensure expand and related exist and are of expected type From 3f51a054b36be9b2a23cd76e6daa121ab3e22a67 Mon Sep 17 00:00:00 2001 From: David Easton Date: Sat, 12 Jul 2025 20:05:46 +0100 Subject: [PATCH 9/9] feat(expand): corrected typing for collection in test --- test/schema/parse-schema.spec.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/schema/parse-schema.spec.ts b/test/schema/parse-schema.spec.ts index c079ca5..9f1c53c 100644 --- a/test/schema/parse-schema.spec.ts +++ b/test/schema/parse-schema.spec.ts @@ -1,12 +1,13 @@ import { z } from "astro/zod"; import { describe, expect, test } from "vitest"; import { parseSchema } from "../../src/schema/parse-schema"; -import type { PocketBaseCollection } from "../../src/types/pocketbase-schema.type"; +import type { PocketBaseCollection } from "../../src/types/pocketbase-collection.type"; describe("parseSchema", () => { describe("number", () => { test("should parse number fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "numberCollection", type: "base", fields: [{ name: "age", type: "number", required: true, hidden: false }] @@ -25,6 +26,7 @@ describe("parseSchema", () => { test("should parse optional number fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "numberCollection", type: "base", fields: [ @@ -44,6 +46,7 @@ describe("parseSchema", () => { test("should parse optional number fields correctly with improved types", () => { const collection: PocketBaseCollection = { + id: "0", name: "numberCollection", type: "base", fields: [ @@ -65,6 +68,7 @@ describe("parseSchema", () => { describe("boolean", () => { test("should parse boolean fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "booleanCollection", type: "base", fields: [ @@ -85,6 +89,7 @@ describe("parseSchema", () => { test("should parse optional boolean fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "booleanCollection", type: "base", fields: [ @@ -104,6 +109,7 @@ describe("parseSchema", () => { test("should parse optional boolean fields correctly with improved types", () => { const collection: PocketBaseCollection = { + id: "0", name: "booleanCollection", type: "base", fields: [ @@ -125,6 +131,7 @@ describe("parseSchema", () => { describe("date", () => { test("should parse date fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "dateCollection", type: "base", fields: [ @@ -147,6 +154,7 @@ describe("parseSchema", () => { test("should parse optional date fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "dateCollection", type: "base", fields: [ @@ -168,6 +176,7 @@ describe("parseSchema", () => { describe("autodate", () => { test("should parse autodate fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "dateCollection", type: "base", fields: [ @@ -195,6 +204,7 @@ describe("parseSchema", () => { test("should parse optional autodate fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "dateCollection", type: "base", fields: [ @@ -219,6 +229,7 @@ describe("parseSchema", () => { test("should parse autodate fields with onCreate correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "dateCollection", type: "base", fields: [ @@ -246,6 +257,7 @@ describe("parseSchema", () => { describe("geoPoint", () => { test("should parse geoPoint fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "geoPointCollection", type: "base", fields: [ @@ -274,6 +286,7 @@ describe("parseSchema", () => { test("should parse optional geoPoint fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "geoPointCollection", type: "base", fields: [ @@ -300,6 +313,7 @@ describe("parseSchema", () => { describe("select", () => { test("should parse select fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "selectCollection", type: "base", fields: [ @@ -326,6 +340,7 @@ describe("parseSchema", () => { test("should throw an error if no values are defined", () => { const collection: PocketBaseCollection = { + id: "0", name: "selectCollection", type: "base", fields: [ @@ -343,6 +358,7 @@ describe("parseSchema", () => { test("should parse select fields with multiple values correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "selectCollection", type: "base", fields: [ @@ -372,6 +388,7 @@ describe("parseSchema", () => { test("should parse optional select fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "selectCollection", type: "base", fields: [ @@ -400,6 +417,7 @@ describe("parseSchema", () => { describe("relation", () => { test("should parse relation fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "relationCollection", type: "base", fields: [ @@ -425,6 +443,7 @@ describe("parseSchema", () => { test("should parse relation fields with multiple values correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "relationCollection", type: "base", fields: [ @@ -451,6 +470,7 @@ describe("parseSchema", () => { test("should parse optional relation fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "relationCollection", type: "base", fields: [ @@ -478,6 +498,7 @@ describe("parseSchema", () => { describe("file", () => { test("should parse file fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "fileCollection", type: "base", fields: [ @@ -503,6 +524,7 @@ describe("parseSchema", () => { test("should parse file fields with multiple values correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "fileCollection", type: "base", fields: [ @@ -536,6 +558,7 @@ describe("parseSchema", () => { test("should parse optional file fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "fileCollection", type: "base", fields: [ @@ -563,6 +586,7 @@ describe("parseSchema", () => { describe("json", () => { test("should parse json fields with custom schema correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "jsonCollection", type: "base", fields: [ @@ -601,6 +625,7 @@ describe("parseSchema", () => { test("should parse json fields without custom schema correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "jsonCollection", type: "base", fields: [ @@ -631,6 +656,7 @@ describe("parseSchema", () => { test("should parse optional json fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "jsonCollection", type: "base", fields: [ @@ -660,6 +686,7 @@ describe("parseSchema", () => { describe("text", () => { test("should parse text fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "stringCollection", type: "base", fields: [{ name: "name", type: "text", required: true, hidden: false }] @@ -678,6 +705,7 @@ describe("parseSchema", () => { test("should parse optional text fields correctly", () => { const collection: PocketBaseCollection = { + id: "0", name: "stringCollection", type: "base", fields: [{ name: "name", type: "text", required: false, hidden: false }]