astro-loader-pocketbase 3.1.0 → 3.1.2-next.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.
Files changed (38) hide show
  1. package/dist/index.d.mts +276 -0
  2. package/dist/index.d.mts.map +1 -0
  3. package/dist/index.mjs +973 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/package.json +24 -15
  6. package/src/index.ts +0 -24
  7. package/src/loader/cleanup-entries.ts +0 -137
  8. package/src/loader/fetch-collection.ts +0 -177
  9. package/src/loader/fetch-entry.ts +0 -83
  10. package/src/loader/handle-realtime-updates.ts +0 -56
  11. package/src/loader/live-collection-loader.ts +0 -51
  12. package/src/loader/live-entry-loader.ts +0 -38
  13. package/src/loader/load-entries.ts +0 -52
  14. package/src/loader/loader.ts +0 -77
  15. package/src/loader/parse-entry.ts +0 -90
  16. package/src/loader/parse-live-entry.ts +0 -66
  17. package/src/pocketbase-loader.ts +0 -98
  18. package/src/schema/generate-schema.ts +0 -200
  19. package/src/schema/generate-type.ts +0 -23
  20. package/src/schema/get-remote-schema.ts +0 -43
  21. package/src/schema/parse-schema.ts +0 -170
  22. package/src/schema/read-local-schema.ts +0 -46
  23. package/src/schema/transform-files.ts +0 -67
  24. package/src/tsconfig.json +0 -7
  25. package/src/types/errors.ts +0 -19
  26. package/src/types/pocketbase-api-response.type.ts +0 -40
  27. package/src/types/pocketbase-entry.type.ts +0 -24
  28. package/src/types/pocketbase-live-loader-filter.type.ts +0 -55
  29. package/src/types/pocketbase-loader-options.type.ts +0 -146
  30. package/src/types/pocketbase-schema.type.ts +0 -101
  31. package/src/utils/combine-fields-for-request.ts +0 -55
  32. package/src/utils/create-token-promise.ts +0 -25
  33. package/src/utils/extract-field-names.ts +0 -15
  34. package/src/utils/format-fields.ts +0 -66
  35. package/src/utils/get-superuser-token.ts +0 -76
  36. package/src/utils/is-realtime-data.ts +0 -34
  37. package/src/utils/should-refresh.ts +0 -37
  38. package/src/utils/slugify.ts +0 -21
@@ -1,83 +0,0 @@
1
- import {
2
- LiveCollectionError,
3
- LiveEntryNotFoundError
4
- } from "astro/content/runtime";
5
- import { PocketBaseAuthenticationError } from "../types/errors";
6
- import { pocketBaseErrorResponse } from "../types/pocketbase-api-response.type";
7
- import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
8
- import { pocketBaseEntry } from "../types/pocketbase-entry.type";
9
- import type { PocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
10
- import { combineFieldsForRequest } from "../utils/combine-fields-for-request";
11
- import { formatFields } from "../utils/format-fields";
12
-
13
- /**
14
- * Retrieves a specific entry from a PocketBase collection using its ID and loader options.
15
- */
16
- export async function fetchEntry<TEntry extends PocketBaseEntry>(
17
- id: string,
18
- options: PocketBaseLiveLoaderOptions,
19
- token: string | undefined
20
- ): Promise<TEntry> {
21
- // Build the URL for the entry endpoint
22
- const entryUrl = new URL(
23
- `api/collections/${options.collectionName}/records/${id}`,
24
- options.url
25
- );
26
-
27
- // Add fields parameter if specified
28
- const fieldsArray = formatFields(options.fields);
29
- const combinedFields = combineFieldsForRequest(fieldsArray, options);
30
- if (combinedFields) {
31
- entryUrl.searchParams.set("fields", combinedFields.join(","));
32
- }
33
-
34
- // Create the headers for the request to append the token (if available)
35
- const entryHeaders = new Headers();
36
- if (token) {
37
- entryHeaders.set("Authorization", token);
38
- }
39
-
40
- // Fetch the entry from the collection
41
- const entryRequest = await fetch(entryUrl.href, {
42
- headers: entryHeaders
43
- });
44
-
45
- // If the request was not successful, return an error
46
- if (!entryRequest.ok) {
47
- // If the entry is locked, a superuser token is required
48
- if (entryRequest.status === 403) {
49
- if (
50
- options.superuserCredentials &&
51
- "impersonateToken" in options.superuserCredentials
52
- ) {
53
- throw new PocketBaseAuthenticationError(
54
- options.collectionName,
55
- "The given impersonate token is not valid."
56
- );
57
- } else {
58
- throw new PocketBaseAuthenticationError(
59
- options.collectionName,
60
- "The entry is not accessible without superuser rights. Please provide superuser credentials in the config."
61
- );
62
- }
63
- }
64
-
65
- if (entryRequest.status === 404) {
66
- throw new LiveEntryNotFoundError(options.collectionName, id);
67
- }
68
-
69
- // Get the reason for the error
70
- const errorResponse = pocketBaseErrorResponse.parse(
71
- await entryRequest.json()
72
- );
73
- throw new LiveCollectionError(
74
- options.collectionName,
75
- errorResponse.message
76
- );
77
- }
78
-
79
- // Get the data from the response
80
- const response = pocketBaseEntry.parse(await entryRequest.json());
81
- // oxlint-disable-next-line no-unsafe-type-assertion
82
- return response as TEntry;
83
- }
@@ -1,56 +0,0 @@
1
- import type { LoaderContext } from "astro/loaders";
2
- import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
3
- import { isRealtimeData } from "../utils/is-realtime-data";
4
- import { parseEntry } from "./parse-entry";
5
-
6
- /**
7
- * Handles realtime updates for the loader without making any new network requests.
8
- *
9
- * Returns `true` if the data was handled and no further action is needed.
10
- */
11
- export async function handleRealtimeUpdates(
12
- context: LoaderContext,
13
- options: PocketBaseLoaderOptions
14
- ): Promise<boolean> {
15
- // Check if a custom filter is set
16
- if (options.filter) {
17
- // Updating an entry directly via realtime updates is not supported when using a custom filter.
18
- // This is because the filter can only be applied via the get request and is not considered in the realtime updates.
19
- // Updating the entry directly would bypass the filter and could lead to inconsistent data.
20
- return false;
21
- }
22
-
23
- // Check if data was provided via the refresh context
24
- if (!context.refreshContextData?.data) {
25
- return false;
26
- }
27
-
28
- // Check if the data is PocketBase realtime data
29
- const data = context.refreshContextData.data;
30
- if (!isRealtimeData(data)) {
31
- return false;
32
- }
33
-
34
- // Check if the collection name matches the current collection
35
- if (data.record.collectionName !== options.collectionName) {
36
- return false;
37
- }
38
-
39
- // Handle deleted entry
40
- if (data.action === "delete") {
41
- context.logger.info("Removing deleted entry");
42
- context.store.delete(data.record.id);
43
- return true;
44
- }
45
-
46
- // Handle updated or new entry
47
- if (data.action === "update") {
48
- context.logger.info("Updating outdated entry");
49
- } else {
50
- context.logger.info("Creating new entry");
51
- }
52
-
53
- // Parse the entry and store
54
- await parseEntry(data.record, context, options);
55
- return true;
56
- }
@@ -1,51 +0,0 @@
1
- import type { LiveDataCollection, LiveDataEntry } from "astro";
2
- import { LiveCollectionError } from "astro/content/runtime";
3
- import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
4
- import type { PocketBaseLiveLoaderCollectionFilter } from "../types/pocketbase-live-loader-filter.type";
5
- import type { PocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
6
- import { fetchCollection } from "./fetch-collection";
7
- import { parseLiveEntry } from "./parse-live-entry";
8
-
9
- /**
10
- * Loads and parses a PocketBase collection for live data, returning entries or an error.
11
- */
12
- export async function liveCollectionLoader<TEntry extends PocketBaseEntry>(
13
- collectionFilter: PocketBaseLiveLoaderCollectionFilter | undefined,
14
- options: PocketBaseLiveLoaderOptions,
15
- token: string | undefined
16
- ): Promise<LiveDataCollection<TEntry> | { error: LiveCollectionError }> {
17
- const entries: Array<LiveDataEntry<TEntry>> = [];
18
-
19
- try {
20
- await fetchCollection<TEntry>(
21
- options,
22
- async (chunk) => {
23
- entries.push(...chunk.map((entry) => parseLiveEntry(entry, options)));
24
- },
25
- token,
26
- collectionFilter
27
- );
28
- } catch (error) {
29
- if (error instanceof LiveCollectionError) {
30
- return { error };
31
- }
32
-
33
- if (error instanceof Error) {
34
- return {
35
- error: new LiveCollectionError(
36
- options.collectionName,
37
- error.message,
38
- error
39
- )
40
- };
41
- }
42
-
43
- return {
44
- error: new LiveCollectionError(options.collectionName, String(error))
45
- };
46
- }
47
-
48
- return {
49
- entries
50
- };
51
- }
@@ -1,38 +0,0 @@
1
- import type { LiveDataEntry } from "astro";
2
- import { LiveCollectionError } from "astro/content/runtime";
3
- import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
4
- import type { PocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
5
- import { fetchEntry } from "./fetch-entry";
6
- import { parseLiveEntry } from "./parse-live-entry";
7
-
8
- /**
9
- * Loads and parses a single PocketBase entry for live data, returning the entry or an error.
10
- */
11
- export async function liveEntryLoader<TEntry extends PocketBaseEntry>(
12
- id: string,
13
- options: PocketBaseLiveLoaderOptions,
14
- token: string | undefined
15
- ): Promise<LiveDataEntry<TEntry> | { error: LiveCollectionError }> {
16
- try {
17
- const entry = await fetchEntry<TEntry>(id, options, token);
18
- return parseLiveEntry(entry, options);
19
- } catch (error) {
20
- if (error instanceof LiveCollectionError) {
21
- return { error };
22
- }
23
-
24
- if (error instanceof Error) {
25
- return {
26
- error: new LiveCollectionError(
27
- options.collectionName,
28
- error.message,
29
- error
30
- )
31
- };
32
- }
33
-
34
- return {
35
- error: new LiveCollectionError(options.collectionName, String(error))
36
- };
37
- }
38
- }
@@ -1,52 +0,0 @@
1
- import type { LoaderContext } from "astro/loaders";
2
- import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
3
- import { fetchCollection } from "./fetch-collection";
4
- import { parseEntry } from "./parse-entry";
5
-
6
- /**
7
- * Load (modified) entries from a PocketBase collection.
8
- *
9
- * @param options Options for the loader.
10
- * @param context Context of the loader.
11
- * @param superuserToken Superuser token to access all resources.
12
- * @param lastModified Date of the last fetch to only update changed entries.
13
- */
14
- export async function loadEntries(
15
- options: PocketBaseLoaderOptions,
16
- context: LoaderContext,
17
- superuserToken: string | undefined,
18
- lastModified: string | undefined
19
- ): Promise<void> {
20
- // Log the fetching of the entries
21
- context.logger.info(
22
- `Fetching${lastModified ? " modified" : ""} data${
23
- lastModified ? ` starting at ${lastModified}` : ""
24
- }${superuserToken ? " as superuser" : ""}`
25
- );
26
-
27
- let numEntries = 0;
28
- await fetchCollection(
29
- options,
30
- async (entries) => {
31
- // Parse and store the entries
32
- for (const entry of entries) {
33
- await parseEntry(entry, context, options);
34
- }
35
-
36
- numEntries += entries.length;
37
- },
38
- superuserToken,
39
- {
40
- lastModified
41
- }
42
- );
43
-
44
- // Log the number of fetched entries
45
- if (lastModified) {
46
- context.logger.info(
47
- `Updated ${numEntries}/${context.store.keys().length} entries.`
48
- );
49
- } else {
50
- context.logger.info(`Fetched ${numEntries} entries.`);
51
- }
52
- }
@@ -1,77 +0,0 @@
1
- import type { LoaderContext } from "astro/loaders";
2
- import packageJson from "../../package.json";
3
- import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
4
- import { shouldRefresh } from "../utils/should-refresh";
5
- import { cleanupEntries } from "./cleanup-entries";
6
- import { handleRealtimeUpdates } from "./handle-realtime-updates";
7
- import { loadEntries } from "./load-entries";
8
-
9
- /**
10
- * Load entries from a PocketBase collection.
11
- */
12
- export async function loader(
13
- context: LoaderContext,
14
- options: PocketBaseLoaderOptions,
15
- token: string | undefined
16
- ): Promise<void> {
17
- context.logger.label = `pocketbase-loader:${options.collectionName}`;
18
-
19
- // Check if the collection should be refreshed.
20
- const refresh = shouldRefresh(
21
- context.refreshContextData,
22
- options.collectionName
23
- );
24
- if (refresh === "skip") {
25
- return;
26
- }
27
-
28
- // Handle realtime updates
29
- const handled = await handleRealtimeUpdates(context, options);
30
- if (handled) {
31
- return;
32
- }
33
-
34
- // Get the date of the last fetch to only update changed entries.
35
- let lastModified = context.meta.get("last-modified");
36
-
37
- // Force a full update if the refresh is forced
38
- if (refresh === "force") {
39
- lastModified = undefined;
40
- context.store.clear();
41
- }
42
-
43
- // Check if the version has changed to force an update
44
- const lastVersion = context.meta.get("version");
45
- if (lastVersion !== packageJson.version) {
46
- if (lastVersion) {
47
- context.logger.info(
48
- `PocketBase loader was updated from ${lastVersion} to ${packageJson.version}. All entries will be loaded again.`
49
- );
50
- }
51
-
52
- // Disable incremental builds and clear the store
53
- lastModified = undefined;
54
- context.store.clear();
55
- }
56
-
57
- // Disable incremental builds if no updated field is provided
58
- if (!options.updatedField) {
59
- context.logger.info(
60
- `No "updatedField" was provided. Incremental builds are disabled.`
61
- );
62
- lastModified = undefined;
63
- }
64
-
65
- if (context.store.keys().length > 0) {
66
- // Cleanup entries that are no longer in the collection
67
- await cleanupEntries(options, context, token);
68
- }
69
-
70
- // Load the (modified) entries
71
- await loadEntries(options, context, token, lastModified);
72
-
73
- // Set the last modified date to the current date
74
- context.meta.set("last-modified", new Date().toISOString().replace("T", " "));
75
-
76
- context.meta.set("version", packageJson.version);
77
- }
@@ -1,90 +0,0 @@
1
- import type { LoaderContext } from "astro/loaders";
2
- import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
3
- import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
4
- import { slugify } from "../utils/slugify";
5
-
6
- /**
7
- * Parse an entry from PocketBase to match the schema and store it in the store.
8
- *
9
- * @param entry Entry to parse.
10
- * @param context Context of the loader.
11
- * @param idField Field to use as id for the entry.
12
- * If not provided, the id of the entry will be used.
13
- * @param contentFields Field(s) to use as content for the entry.
14
- * If multiple fields are used, they will be concatenated and wrapped in `<section>` elements.
15
- */
16
- export async function parseEntry(
17
- entry: PocketBaseEntry,
18
- { generateDigest, parseData, store, logger }: LoaderContext,
19
- { idField, contentFields, updatedField }: PocketBaseLoaderOptions
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
-
42
- // Parse the data to match the schema
43
- // This will throw an error if the data does not match the schema
44
- const data = await parseData({
45
- id,
46
- data: entry
47
- });
48
-
49
- // Get the updated date of the entry
50
- let updated: string | undefined;
51
- if (updatedField) {
52
- updated = `${entry[updatedField]}`;
53
- }
54
-
55
- // Generate a digest for the entry
56
- // If no updated date is available, the digest will be generated from the whole entry
57
- const digest = generateDigest(updated ?? entry);
58
-
59
- if (!contentFields) {
60
- // Store the entry
61
- store.set({
62
- id,
63
- data,
64
- digest
65
- });
66
- return;
67
- }
68
-
69
- // Generate the content for the entry
70
- let content: string;
71
- if (typeof contentFields === "string") {
72
- // Only one field is used as content
73
- content = `${entry[contentFields]}`;
74
- } else {
75
- // Multiple fields are used as content, wrap each block in a section and concatenate them
76
- content = contentFields
77
- .map((field) => `<section id="${field}">${entry[field]}</section>`)
78
- .join("");
79
- }
80
-
81
- // Store the entry
82
- store.set({
83
- id,
84
- data,
85
- digest,
86
- rendered: {
87
- html: content
88
- }
89
- });
90
- }
@@ -1,66 +0,0 @@
1
- import type { LiveDataEntry } from "astro";
2
- import { LiveCollectionValidationError } from "astro/content/runtime";
3
- import { z } from "astro/zod";
4
- import type { PocketBaseEntry } from "../types/pocketbase-entry.type";
5
- import type { PocketBaseLiveLoaderOptions } from "../types/pocketbase-loader-options.type";
6
-
7
- /**
8
- * Converts a PocketBase entry into a LiveDataEntry for Astro, extracting content and cache metadata.
9
- */
10
- export function parseLiveEntry<TEntry extends PocketBaseEntry>(
11
- entry: TEntry,
12
- options: PocketBaseLiveLoaderOptions
13
- ): LiveDataEntry<TEntry> {
14
- // Build a cache tag
15
- const tag = `${options.collectionName}-${entry.id}`;
16
-
17
- let lastModified: Date | undefined = undefined;
18
- // If an updated field is provided and the entry has a valid date value,
19
- // use it as the last modified date cache hint
20
- if (options.updatedField && entry[options.updatedField]) {
21
- const value = `${entry[options.updatedField]}`;
22
- const date = z.coerce.date().safeParse(value);
23
- if (!date.success) {
24
- throw new LiveCollectionValidationError(
25
- options.collectionName,
26
- entry.id,
27
- date.error
28
- );
29
- }
30
- lastModified = date.data;
31
- }
32
-
33
- if (!options.contentFields) {
34
- return {
35
- id: entry.id,
36
- data: entry,
37
- cacheHint: {
38
- tags: [tag],
39
- lastModified
40
- }
41
- };
42
- }
43
-
44
- let content: string;
45
- if (typeof options.contentFields === "string") {
46
- // If a single content field is provided, use it directly
47
- content = `${entry[options.contentFields]}`;
48
- } else {
49
- // If multiple content fields are provided, concatenate them with `<section>` tags
50
- content = options.contentFields
51
- .map((field) => `<section id="${field}">${entry[field]}</section>`)
52
- .join("");
53
- }
54
-
55
- return {
56
- id: entry.id,
57
- data: entry,
58
- rendered: {
59
- html: content
60
- },
61
- cacheHint: {
62
- tags: [tag],
63
- lastModified
64
- }
65
- };
66
- }
@@ -1,98 +0,0 @@
1
- import type { LiveDataCollection, LiveDataEntry } from "astro";
2
- import type { LiveCollectionError } from "astro/content/runtime";
3
- import type { LiveLoader, Loader, LoaderContext } from "astro/loaders";
4
- import { liveCollectionLoader } from "./loader/live-collection-loader";
5
- import { liveEntryLoader } from "./loader/live-entry-loader";
6
- import { loader } from "./loader/loader";
7
- import { generateSchema } from "./schema/generate-schema";
8
- import { generateType } from "./schema/generate-type";
9
- import type { PocketBaseEntry } from "./types/pocketbase-entry.type";
10
- import type {
11
- PocketBaseLiveLoaderCollectionFilter,
12
- PocketBaseLiveLoaderEntryFilter
13
- } from "./types/pocketbase-live-loader-filter.type";
14
- import type {
15
- PocketBaseLiveLoaderOptions,
16
- PocketBaseLoaderOptions
17
- } from "./types/pocketbase-loader-options.type";
18
- import { createTokenPromise } from "./utils/create-token-promise";
19
-
20
- /**
21
- * Loader for collections stored in PocketBase.
22
- *
23
- * @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
24
- */
25
- // oxlint-disable-next-line explicit-module-boundary-types
26
- export function pocketbaseLoader(options: PocketBaseLoaderOptions) {
27
- // Create shared promise for the superuser token, which can be reused
28
- const tokenPromise = createTokenPromise(options);
29
-
30
- return {
31
- name: "pocketbase-loader",
32
- load: async (context: LoaderContext): Promise<void> => {
33
- if (options.experimental?.liveTypesOnly) {
34
- context.logger.label = `pocketbase-loader:${options.collectionName}`;
35
- context.logger.info(
36
- "Experimental live types only mode enabled. No data will be loaded, only types will be generated."
37
- );
38
- return;
39
- }
40
-
41
- const token = await tokenPromise;
42
-
43
- // Load the entries from the collection
44
- await loader(context, options, token);
45
- },
46
- createSchema: async () => {
47
- const token = await tokenPromise;
48
-
49
- // Generate the schema for the collection according to the API
50
- const schema = await generateSchema(options, token);
51
- const types = generateType(schema);
52
-
53
- return {
54
- schema,
55
- types
56
- };
57
- }
58
- } satisfies Loader;
59
- }
60
-
61
- /**
62
- * Live loader for collections stored in PocketBase.
63
- *
64
- * @param options Options for the live loader. See {@link PocketBaseLiveLoaderOptions} for more details.
65
- */
66
- export function pocketbaseLiveLoader<TEntry extends PocketBaseEntry>(
67
- options: PocketBaseLiveLoaderOptions
68
- ): LiveLoader<
69
- TEntry,
70
- PocketBaseLiveLoaderEntryFilter,
71
- PocketBaseLiveLoaderCollectionFilter,
72
- LiveCollectionError
73
- > {
74
- // Create shared promise for the superuser token, which can be reused
75
- const tokenPromise = createTokenPromise(options);
76
-
77
- return {
78
- name: "pocketbase-live-loader",
79
- loadCollection: async ({
80
- filter
81
- }): Promise<
82
- LiveDataCollection<TEntry> | { error: LiveCollectionError }
83
- > => {
84
- const token = await tokenPromise;
85
-
86
- // Load entries from the collection
87
- return liveCollectionLoader<TEntry>(filter, options, token);
88
- },
89
- loadEntry: async ({
90
- filter
91
- }): Promise<LiveDataEntry<TEntry> | { error: LiveCollectionError }> => {
92
- const token = await tokenPromise;
93
-
94
- // Load a single entry from the collection
95
- return liveEntryLoader<TEntry>(filter.id, options, token);
96
- }
97
- };
98
- }