astro-loader-pocketbase 0.4.0 → 0.5.0-custom-id-rc.2

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
@@ -62,6 +62,25 @@ While the API only returns the filenames of these images and files, the loader w
62
62
  This doesn't mean that the files are downloaded during the build process.
63
63
  But you can directly use these URLs in your Astro components to display images or link to the files.
64
64
 
65
+ ### Custom ids
66
+
67
+ By default, the loader will use the `id` field of the collection as the unique identifier.
68
+ If you want to use another field as the id, e.g. a slug of the title, you can specify this field via the `id` option.
69
+
70
+ ```ts
71
+ const blog = defineCollection({
72
+ loader: pocketbaseLoader({
73
+ ...options,
74
+ id: "<field-in-collection>"
75
+ })
76
+ });
77
+ ```
78
+
79
+ Please note that the id should be unique for every entry in the collection.
80
+ The loader will also automatically convert the value into a slug to be easily used in URLs.
81
+ It's recommended to use e.g. the title of the entry to be easily searchable and readable.
82
+ **Do not use e.g. rich text fields as ids.**
83
+
65
84
  ## Type generation
66
85
 
67
86
  The loader can automatically generate types for your collection.
@@ -116,6 +135,7 @@ This manual schema will **always override the automatic type generation**.
116
135
  | `content` | `string \| Array<string>` | | The field in the collection to use as content. This can also be an array of fields. |
117
136
  | `adminEmail` | `string` | | The email of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
118
137
  | `adminPassword` | `string` | | The password of the admin of the PocketBase instance. This is used for automatic type generation and access to private collections. |
138
+ | `id` | `string` | | The field in the collection to use as unique id. Defaults to `id`. |
119
139
  | `localSchema` | `string` | | The path to a local schema file. This is used for automatic type generation. |
120
140
  | `jsonSchemas` | `Record<string, z.ZodSchema>` | | A record of Zod schemas to use for type generation of `json` fields. |
121
141
  | `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,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-loader-pocketbase",
3
- "version": "0.4.0",
3
+ "version": "0.5.0-custom-id-rc.2",
4
4
  "description": "A content loader for Astro that uses the PocketBase API",
5
5
  "license": "MIT",
6
6
  "author": "Luis Wolf <development@pawcode.de> (https://pawcode.de)",
@@ -24,7 +24,7 @@
24
24
  "@eslint/js": "^9.11.1",
25
25
  "@stylistic/eslint-plugin": "^2.8.0",
26
26
  "@types/node": "^22.7.4",
27
- "astro": "^5.0.0-beta.2",
27
+ "astro": "^5.0.0-beta.6",
28
28
  "eslint": "^9.11.1",
29
29
  "globals": "^15.9.0",
30
30
  "husky": "^9.1.6",
@@ -74,7 +74,7 @@ export async function cleanupEntries(
74
74
  let cleanedUp = 0;
75
75
 
76
76
  // Get all ids of the entries in the store
77
- const storedIds = context.store.keys();
77
+ const storedIds = context.store.values().map((entry) => entry.data.id) as Array<string>;
78
78
  for (const id of storedIds) {
79
79
  // If the id is not in the entries set, remove the entry from the store
80
80
  if (!entries.has(id)) {
@@ -11,7 +11,7 @@ import { transformFiles } from "./utils/transform-files";
11
11
  * Basic schema for every PocketBase collection.
12
12
  */
13
13
  const BASIC_SCHEMA = {
14
- id: z.string().length(15),
14
+ id: z.string(),
15
15
  collectionId: z.string().length(15),
16
16
  collectionName: z.string(),
17
17
  created: z.coerce.date(),
@@ -29,6 +29,11 @@ const VIEW_SCHEMA = {
29
29
  updated: z.preprocess((val) => val || undefined, z.optional(z.coerce.date()))
30
30
  };
31
31
 
32
+ /**
33
+ * Types of fields that can be used as an ID.
34
+ */
35
+ const VALID_ID_TYPES = ["text", "number", "email", "url", "date"];
36
+
32
37
  /**
33
38
  * Generate a schema for the collection based on the collection's schema in PocketBase.
34
39
  * By default, a basic schema is returned if no other schema is available.
@@ -65,6 +70,31 @@ export async function generateSchema(
65
70
  // Parse the schema
66
71
  const fields = parseSchema(collection, options.jsonSchemas);
67
72
 
73
+ // Check if custom id field is present
74
+ if (options.id) {
75
+ // Find the id field in the schema
76
+ const idField = collection.schema.find(
77
+ (field) => field.name === options.id
78
+ );
79
+
80
+ // Check if the id field is present and of a valid type
81
+ if (!idField) {
82
+ console.error(
83
+ `The id field "${options.id}" is not present in the schema of the collection "${options.collectionName}".`
84
+ );
85
+ } else if (!VALID_ID_TYPES.includes(idField.type)) {
86
+ console.error(
87
+ `The id field "${options.id}" for collection "${
88
+ options.collectionName
89
+ }" is of type "${
90
+ idField.type
91
+ }" which is not recommended. Please use one of the following types: ${VALID_ID_TYPES.join(
92
+ ", "
93
+ )}.`
94
+ );
95
+ }
96
+ }
97
+
68
98
  // Check if the content field is present
69
99
  if (typeof options.content === "string" && !fields[options.content]) {
70
100
  console.error(
@@ -82,7 +82,7 @@ export async function loadEntries(
82
82
 
83
83
  // Parse and store the entries
84
84
  for (const entry of response.items) {
85
- await parseEntry(entry, context, options.content);
85
+ await parseEntry(entry, context, options.id, options.content);
86
86
 
87
87
  // Check if the entry has an `updated` column
88
88
  // This is used to enable the incremental fetching of entries
@@ -12,6 +12,15 @@ export interface PocketBaseLoaderOptions {
12
12
  * Name of the collection in PocketBase.
13
13
  */
14
14
  collectionName: string;
15
+ /**
16
+ * Field that should be used as the unique identifier for the collection.
17
+ * This must be the name of a field in the collection that contains unique values.
18
+ * If not provided, the `id` field will be used.
19
+ * The value of this field will be used in `getEntry` and `getEntries` to load the entry or entries.
20
+ *
21
+ * If the field is a string, it will be slugified to be used in the URL.
22
+ */
23
+ id?: string;
15
24
  /**
16
25
  * Content of the collection in PocketBase.
17
26
  * This must be the name of a field in the collection that contains the content.
@@ -1,23 +1,48 @@
1
1
  import type { LoaderContext } from "astro/loaders";
2
2
  import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
3
+ import { slugify } from "./slugify";
3
4
 
4
5
  /**
5
6
  * Parse an entry from PocketBase to match the schema and store it in the store.
6
7
  *
7
8
  * @param entry Entry to parse.
8
9
  * @param context Context of the loader.
10
+ * @param idField Field to use as id for the entry.
11
+ * If not provided, the id of the entry will be used.
9
12
  * @param contentFields Field(s) to use as content for the entry.
10
13
  * If multiple fields are used, they will be concatenated and wrapped in `<section>` elements.
11
14
  */
12
15
  export async function parseEntry(
13
16
  entry: PocketBaseEntry,
14
- { generateDigest, parseData, store }: LoaderContext,
17
+ { generateDigest, parseData, store, logger }: LoaderContext,
18
+ idField?: string,
15
19
  contentFields?: string | Array<string>
16
20
  ): Promise<void> {
21
+ let id = entry.id;
22
+ if (idField) {
23
+ // Get the custom ID of the entry if it exists
24
+ const customEntryId = entry[idField];
25
+
26
+ if (!customEntryId) {
27
+ logger.warn(
28
+ `The entry "${id}" does not have a value for field ${idField}. Using the default ID instead.`
29
+ );
30
+ } else {
31
+ id = slugify(`${customEntryId}`);
32
+ }
33
+ }
34
+
35
+ const oldEntry = store.get(id);
36
+ if (oldEntry && oldEntry.data.id !== entry.id) {
37
+ logger.warn(
38
+ `The entry "${entry.id}" seems to be a duplicate of "${oldEntry.data.id}". Please make sure to use unique IDs in the column "${idField}".`
39
+ );
40
+ }
41
+
17
42
  // Parse the data to match the schema
18
43
  // This will throw an error if the data does not match the schema
19
44
  const data = await parseData({
20
- id: entry.id,
45
+ id,
21
46
  data: entry
22
47
  });
23
48
 
@@ -30,7 +55,7 @@ export async function parseEntry(
30
55
  if (!contentFields) {
31
56
  // Store the entry
32
57
  store.set({
33
- id: entry.id,
58
+ id,
34
59
  data,
35
60
  digest
36
61
  });
@@ -52,7 +77,7 @@ export async function parseEntry(
52
77
 
53
78
  // Store the entry
54
79
  store.set({
55
- id: entry.id,
80
+ id,
56
81
  data,
57
82
  digest,
58
83
  rendered: {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Convert a string to a slog.
3
+ *
4
+ * Example:
5
+ * ```ts
6
+ * slugify("Hello World!"); // hello-world
7
+ * ```
8
+ */
9
+ export function slugify(input: string): string {
10
+ return input
11
+ .toString()
12
+ .toLowerCase()
13
+ .replace(/\s+/g, "-") // Replace spaces with -
14
+ .replace(/ä/g, "ae") // Replace umlauts
15
+ .replace(/ö/g, "oe")
16
+ .replace(/ü/g, "ue")
17
+ .replace(/ß/g, "ss")
18
+ .replace(/[^\w-]+/g, "") // Remove all non-word chars
19
+ .replace(/--+/g, "-") // Replace multiple - with single -
20
+ .replace(/^-+/, "") // Trim - from start of text
21
+ .replace(/-+$/, ""); // Trim - from end of text
22
+ }