astro-loader-pocketbase 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 pawcode Development
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,10 @@
1
- # astro-pocketbase-loader
1
+ # astro-loader-pocketbase
2
+
3
+ <!-- ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/pawcoding/astro-loader-pocketbase/release.yaml?style=flat-square) -->
4
+ [![NPM Version](https://img.shields.io/npm/v/astro-loader-pocketbase?style=flat-square)](https://www.npmjs.com/package/astro-loader-pocketbase)
5
+ [![NPM Downloads](https://img.shields.io/npm/dw/astro-loader-pocketbase?style=flat-square)](https://www.npmjs.com/package/astro-loader-pocketbase)
6
+ [![GitHub License](https://img.shields.io/github/license/pawcoding/astro-loader-pocketbase?style=flat-square)](https://github.com/pawcoding/astro-loader-pocketbase/blob/master/LICENSE)
7
+ [![Discord](https://img.shields.io/discord/484669557747875862?style=flat-square&label=Discord)](https://discord.gg/GzgTh4hxrx)
2
8
 
3
9
  This package is a simple loader to load data from a PocketBase database into Astro using the [Astro Loader API](https://5-0-0-beta.docs.astro.build/en/reference/loader-reference/) introduced in Astro 5.
4
10
 
@@ -19,12 +25,36 @@ const blog = defineCollection({
19
25
  });
20
26
  ```
21
27
 
28
+ By default, the loader will only fetch entries that have been modified since the last build.
29
+ Remember that due to the nature [Astros Content Layer lifecycle](https://astro.build/blog/content-layer-deep-dive#content-layer-lifecycle), the loader will **only fetch entries at build time**, even when using on-demand rendering.
30
+ If you want to update your deployed site with new entries, you need to rebuild it.
31
+
32
+ <sub>When running the dev server, you can trigger a reload by using `s + enter`.</sub>
33
+
22
34
  ### Type Generation
23
35
 
24
36
  The loader can automatically generate types for your collection.
25
- To do this, you need to provide the email and password of an admin of the PocketBase instance.
37
+ This is useful for type checking and autocompletion in your editor.
38
+ These types can be generated in two ways:
39
+
40
+ #### Remote Schema
41
+
42
+ To use the lice remote schema, you need to provide the email and password of an admin of the PocketBase instance.
26
43
  Under the hood, the loader will use the [PocketBase API](https://pocketbase.io/docs/api-collections/#view-collection) to fetch the schema of your collection and generate types with Zod based on that schema.
27
44
 
45
+ #### Local Schema
46
+
47
+ If you don't want to provide the admin credentials (e.g. in a public repository), you can also provide the schema manually via a JSON file.
48
+ In PocketBase you can export the schema of the whole database to a `pb_schema.json` file.
49
+ If you provide the path to this file, the loader will use this schema to generate the types locally.
50
+
51
+ When admin credentials are provided, the loader will **always use the remote schema** since it's more up-to-date.
52
+
53
+ #### Manual Schema
54
+
55
+ If you don't want to use the automatic type generation, you can also [provide your own schema manually](https://5-0-0-beta.docs.astro.build/en/guides/content-collections/#defining-the-collection-schema).
56
+ This manual schema will **always override the automatic type generation**.
57
+
28
58
  ### Private Collections
29
59
 
30
60
  If you want to access a private collection, you also need to provide the admin credentials.
@@ -39,4 +69,5 @@ Otherwise, you need to make the collection public in the PocketBase dashboard.
39
69
  | `content` | `string \| Array<string>` | x | The field in the collection to use as content. This can also be an array of fields. |
40
70
  | `adminEmail` | `string` | | The email of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
41
71
  | `adminPassword` | `string` | | The password of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
72
+ | `localSchema` | `string` | | The path to a local schema file. This is used for automatic type generation. |
42
73
  | `forceUpdate` | `boolean` | | If set to `true`, the loader will fetch every entry instead of only the ones modified since the last build. |
package/package.json CHANGED
@@ -1,38 +1,41 @@
1
1
  {
2
2
  "name": "astro-loader-pocketbase",
3
+ "version": "0.2.1",
3
4
  "description": "A content loader for Astro that uses the PocketBase API",
5
+ "license": "MIT",
6
+ "author": "Luis Wolf <development@pawcode.de> (https://pawcode.de)",
4
7
  "homepage": "https://github.com/pawcoding/astro-loader-pocketbase",
5
- "version": "0.1.0",
6
8
  "type": "module",
7
9
  "exports": {
8
10
  ".": "./index.ts"
9
11
  },
10
12
  "files": [
11
- "src",
12
- "index.ts"
13
+ "index.ts",
14
+ "src"
13
15
  ],
14
- "author": {
15
- "name": "Luis Wolf",
16
- "email": "development@pawcode.de",
17
- "url": "https://pawcode.de"
18
- },
19
- "keywords": [
20
- "withastro",
21
- "astro",
22
- "astro-loader",
23
- "astro-content-loader",
24
- "pocketbase"
25
- ],
26
- "scripts": {},
27
- "devDependencies": {
28
- "astro": "^5.0.0-beta.1"
16
+ "scripts": {
17
+ "lint": "npx eslint",
18
+ "prepare": "husky"
29
19
  },
30
20
  "peerDependencies": {
31
21
  "astro": "^5.0.0"
32
22
  },
33
- "dependencies": {
34
- "@astrojs/check": "^0.9.3",
35
- "typescript": "^5.6.2"
23
+ "devDependencies": {
24
+ "@eslint/js": "^9.11.1",
25
+ "@stylistic/eslint-plugin": "^2.8.0",
26
+ "@types/node": "^22.5.5",
27
+ "astro": "^5.0.0-beta.1",
28
+ "eslint": "^9.11.1",
29
+ "globals": "^15.9.0",
30
+ "husky": "^9.1.6",
31
+ "typescript": "^5.6.2",
32
+ "typescript-eslint": "^8.7.0"
36
33
  },
37
- "license": "MIT"
34
+ "keywords": [
35
+ "astro",
36
+ "astro-content-loader",
37
+ "astro-loader",
38
+ "pocketbase",
39
+ "withastro"
40
+ ]
38
41
  }
@@ -1,8 +1,10 @@
1
1
  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
- import { getAdminToken } from "./utils/get-admin-token";
4
+ import type { PocketBaseSchemaEntry } from "./types/pocketbase-schema.type";
5
+ import { getRemoteSchema } from "./utils/get-remote-schema";
5
6
  import { parseSchema } from "./utils/parse-schema";
7
+ import { readLocalSchema } from "./utils/read-local-schema";
6
8
 
7
9
  /**
8
10
  * Basic schema for every PocketBase collection.
@@ -24,57 +26,26 @@ const BASIC_SCHEMA = {
24
26
  export async function generateSchema(
25
27
  options: PocketBaseLoaderOptions
26
28
  ): Promise<ZodSchema> {
27
- // TODO: Add support for local schema files
29
+ let schema: Array<PocketBaseSchemaEntry> | undefined;
28
30
 
29
- // If no admin email and password are provided, we can't get the schema from the API
30
- if (!options.adminEmail || !options.adminPassword) {
31
- console.warn(
32
- "Make sure to add an admin email and password to the config to get automatic type generation."
33
- );
31
+ // Try to get the schema directly from the PocketBase instance
32
+ schema = await getRemoteSchema(options);
34
33
 
35
- // Return the basic schema
36
- return z.object(BASIC_SCHEMA);
34
+ // If the schema is not available, try to read it from a local schema file
35
+ if (!schema && options.localSchema) {
36
+ schema = await readLocalSchema(options.localSchema, options.collectionName);
37
37
  }
38
38
 
39
- // Get the admin token
40
- const token = await getAdminToken(
41
- options.url,
42
- options.adminEmail,
43
- options.adminPassword
44
- );
45
-
46
- // If the token is invalid, return the basic schema
47
- if (!token) {
48
- return z.object(BASIC_SCHEMA);
49
- }
50
-
51
- // Build URL and headers for the schema request
52
- const schemaUrl = new URL(
53
- `api/collections/${options.collectionName}`,
54
- options.url
55
- ).href;
56
- const schemaHeaders = new Headers();
57
- schemaHeaders.set("Authorization", token);
58
-
59
- // Fetch the schema
60
- const schemaRequest = await fetch(schemaUrl, {
61
- headers: schemaHeaders
62
- });
63
-
64
- // If the request was not successful, return the basic schema
65
- if (!schemaRequest.ok) {
66
- const reason = await schemaRequest.json().then((data) => data.message);
67
- const errorMessage = `Fetching schema from ${options.collectionName} failed with status code ${schemaRequest.status}.\nReason: ${reason}`;
68
- console.error(errorMessage);
69
-
39
+ // If the schema is still not available, return the basic schema
40
+ if (!schema) {
41
+ console.error(
42
+ `No schema available for ${options.collectionName}. Only basic types are available. Please check your configuration and provide a valid schema file or admin credentials.`
43
+ );
70
44
  return z.object(BASIC_SCHEMA);
71
45
  }
72
46
 
73
- // Get the schema from the response
74
- const schema = await schemaRequest.json();
75
-
76
47
  // Parse the schema
77
- const fields = parseSchema(schema.schema);
48
+ const fields = parseSchema(schema);
78
49
 
79
50
  // Check if the content field is present
80
51
  if (typeof options.content === "string" && !fields[options.content]) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Base interface for all PocketBase entries.
3
+ */
4
+ interface PocketBaseBaseEntry {
5
+ /**
6
+ * ID of the entry.
7
+ */
8
+ id: string;
9
+ /**
10
+ * ID of the collection the entry belongs to.
11
+ */
12
+ collectionId: string;
13
+ /**
14
+ * Name of the collection the entry belongs to.
15
+ */
16
+ collectionName: string;
17
+ /**
18
+ * Date the entry was created.
19
+ */
20
+ created: string;
21
+ /**
22
+ * Date the entry was last updated.
23
+ */
24
+ updated: string;
25
+ }
26
+
27
+ /**
28
+ * Type for a PocketBase entry.
29
+ */
30
+ export type PocketBaseEntry = PocketBaseBaseEntry & Record<string, unknown>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Options for the PocketBase loader.
3
3
  */
4
- export type PocketBaseLoaderOptions = {
4
+ export interface PocketBaseLoaderOptions {
5
5
  /**
6
6
  * URL of the PocketBase instance.
7
7
  */
@@ -30,9 +30,15 @@ export type PocketBaseLoaderOptions = {
30
30
  * Together with `adminEmail` this is required to get automatic type generation and to access all resources even if they are not public.
31
31
  */
32
32
  adminPassword?: string;
33
+ /**
34
+ * File path to the local schema file.
35
+ * This file will be used to generate the schema for the collection.
36
+ * If admin credentials are provided (see `adminEmail` and `adminPassword`), this option will be ignored.
37
+ */
38
+ localSchema?: string;
33
39
  /**
34
40
  * By default, the loader will only fetch entries that have been modified since the last fetch.
35
41
  * If you want to fetch all entries, set this to `true`.
36
42
  */
37
43
  forceUpdate?: boolean;
38
- };
44
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Entry for a collections schema in PocketBase.
3
+ */
4
+ export interface PocketBaseSchemaEntry {
5
+ /**
6
+ * Name of the field.
7
+ */
8
+ name: string;
9
+ /**
10
+ * Type of the field.
11
+ */
12
+ type: string;
13
+ /**
14
+ * Whether the field is required.
15
+ */
16
+ required: boolean;
17
+ /**
18
+ * Options for the field.
19
+ */
20
+ options: {
21
+ /**
22
+ * Values for a select field.
23
+ * This is only present if the field type is "select".
24
+ */
25
+ values?: Array<string>;
26
+ /**
27
+ * Maximum number of values for a select field.
28
+ * This is only present on "select", "relation", and "file" fields.
29
+ */
30
+ maxSelect?: number;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Schema for a PocketBase collection.
36
+ */
37
+ export interface PocketBaseCollection {
38
+ /**
39
+ * Name of the collection.
40
+ */
41
+ name: string;
42
+ /**
43
+ * Schema of the collection.
44
+ */
45
+ schema: Array<PocketBaseSchemaEntry>;
46
+ }
@@ -0,0 +1,57 @@
1
+ import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
2
+ import type {
3
+ PocketBaseCollection,
4
+ PocketBaseSchemaEntry
5
+ } from "../types/pocketbase-schema.type";
6
+ import { getAdminToken } from "./get-admin-token";
7
+
8
+ /**
9
+ * Fetches the schema for the specified collection from the PocketBase instance.
10
+ *
11
+ * @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
12
+ */
13
+ export async function getRemoteSchema(
14
+ options: PocketBaseLoaderOptions
15
+ ): Promise<Array<PocketBaseSchemaEntry> | undefined> {
16
+ if (!options.adminEmail || !options.adminPassword) {
17
+ return undefined;
18
+ }
19
+
20
+ // Get the admin token
21
+ const token = await getAdminToken(
22
+ options.url,
23
+ options.adminEmail,
24
+ options.adminPassword
25
+ );
26
+
27
+ // If the token is invalid, return the basic schema
28
+ if (!token) {
29
+ return undefined;
30
+ }
31
+
32
+ // Build URL and headers for the schema request
33
+ const schemaUrl = new URL(
34
+ `api/collections/${options.collectionName}`,
35
+ options.url
36
+ ).href;
37
+ const schemaHeaders = new Headers();
38
+ schemaHeaders.set("Authorization", token);
39
+
40
+ // Fetch the schema
41
+ const schemaRequest = await fetch(schemaUrl, {
42
+ headers: schemaHeaders
43
+ });
44
+
45
+ // If the request was not successful, return the basic schema
46
+ if (!schemaRequest.ok) {
47
+ const reason = await schemaRequest.json().then((data) => data.message);
48
+ const errorMessage = `Fetching schema from ${options.collectionName} failed with status code ${schemaRequest.status}.\nReason: ${reason}`;
49
+ console.error(errorMessage);
50
+
51
+ return undefined;
52
+ }
53
+
54
+ // Get the schema from the response
55
+ const schema: PocketBaseCollection = await schemaRequest.json();
56
+ return schema.schema;
57
+ }
@@ -1,4 +1,5 @@
1
1
  import type { LoaderContext } from "astro/loaders";
2
+ import type { PocketBaseEntry } from "../types/pocketbase-base.type";
2
3
 
3
4
  /**
4
5
  * Parse an entry from PocketBase to match the schema and store it in the store.
@@ -9,7 +10,7 @@ import type { LoaderContext } from "astro/loaders";
9
10
  * If multiple fields are used, they will be concatenated and wrapped in `<section>` elements.
10
11
  */
11
12
  export async function parseEntry(
12
- entry: Record<string, any>,
13
+ entry: PocketBaseEntry,
13
14
  { generateDigest, parseData, store }: LoaderContext,
14
15
  contentFields: string | Array<string>
15
16
  ): Promise<void> {
@@ -28,7 +29,7 @@ export async function parseEntry(
28
29
  let content: string;
29
30
  if (typeof contentFields === "string") {
30
31
  // Only one field is used as content
31
- content = entry[contentFields];
32
+ content = `${entry[contentFields]}`;
32
33
  } else {
33
34
  // Multiple fields are used as content, wrap each block in a section and concatenate them
34
35
  content = contentFields
@@ -1,7 +1,8 @@
1
1
  import { z } from "astro/zod";
2
+ import type { PocketBaseSchemaEntry } from "../types/pocketbase-schema.type";
2
3
 
3
4
  export function parseSchema(
4
- schema: Array<Record<string, any>>
5
+ schema: Array<PocketBaseSchemaEntry>
5
6
  ): Record<string, z.ZodType> {
6
7
  // Prepare the schemas fields
7
8
  const fields: Record<string, z.ZodType> = {};
@@ -25,18 +26,25 @@ export function parseSchema(
25
26
  fieldType = z.date({ coerce: true });
26
27
  break;
27
28
  case "select":
29
+ if (!field.options.values) {
30
+ throw new Error(
31
+ `Field ${field.name} is of type "select" but has no values defined.`
32
+ );
33
+ }
34
+
28
35
  // Create an enum for the select values
36
+ // @ts-expect-error - Zod complains because the values are not known at compile time and thus the array is not static.
29
37
  const values = z.enum(field.options.values);
30
38
 
31
39
  // Parse the field type based on the number of values it can have
32
- fieldType = parseSingleOrMultipleValues(field as any, values);
40
+ fieldType = parseSingleOrMultipleValues(field, values);
33
41
  break;
34
42
  case "relation":
35
43
  case "file":
36
44
  // NOTE: Relations and files are currently not supported and are treated as strings
37
45
 
38
46
  // Parse the field type based on the number of values it can have
39
- fieldType = parseSingleOrMultipleValues(field as any, z.string());
47
+ fieldType = parseSingleOrMultipleValues(field, z.string());
40
48
  break;
41
49
  case "json":
42
50
  // Parse the field as an object
@@ -69,11 +77,11 @@ export function parseSchema(
69
77
  * @returns The parsed field type
70
78
  */
71
79
  function parseSingleOrMultipleValues(
72
- field: { options: { maxSelect: number } },
80
+ field: PocketBaseSchemaEntry,
73
81
  type: z.ZodType
74
82
  ) {
75
83
  // If the select allows multiple values, create an array of the enum
76
- if (field.options.maxSelect === 1) {
84
+ if (field.options.maxSelect === undefined || field.options.maxSelect === 1) {
77
85
  return type;
78
86
  } else {
79
87
  return z.array(type);
@@ -0,0 +1,48 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import type {
4
+ PocketBaseCollection,
5
+ PocketBaseSchemaEntry
6
+ } from "../types/pocketbase-schema.type";
7
+
8
+ /**
9
+ * Reads the local PocketBase schema file and returns the schema for the specified collection.
10
+ *
11
+ * @param localSchemaPath Path to the local schema file.
12
+ * @param collectionName Name of the collection to get the schema for.
13
+ */
14
+ export async function readLocalSchema(
15
+ localSchemaPath: string,
16
+ collectionName: string
17
+ ): Promise<Array<PocketBaseSchemaEntry> | undefined> {
18
+ const realPath = path.join(process.cwd(), localSchemaPath);
19
+
20
+ try {
21
+ // Read the schema file
22
+ const schemaFile = await fs.readFile(realPath, "utf-8");
23
+ const database: Array<PocketBaseCollection> = JSON.parse(schemaFile);
24
+
25
+ // Check if the database file is valid
26
+ if (!database || !Array.isArray(database)) {
27
+ throw new Error("Invalid schema file");
28
+ }
29
+
30
+ // Find and return the schema for the collection
31
+ const schema = database.find(
32
+ (collection) => collection.name === collectionName
33
+ );
34
+
35
+ if (!schema) {
36
+ throw new Error(
37
+ `Collection "${collectionName}" not found in schema file`
38
+ );
39
+ }
40
+
41
+ return schema.schema;
42
+ } catch (error) {
43
+ console.error(
44
+ `Failed to read local schema from ${localSchemaPath}. No types will be generated.\nReason: ${error}`
45
+ );
46
+ return undefined;
47
+ }
48
+ }