astro-loader-pocketbase 0.2.1 → 0.3.0-rc.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/package.json +1 -1
- package/src/generate-schema.ts +33 -12
- package/src/load-entries.ts +19 -7
- package/src/pocketbase-loader.ts +22 -1
- package/src/types/{pocketbase-base.type.ts → pocketbase-entry.type.ts} +2 -2
- package/src/types/pocketbase-schema.type.ts +4 -0
- package/src/utils/get-remote-schema.ts +3 -7
- package/src/utils/parse-entry.ts +5 -3
- package/src/utils/parse-schema.ts +13 -7
- package/src/utils/read-local-schema.ts +3 -6
package/package.json
CHANGED
package/src/generate-schema.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 type {
|
|
4
|
+
import type { PocketBaseCollection } from "./types/pocketbase-schema.type";
|
|
5
5
|
import { getRemoteSchema } from "./utils/get-remote-schema";
|
|
6
6
|
import { parseSchema } from "./utils/parse-schema";
|
|
7
7
|
import { readLocalSchema } from "./utils/read-local-schema";
|
|
@@ -13,39 +13,56 @@ const BASIC_SCHEMA = {
|
|
|
13
13
|
id: z.string().length(15),
|
|
14
14
|
collectionId: z.string().length(15),
|
|
15
15
|
collectionName: z.string(),
|
|
16
|
-
created: z.date(
|
|
17
|
-
updated: z.date(
|
|
16
|
+
created: z.coerce.date(),
|
|
17
|
+
updated: z.coerce.date()
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Basic schema for a view in PocketBase.
|
|
22
|
+
*/
|
|
23
|
+
const VIEW_SCHEMA = {
|
|
24
|
+
id: z.string(),
|
|
25
|
+
collectionId: z.string().length(15),
|
|
26
|
+
collectionName: z.string(),
|
|
27
|
+
created: z.preprocess((val) => val || undefined, z.optional(z.coerce.date())),
|
|
28
|
+
updated: z.preprocess((val) => val || undefined, z.optional(z.coerce.date()))
|
|
18
29
|
};
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
32
|
* Generate a schema for the collection based on the collection's schema in PocketBase.
|
|
22
|
-
*
|
|
33
|
+
* By default, a basic schema is returned if no other schema is available.
|
|
34
|
+
* If admin credentials are provided, the schema is fetched from the PocketBase API.
|
|
35
|
+
* If a path to a local schema file is provided, the schema is read from the file.
|
|
23
36
|
*
|
|
24
37
|
* @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
|
|
25
38
|
*/
|
|
26
39
|
export async function generateSchema(
|
|
27
40
|
options: PocketBaseLoaderOptions
|
|
28
41
|
): Promise<ZodSchema> {
|
|
29
|
-
let
|
|
42
|
+
let collection: PocketBaseCollection | undefined;
|
|
30
43
|
|
|
31
44
|
// Try to get the schema directly from the PocketBase instance
|
|
32
|
-
|
|
45
|
+
collection = await getRemoteSchema(options);
|
|
33
46
|
|
|
34
47
|
// If the schema is not available, try to read it from a local schema file
|
|
35
|
-
if (!
|
|
36
|
-
|
|
48
|
+
if (!collection && options.localSchema) {
|
|
49
|
+
collection = await readLocalSchema(
|
|
50
|
+
options.localSchema,
|
|
51
|
+
options.collectionName
|
|
52
|
+
);
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
// If the schema is still not available, return the basic schema
|
|
40
|
-
if (!
|
|
56
|
+
if (!collection) {
|
|
41
57
|
console.error(
|
|
42
58
|
`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
59
|
);
|
|
44
|
-
|
|
60
|
+
// Return the view schema since every collection has at least the view schema
|
|
61
|
+
return z.object(VIEW_SCHEMA);
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
// Parse the schema
|
|
48
|
-
const fields = parseSchema(
|
|
65
|
+
const fields = parseSchema(collection);
|
|
49
66
|
|
|
50
67
|
// Check if the content field is present
|
|
51
68
|
if (typeof options.content === "string" && !fields[options.content]) {
|
|
@@ -62,9 +79,13 @@ export async function generateSchema(
|
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
// Use the corresponding base schema for the type of collection
|
|
83
|
+
// Auth collections are basically a superset of the basic schema.
|
|
84
|
+
const base = collection.type === "view" ? VIEW_SCHEMA : BASIC_SCHEMA;
|
|
85
|
+
|
|
65
86
|
// Combine the basic schema with the parsed fields
|
|
66
87
|
return z.object({
|
|
67
|
-
...
|
|
88
|
+
...base,
|
|
68
89
|
...fields
|
|
69
90
|
});
|
|
70
91
|
}
|
package/src/load-entries.ts
CHANGED
|
@@ -9,13 +9,15 @@ import { parseEntry } from "./utils/parse-entry";
|
|
|
9
9
|
* @param context Context of the loader.
|
|
10
10
|
* @param adminToken Admin token to access all resources.
|
|
11
11
|
* @param lastModified Date of the last fetch to only update changed entries.
|
|
12
|
+
*
|
|
13
|
+
* @returns `true` if the collection has an updated column, `false` otherwise.
|
|
12
14
|
*/
|
|
13
15
|
export async function loadEntries(
|
|
14
16
|
options: PocketBaseLoaderOptions,
|
|
15
17
|
context: LoaderContext,
|
|
16
18
|
adminToken: string | undefined,
|
|
17
19
|
lastModified: string | undefined
|
|
18
|
-
): Promise<
|
|
20
|
+
): Promise<boolean> {
|
|
19
21
|
// Build the URL for the collections endpoint
|
|
20
22
|
const collectionUrl = new URL(
|
|
21
23
|
`api/collections/${options.collectionName}/records`,
|
|
@@ -41,14 +43,17 @@ export async function loadEntries(
|
|
|
41
43
|
let page = 1;
|
|
42
44
|
let totalPages = 0;
|
|
43
45
|
let entries = 0;
|
|
46
|
+
let hasUpdatedColumn = false;
|
|
44
47
|
|
|
45
48
|
// Fetch all (modified) entries
|
|
46
49
|
do {
|
|
47
50
|
// Fetch entries from the collection
|
|
48
51
|
// If `lastModified` is set, only fetch entries that have been modified since the last fetch
|
|
49
52
|
const collectionRequest = await fetch(
|
|
50
|
-
`${collectionUrl}?page=${page}&perPage=100
|
|
51
|
-
lastModified
|
|
53
|
+
`${collectionUrl}?page=${page}&perPage=100${
|
|
54
|
+
lastModified
|
|
55
|
+
? `&sort=-updated,id&filter=(updated>"${lastModified}")`
|
|
56
|
+
: ""
|
|
52
57
|
}`,
|
|
53
58
|
{
|
|
54
59
|
headers: collectionHeaders
|
|
@@ -59,10 +64,9 @@ export async function loadEntries(
|
|
|
59
64
|
if (!collectionRequest.ok) {
|
|
60
65
|
// If the collection is locked, an admin token is required
|
|
61
66
|
if (collectionRequest.status === 403) {
|
|
62
|
-
|
|
67
|
+
throw new Error(
|
|
63
68
|
`The collection ${options.collectionName} is not accessible without an admin rights. Please provide an admin email and password in the config.`
|
|
64
69
|
);
|
|
65
|
-
return;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
// Get the reason for the error
|
|
@@ -70,8 +74,7 @@ export async function loadEntries(
|
|
|
70
74
|
.json()
|
|
71
75
|
.then((data) => data.message);
|
|
72
76
|
const errorMessage = `Fetching data from ${options.collectionName} failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
|
|
73
|
-
|
|
74
|
-
return;
|
|
77
|
+
throw new Error(errorMessage);
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
// Get the data from the response
|
|
@@ -80,6 +83,12 @@ export async function loadEntries(
|
|
|
80
83
|
// Parse and store the entries
|
|
81
84
|
for (const entry of response.items) {
|
|
82
85
|
await parseEntry(entry, context, options.content);
|
|
86
|
+
|
|
87
|
+
// Check if the entry has an `updated` column
|
|
88
|
+
// This is used to enable the incremental fetching of entries
|
|
89
|
+
if (!hasUpdatedColumn && "updated" in entry) {
|
|
90
|
+
hasUpdatedColumn = true;
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
// Update the page and total pages
|
|
@@ -94,4 +103,7 @@ export async function loadEntries(
|
|
|
94
103
|
context.collection
|
|
95
104
|
}`
|
|
96
105
|
);
|
|
106
|
+
|
|
107
|
+
// Return if the collection has an updated column
|
|
108
|
+
return hasUpdatedColumn;
|
|
97
109
|
}
|
package/src/pocketbase-loader.ts
CHANGED
|
@@ -20,6 +20,9 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
|
|
|
20
20
|
? undefined
|
|
21
21
|
: context.meta.get("last-modified");
|
|
22
22
|
|
|
23
|
+
// Get the `has-updated-column` meta to check if the collection has an updated column
|
|
24
|
+
let hasUpdatedColumn = context.meta.get("has-updated-column") === "true";
|
|
25
|
+
|
|
23
26
|
// Clear the store if we want to fetch all entries again
|
|
24
27
|
if (options.forceUpdate) {
|
|
25
28
|
context.store.clear();
|
|
@@ -42,7 +45,25 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
// Load the (modified) entries
|
|
45
|
-
|
|
48
|
+
try {
|
|
49
|
+
hasUpdatedColumn = await loadEntries(
|
|
50
|
+
options,
|
|
51
|
+
context,
|
|
52
|
+
token,
|
|
53
|
+
// Only fetch entries that have been modified since the last fetch
|
|
54
|
+
// If the collection does not have an updated column, all entries will be fetched
|
|
55
|
+
hasUpdatedColumn ? lastModified : undefined
|
|
56
|
+
);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Set the `has-updated-column` meta to `false` if an error occurred
|
|
59
|
+
// This will force the loader to fetch all entries again in the next run
|
|
60
|
+
context.meta.set("has-updated-column", `${false}`);
|
|
61
|
+
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Set the `has-updated-column` meta to `true` if the collection has an updated column
|
|
66
|
+
context.meta.set("has-updated-column", `${hasUpdatedColumn}`);
|
|
46
67
|
|
|
47
68
|
// Set the last modified date to the current date
|
|
48
69
|
context.meta.set("last-modified", new Date().toISOString());
|
|
@@ -17,11 +17,11 @@ interface PocketBaseBaseEntry {
|
|
|
17
17
|
/**
|
|
18
18
|
* Date the entry was created.
|
|
19
19
|
*/
|
|
20
|
-
created
|
|
20
|
+
created?: string | undefined;
|
|
21
21
|
/**
|
|
22
22
|
* Date the entry was last updated.
|
|
23
23
|
*/
|
|
24
|
-
updated
|
|
24
|
+
updated?: string | undefined;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
|
|
2
|
-
import type {
|
|
3
|
-
PocketBaseCollection,
|
|
4
|
-
PocketBaseSchemaEntry
|
|
5
|
-
} from "../types/pocketbase-schema.type";
|
|
2
|
+
import type { PocketBaseCollection } from "../types/pocketbase-schema.type";
|
|
6
3
|
import { getAdminToken } from "./get-admin-token";
|
|
7
4
|
|
|
8
5
|
/**
|
|
@@ -12,7 +9,7 @@ import { getAdminToken } from "./get-admin-token";
|
|
|
12
9
|
*/
|
|
13
10
|
export async function getRemoteSchema(
|
|
14
11
|
options: PocketBaseLoaderOptions
|
|
15
|
-
): Promise<
|
|
12
|
+
): Promise<PocketBaseCollection | undefined> {
|
|
16
13
|
if (!options.adminEmail || !options.adminPassword) {
|
|
17
14
|
return undefined;
|
|
18
15
|
}
|
|
@@ -52,6 +49,5 @@ export async function getRemoteSchema(
|
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
// Get the schema from the response
|
|
55
|
-
|
|
56
|
-
return schema.schema;
|
|
52
|
+
return await schemaRequest.json();
|
|
57
53
|
}
|
package/src/utils/parse-entry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LoaderContext } from "astro/loaders";
|
|
2
|
-
import type { PocketBaseEntry } from "../types/pocketbase-
|
|
2
|
+
import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Parse an entry from PocketBase to match the schema and store it in the store.
|
|
@@ -22,8 +22,10 @@ export async function parseEntry(
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
// Generate a digest for the entry
|
|
25
|
-
//
|
|
26
|
-
|
|
25
|
+
// Normal collections use the updated date that is always updated when the entry is updated.
|
|
26
|
+
// If the entry was never updated, the created date can be used as a fallback.
|
|
27
|
+
// View collections don't necessarily publish the updated date, so the whole entry is used for the digest.
|
|
28
|
+
const digest = generateDigest(entry.updated ?? entry.created ?? entry);
|
|
27
29
|
|
|
28
30
|
// Generate the content for the entry
|
|
29
31
|
let content: string;
|
|
@@ -1,29 +1,32 @@
|
|
|
1
1
|
import { z } from "astro/zod";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
PocketBaseCollection,
|
|
4
|
+
PocketBaseSchemaEntry
|
|
5
|
+
} from "../types/pocketbase-schema.type";
|
|
3
6
|
|
|
4
7
|
export function parseSchema(
|
|
5
|
-
|
|
8
|
+
collection: PocketBaseCollection
|
|
6
9
|
): Record<string, z.ZodType> {
|
|
7
10
|
// Prepare the schemas fields
|
|
8
11
|
const fields: Record<string, z.ZodType> = {};
|
|
9
12
|
|
|
10
13
|
// Parse every field in the schema
|
|
11
|
-
for (const field of schema) {
|
|
14
|
+
for (const field of collection.schema) {
|
|
12
15
|
let fieldType;
|
|
13
16
|
|
|
14
17
|
// Determine the field type and create the corresponding Zod type
|
|
15
18
|
switch (field.type) {
|
|
16
19
|
case "number":
|
|
17
20
|
// Coerce the value to a number
|
|
18
|
-
fieldType = z.number(
|
|
21
|
+
fieldType = z.coerce.number();
|
|
19
22
|
break;
|
|
20
23
|
case "bool":
|
|
21
24
|
// Coerce the value to a boolean
|
|
22
|
-
fieldType = z.boolean(
|
|
25
|
+
fieldType = z.coerce.boolean();
|
|
23
26
|
break;
|
|
24
27
|
case "date":
|
|
25
28
|
// Coerce and parse the value as a date
|
|
26
|
-
fieldType = z.date(
|
|
29
|
+
fieldType = z.coerce.date();
|
|
27
30
|
break;
|
|
28
31
|
case "select":
|
|
29
32
|
if (!field.options.values) {
|
|
@@ -58,7 +61,10 @@ export function parseSchema(
|
|
|
58
61
|
|
|
59
62
|
// If the field is not required, mark it as optional
|
|
60
63
|
if (!field.required) {
|
|
61
|
-
fieldType.
|
|
64
|
+
fieldType = z.preprocess(
|
|
65
|
+
(val) => val || undefined,
|
|
66
|
+
z.optional(fieldType)
|
|
67
|
+
);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
// Add the field to the fields object
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import type {
|
|
4
|
-
PocketBaseCollection,
|
|
5
|
-
PocketBaseSchemaEntry
|
|
6
|
-
} from "../types/pocketbase-schema.type";
|
|
3
|
+
import type { PocketBaseCollection } from "../types/pocketbase-schema.type";
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Reads the local PocketBase schema file and returns the schema for the specified collection.
|
|
@@ -14,7 +11,7 @@ import type {
|
|
|
14
11
|
export async function readLocalSchema(
|
|
15
12
|
localSchemaPath: string,
|
|
16
13
|
collectionName: string
|
|
17
|
-
): Promise<
|
|
14
|
+
): Promise<PocketBaseCollection | undefined> {
|
|
18
15
|
const realPath = path.join(process.cwd(), localSchemaPath);
|
|
19
16
|
|
|
20
17
|
try {
|
|
@@ -38,7 +35,7 @@ export async function readLocalSchema(
|
|
|
38
35
|
);
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
return schema
|
|
38
|
+
return schema;
|
|
42
39
|
} catch (error) {
|
|
43
40
|
console.error(
|
|
44
41
|
`Failed to read local schema from ${localSchemaPath}. No types will be generated.\nReason: ${error}`
|