-
Notifications
You must be signed in to change notification settings - Fork 4
feat(expand): Enables the use of Pocketbases 'Expand' property #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
6b3284e
4134e77
75d2917
258f04d
9d20391
3933c35
e6b541f
bc1cca0
0f342cb
3f51a05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ZodSchema> { | ||
| 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({ | ||
cf-david-easton marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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() | ||
cf-david-easton marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| 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<string, ZodSchema | Array<ZodSchema>> | ||
cf-david-easton marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ): ZodObject<Record<string, ZodSchema | z.ZodArray<ZodSchema>>> { | ||
| const shape: Record<string, ZodSchema | z.ZodArray<ZodSchema>> = {}; | ||
|
|
||
| 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); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import type { ZodSchema } from "astro/zod"; | ||
| import type { PocketBaseSchemaEntry } from "./pocketbase-schema.type"; | ||
|
|
||
| /** | ||
| * Base interface for all PocketBase entries. | ||
cf-david-easton marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| */ | ||
| 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<PocketBaseSchemaEntry>; | ||
| } | ||
|
|
||
| /** | ||
| * Type for a PocketBase entry. | ||
| */ | ||
| export type PocketBaseCollection = PocketBaseBaseCollection & | ||
| Record<string, unknown>; | ||
cf-david-easton marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| export type ExpandedFields = Record<string, ZodSchema | Array<ZodSchema>>; | ||
cf-david-easton marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<PocketBaseEntry> = []; | ||
|
|
||
| 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(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.