astro-loader-pocketbase 3.1.1 → 3.1.2-next.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/dist/index.d.mts +276 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +973 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +26 -14
- package/src/index.ts +0 -24
- package/src/loader/cleanup-entries.ts +0 -137
- package/src/loader/fetch-collection.ts +0 -177
- package/src/loader/fetch-entry.ts +0 -83
- package/src/loader/handle-realtime-updates.ts +0 -56
- package/src/loader/live-collection-loader.ts +0 -51
- package/src/loader/live-entry-loader.ts +0 -38
- package/src/loader/load-entries.ts +0 -52
- package/src/loader/loader.ts +0 -77
- package/src/loader/parse-entry.ts +0 -90
- package/src/loader/parse-live-entry.ts +0 -66
- package/src/pocketbase-loader.ts +0 -98
- package/src/schema/generate-schema.ts +0 -200
- package/src/schema/generate-type.ts +0 -23
- package/src/schema/get-remote-schema.ts +0 -43
- package/src/schema/parse-schema.ts +0 -170
- package/src/schema/read-local-schema.ts +0 -46
- package/src/schema/transform-files.ts +0 -67
- package/src/tsconfig.json +0 -7
- package/src/types/errors.ts +0 -19
- package/src/types/pocketbase-api-response.type.ts +0 -40
- package/src/types/pocketbase-entry.type.ts +0 -24
- package/src/types/pocketbase-live-loader-filter.type.ts +0 -55
- package/src/types/pocketbase-loader-options.type.ts +0 -146
- package/src/types/pocketbase-schema.type.ts +0 -101
- package/src/utils/combine-fields-for-request.ts +0 -55
- package/src/utils/create-token-promise.ts +0 -25
- package/src/utils/extract-field-names.ts +0 -15
- package/src/utils/format-fields.ts +0 -66
- package/src/utils/get-superuser-token.ts +0 -76
- package/src/utils/is-realtime-data.ts +0 -34
- package/src/utils/should-refresh.ts +0 -37
- package/src/utils/slugify.ts +0 -21
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import { LiveCollectionError, LiveCollectionValidationError, LiveEntryNotFoundError } from "astro/content/runtime";
|
|
2
|
+
import { z } from "astro/zod";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { createAuxiliaryTypeStore, createTypeAlias, printNode, zodToTs } from "zod-to-ts";
|
|
6
|
+
//#region src/types/errors.ts
|
|
7
|
+
/**
|
|
8
|
+
* Error thrown when there is an authentication issue with PocketBase.
|
|
9
|
+
*/
|
|
10
|
+
var PocketBaseAuthenticationError = class extends LiveCollectionError {
|
|
11
|
+
constructor(collection, message) {
|
|
12
|
+
super(collection, message);
|
|
13
|
+
this.name = "PocketBaseAuthenticationError";
|
|
14
|
+
}
|
|
15
|
+
static is(error) {
|
|
16
|
+
return !!error && error.name === "PocketBaseAuthenticationError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/types/pocketbase-entry.type.ts
|
|
21
|
+
/**
|
|
22
|
+
* Schema for a PocketBase entry.
|
|
23
|
+
*/
|
|
24
|
+
const pocketBaseEntry = z.looseObject({
|
|
25
|
+
/**
|
|
26
|
+
* ID of the entry.
|
|
27
|
+
*/
|
|
28
|
+
id: z.string(),
|
|
29
|
+
/**
|
|
30
|
+
* ID of the collection the entry belongs to.
|
|
31
|
+
*/
|
|
32
|
+
collectionId: z.string(),
|
|
33
|
+
/**
|
|
34
|
+
* Name of the collection the entry belongs to.
|
|
35
|
+
*/
|
|
36
|
+
collectionName: z.string()
|
|
37
|
+
});
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/types/pocketbase-api-response.type.ts
|
|
40
|
+
/**
|
|
41
|
+
* The schema for a PocketBase error response.
|
|
42
|
+
*/
|
|
43
|
+
const pocketBaseErrorResponse = z.object({
|
|
44
|
+
/**
|
|
45
|
+
* The error message returned by PocketBase.
|
|
46
|
+
*/
|
|
47
|
+
message: z.string() });
|
|
48
|
+
/**
|
|
49
|
+
* The schema for a PocketBase list response.
|
|
50
|
+
*/
|
|
51
|
+
const pocketBaseListResponse = z.object({
|
|
52
|
+
/**
|
|
53
|
+
* Current page number.
|
|
54
|
+
*/
|
|
55
|
+
page: z.number(),
|
|
56
|
+
/**
|
|
57
|
+
* Total number of pages available.
|
|
58
|
+
*/
|
|
59
|
+
totalPages: z.number(),
|
|
60
|
+
/**
|
|
61
|
+
* Array of items in the current page.
|
|
62
|
+
*/
|
|
63
|
+
items: z.array(pocketBaseEntry)
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* The schema for a PocketBase login response.
|
|
67
|
+
*/
|
|
68
|
+
const pocketBaseLoginResponse = z.object({
|
|
69
|
+
/**
|
|
70
|
+
* The authentication token returned by PocketBase.
|
|
71
|
+
*/
|
|
72
|
+
token: z.string() });
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/utils/combine-fields-for-request.ts
|
|
75
|
+
/**
|
|
76
|
+
* Combine basic, special, and user-specified fields for PocketBase API requests.
|
|
77
|
+
* This utility ensures that required system fields are always included in API requests.
|
|
78
|
+
*
|
|
79
|
+
* @param userFields Array of fields specified by the user, or undefined for all fields
|
|
80
|
+
* @param options PocketBase loader options containing custom field configurations
|
|
81
|
+
* @returns Combined array of fields to include in the API request, or undefined for all fields
|
|
82
|
+
*/
|
|
83
|
+
function combineFieldsForRequest(userFields, options) {
|
|
84
|
+
if (!userFields) return;
|
|
85
|
+
const basicFields = [
|
|
86
|
+
"id",
|
|
87
|
+
"collectionId",
|
|
88
|
+
"collectionName"
|
|
89
|
+
];
|
|
90
|
+
const specialFields = [];
|
|
91
|
+
if (options.idField && options.idField !== "id") specialFields.push(options.idField);
|
|
92
|
+
if (options.updatedField) specialFields.push(options.updatedField);
|
|
93
|
+
if (options.contentFields) if (Array.isArray(options.contentFields)) specialFields.push(...options.contentFields);
|
|
94
|
+
else specialFields.push(options.contentFields);
|
|
95
|
+
return [...new Set([
|
|
96
|
+
...basicFields,
|
|
97
|
+
...specialFields,
|
|
98
|
+
...userFields
|
|
99
|
+
])];
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/utils/format-fields.ts
|
|
103
|
+
/**
|
|
104
|
+
* Format fields option into an array and validate for expand usage.
|
|
105
|
+
* Handles wildcard "*" and preserves excerpt field modifiers.
|
|
106
|
+
*
|
|
107
|
+
* @param fields The fields option (string or array)
|
|
108
|
+
* @returns Formatted fields array, or undefined if no fields specified or "*" wildcard is used
|
|
109
|
+
*/
|
|
110
|
+
function formatFields(fields) {
|
|
111
|
+
if (!fields || fields.length === 0) return;
|
|
112
|
+
let fieldList;
|
|
113
|
+
if (Array.isArray(fields)) fieldList = fields.map((f) => f.trim());
|
|
114
|
+
else fieldList = splitFieldsString(fields).map((f) => f.trim());
|
|
115
|
+
if (fieldList.some((field) => field.includes("expand"))) {
|
|
116
|
+
console.warn("The \"expand\" parameter is not currently supported by astro-loader-pocketbase and will be filtered out.");
|
|
117
|
+
fieldList = fieldList.filter((field) => !field.includes("expand"));
|
|
118
|
+
}
|
|
119
|
+
if (fieldList.some((field) => field === "*")) return;
|
|
120
|
+
return fieldList;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Splits the fields string at `,` but respects the `:excerpt(number, boolean)` option
|
|
124
|
+
*/
|
|
125
|
+
function splitFieldsString(fieldsString) {
|
|
126
|
+
const initialSplit = fieldsString.split(",");
|
|
127
|
+
const fields = [];
|
|
128
|
+
for (let i = 0; i < initialSplit.length; i++) {
|
|
129
|
+
const part = initialSplit.at(i);
|
|
130
|
+
if (!part) continue;
|
|
131
|
+
if (part.includes("(") && !part.includes(")")) {
|
|
132
|
+
fields.push(`${part},${initialSplit[++i]}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
fields.push(part);
|
|
136
|
+
}
|
|
137
|
+
return fields;
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/loader/fetch-collection.ts
|
|
141
|
+
/**
|
|
142
|
+
* Fetches entries from a PocketBase collection, optionally filtering by modification date and supporting pagination.
|
|
143
|
+
*/
|
|
144
|
+
async function fetchCollection(options, chunkLoaded, token, collectionFilter) {
|
|
145
|
+
const collectionUrl = new URL(`api/collections/${options.collectionName}/records`, options.url);
|
|
146
|
+
const collectionHeaders = new Headers();
|
|
147
|
+
if (token) collectionHeaders.set("Authorization", token);
|
|
148
|
+
const combinedFields = combineFieldsForRequest(formatFields(options.fields), options);
|
|
149
|
+
let page = 0;
|
|
150
|
+
let totalPages = 0;
|
|
151
|
+
do {
|
|
152
|
+
collectionUrl.search = buildSearchParams(options, combinedFields, {
|
|
153
|
+
...collectionFilter,
|
|
154
|
+
page: collectionFilter?.page ?? ++page,
|
|
155
|
+
perPage: collectionFilter?.perPage ?? 100
|
|
156
|
+
}).toString();
|
|
157
|
+
const collectionRequest = await fetch(collectionUrl.href, { headers: collectionHeaders });
|
|
158
|
+
if (!collectionRequest.ok) {
|
|
159
|
+
if (collectionRequest.status === 403) if (options.superuserCredentials && "impersonateToken" in options.superuserCredentials) throw new PocketBaseAuthenticationError(options.collectionName, "The given impersonate token is not valid.");
|
|
160
|
+
else throw new PocketBaseAuthenticationError(options.collectionName, "The collection is not accessible without superuser rights. Please provide superuser credentials in the config.");
|
|
161
|
+
if (collectionRequest.status === 404) throw new LiveEntryNotFoundError(options.collectionName, { ...collectionFilter });
|
|
162
|
+
const errorResponse = pocketBaseErrorResponse.parse(await collectionRequest.json());
|
|
163
|
+
throw new LiveCollectionError(options.collectionName, errorResponse.message);
|
|
164
|
+
}
|
|
165
|
+
const response = pocketBaseListResponse.parse(await collectionRequest.json());
|
|
166
|
+
await chunkLoaded(response.items);
|
|
167
|
+
page = response.page;
|
|
168
|
+
totalPages = response.totalPages;
|
|
169
|
+
} while (!collectionFilter?.perPage && page < totalPages);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build search parameters for the PocketBase collection request.
|
|
173
|
+
*/
|
|
174
|
+
function buildSearchParams(loaderOptions, combinedFields, collectionFilter) {
|
|
175
|
+
const searchParams = new URLSearchParams();
|
|
176
|
+
if (collectionFilter.page) searchParams.set("page", `${collectionFilter.page}`);
|
|
177
|
+
if (collectionFilter.perPage) searchParams.set("perPage", `${collectionFilter.perPage}`);
|
|
178
|
+
const filters = [];
|
|
179
|
+
if (collectionFilter.lastModified && loaderOptions.updatedField) {
|
|
180
|
+
filters.push(`(${loaderOptions.updatedField}>"${collectionFilter.lastModified}")`);
|
|
181
|
+
searchParams.set("sort", `-${loaderOptions.updatedField},id`);
|
|
182
|
+
}
|
|
183
|
+
if (loaderOptions.filter) filters.push(`(${loaderOptions.filter})`);
|
|
184
|
+
if (collectionFilter.filter) filters.push(`(${collectionFilter.filter})`);
|
|
185
|
+
if (filters.length > 0) searchParams.set("filter", filters.join("&&"));
|
|
186
|
+
if (collectionFilter.sort) searchParams.set("sort", collectionFilter.sort);
|
|
187
|
+
if (combinedFields) searchParams.set("fields", combinedFields.join(","));
|
|
188
|
+
return searchParams;
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/loader/parse-live-entry.ts
|
|
192
|
+
/**
|
|
193
|
+
* Converts a PocketBase entry into a LiveDataEntry for Astro, extracting content and cache metadata.
|
|
194
|
+
*/
|
|
195
|
+
function parseLiveEntry(entry, options) {
|
|
196
|
+
const tag = `${options.collectionName}-${entry.id}`;
|
|
197
|
+
let lastModified = void 0;
|
|
198
|
+
if (options.updatedField && entry[options.updatedField]) {
|
|
199
|
+
const value = `${entry[options.updatedField]}`;
|
|
200
|
+
const date = z.coerce.date().safeParse(value);
|
|
201
|
+
if (!date.success) throw new LiveCollectionValidationError(options.collectionName, entry.id, date.error);
|
|
202
|
+
lastModified = date.data;
|
|
203
|
+
}
|
|
204
|
+
if (!options.contentFields) return {
|
|
205
|
+
id: entry.id,
|
|
206
|
+
data: entry,
|
|
207
|
+
cacheHint: {
|
|
208
|
+
tags: [tag],
|
|
209
|
+
lastModified
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
let content;
|
|
213
|
+
if (typeof options.contentFields === "string") content = `${entry[options.contentFields]}`;
|
|
214
|
+
else content = options.contentFields.map((field) => `<section id="${field}">${entry[field]}</section>`).join("");
|
|
215
|
+
return {
|
|
216
|
+
id: entry.id,
|
|
217
|
+
data: entry,
|
|
218
|
+
rendered: { html: content },
|
|
219
|
+
cacheHint: {
|
|
220
|
+
tags: [tag],
|
|
221
|
+
lastModified
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/loader/live-collection-loader.ts
|
|
227
|
+
/**
|
|
228
|
+
* Loads and parses a PocketBase collection for live data, returning entries or an error.
|
|
229
|
+
*/
|
|
230
|
+
async function liveCollectionLoader(collectionFilter, options, token) {
|
|
231
|
+
const entries = [];
|
|
232
|
+
try {
|
|
233
|
+
await fetchCollection(options, async (chunk) => {
|
|
234
|
+
entries.push(...chunk.map((entry) => parseLiveEntry(entry, options)));
|
|
235
|
+
}, token, collectionFilter);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error instanceof LiveCollectionError) return { error };
|
|
238
|
+
if (error instanceof Error) return { error: new LiveCollectionError(options.collectionName, error.message, error) };
|
|
239
|
+
return { error: new LiveCollectionError(options.collectionName, String(error)) };
|
|
240
|
+
}
|
|
241
|
+
return { entries };
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/loader/fetch-entry.ts
|
|
245
|
+
/**
|
|
246
|
+
* Retrieves a specific entry from a PocketBase collection using its ID and loader options.
|
|
247
|
+
*/
|
|
248
|
+
async function fetchEntry(id, options, token) {
|
|
249
|
+
const entryUrl = new URL(`api/collections/${options.collectionName}/records/${id}`, options.url);
|
|
250
|
+
const combinedFields = combineFieldsForRequest(formatFields(options.fields), options);
|
|
251
|
+
if (combinedFields) entryUrl.searchParams.set("fields", combinedFields.join(","));
|
|
252
|
+
const entryHeaders = new Headers();
|
|
253
|
+
if (token) entryHeaders.set("Authorization", token);
|
|
254
|
+
const entryRequest = await fetch(entryUrl.href, { headers: entryHeaders });
|
|
255
|
+
if (!entryRequest.ok) {
|
|
256
|
+
if (entryRequest.status === 403) if (options.superuserCredentials && "impersonateToken" in options.superuserCredentials) throw new PocketBaseAuthenticationError(options.collectionName, "The given impersonate token is not valid.");
|
|
257
|
+
else throw new PocketBaseAuthenticationError(options.collectionName, "The entry is not accessible without superuser rights. Please provide superuser credentials in the config.");
|
|
258
|
+
if (entryRequest.status === 404) throw new LiveEntryNotFoundError(options.collectionName, id);
|
|
259
|
+
const errorResponse = pocketBaseErrorResponse.parse(await entryRequest.json());
|
|
260
|
+
throw new LiveCollectionError(options.collectionName, errorResponse.message);
|
|
261
|
+
}
|
|
262
|
+
return pocketBaseEntry.parse(await entryRequest.json());
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/loader/live-entry-loader.ts
|
|
266
|
+
/**
|
|
267
|
+
* Loads and parses a single PocketBase entry for live data, returning the entry or an error.
|
|
268
|
+
*/
|
|
269
|
+
async function liveEntryLoader(id, options, token) {
|
|
270
|
+
try {
|
|
271
|
+
return parseLiveEntry(await fetchEntry(id, options, token), options);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error instanceof LiveCollectionError) return { error };
|
|
274
|
+
if (error instanceof Error) return { error: new LiveCollectionError(options.collectionName, error.message, error) };
|
|
275
|
+
return { error: new LiveCollectionError(options.collectionName, String(error)) };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region package.json
|
|
280
|
+
var version = "3.1.2-next.2";
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/utils/should-refresh.ts
|
|
283
|
+
/**
|
|
284
|
+
* Checks if the collection should be refreshed.
|
|
285
|
+
*/
|
|
286
|
+
function shouldRefresh(context, collectionName) {
|
|
287
|
+
if (context?.source !== "astro-integration-pocketbase") return "refresh";
|
|
288
|
+
if (context.force) return "force";
|
|
289
|
+
if (!context.collection) return "refresh";
|
|
290
|
+
if (typeof context.collection === "string") return context.collection === collectionName ? "refresh" : "skip";
|
|
291
|
+
if (Array.isArray(context.collection)) return context.collection.includes(collectionName) ? "refresh" : "skip";
|
|
292
|
+
return "refresh";
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/loader/cleanup-entries.ts
|
|
296
|
+
/**
|
|
297
|
+
* Cleanup entries that are no longer in the collection.
|
|
298
|
+
*
|
|
299
|
+
* @param options Options for the loader.
|
|
300
|
+
* @param context Context of the loader.
|
|
301
|
+
* @param superuserToken Superuser token to access all resources.
|
|
302
|
+
*/
|
|
303
|
+
async function cleanupEntries(options, context, superuserToken) {
|
|
304
|
+
const collectionUrl = new URL(`api/collections/${options.collectionName}/records`, options.url).href;
|
|
305
|
+
const collectionHeaders = new Headers();
|
|
306
|
+
if (superuserToken) collectionHeaders.set("Authorization", superuserToken);
|
|
307
|
+
let page = 0;
|
|
308
|
+
let totalPages = 0;
|
|
309
|
+
const entries = /* @__PURE__ */ new Set();
|
|
310
|
+
do {
|
|
311
|
+
const searchParams = new URLSearchParams({
|
|
312
|
+
page: `${++page}`,
|
|
313
|
+
perPage: "1000",
|
|
314
|
+
fields: "id"
|
|
315
|
+
});
|
|
316
|
+
if (options.filter) searchParams.set("filter", `(${options.filter})`);
|
|
317
|
+
const collectionRequest = await fetch(`${collectionUrl}?${searchParams.toString()}`, { headers: collectionHeaders });
|
|
318
|
+
if (!collectionRequest.ok) {
|
|
319
|
+
if (collectionRequest.status === 403) if (options.superuserCredentials && "impersonateToken" in options.superuserCredentials) context.logger.error("The given impersonate token is not valid.");
|
|
320
|
+
else context.logger.error("The collection is not accessible without superuser rights. Please provide superuser credentials in the config.");
|
|
321
|
+
else {
|
|
322
|
+
const errorResponse = pocketBaseErrorResponse.parse(await collectionRequest.json());
|
|
323
|
+
const errorMessage = `Fetching ids failed with status code ${collectionRequest.status}.\nReason: ${errorResponse.message}`;
|
|
324
|
+
context.logger.error(errorMessage);
|
|
325
|
+
}
|
|
326
|
+
context.logger.info(`Removing all entries from the store.`);
|
|
327
|
+
context.store.clear();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const response = cleanUpEntriesResponse.parse(await collectionRequest.json());
|
|
331
|
+
for (const item of response.items) entries.add(item.id);
|
|
332
|
+
page = response.page;
|
|
333
|
+
totalPages = response.totalPages;
|
|
334
|
+
} while (page < totalPages);
|
|
335
|
+
let cleanedUp = 0;
|
|
336
|
+
const storedIds = new Map(context.store.values().map((entry) => [entry.data.id, entry.id]));
|
|
337
|
+
for (const [pocketbaseId, storeKey] of storedIds.entries()) if (!entries.has(pocketbaseId)) {
|
|
338
|
+
context.store.delete(storeKey);
|
|
339
|
+
cleanedUp++;
|
|
340
|
+
}
|
|
341
|
+
if (cleanedUp > 0) context.logger.info(`Cleaned up ${cleanedUp} old entries.`);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The response schema for cleaning up entries.
|
|
345
|
+
*/
|
|
346
|
+
const cleanUpEntriesResponse = pocketBaseListResponse.omit({ items: true }).extend({ items: z.array(pocketBaseEntry.pick({ id: true })) });
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/utils/is-realtime-data.ts
|
|
349
|
+
/**
|
|
350
|
+
* Schema for realtime data received from PocketBase.
|
|
351
|
+
*/
|
|
352
|
+
const realtimeDataSchema = z.object({
|
|
353
|
+
action: z.union([
|
|
354
|
+
z.literal("create"),
|
|
355
|
+
z.literal("update"),
|
|
356
|
+
z.literal("delete")
|
|
357
|
+
]),
|
|
358
|
+
record: z.object({
|
|
359
|
+
id: z.string(),
|
|
360
|
+
collectionName: z.string(),
|
|
361
|
+
collectionId: z.string()
|
|
362
|
+
})
|
|
363
|
+
});
|
|
364
|
+
/**
|
|
365
|
+
* Checks if the given data is realtime data received from PocketBase.
|
|
366
|
+
*/
|
|
367
|
+
function isRealtimeData(data) {
|
|
368
|
+
try {
|
|
369
|
+
realtimeDataSchema.parse(data);
|
|
370
|
+
return true;
|
|
371
|
+
} catch {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/utils/slugify.ts
|
|
377
|
+
/**
|
|
378
|
+
* Convert a string to a slug.
|
|
379
|
+
*
|
|
380
|
+
* Example:
|
|
381
|
+
* ```ts
|
|
382
|
+
* slugify("Hello World!"); // hello-world
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
function slugify(input) {
|
|
386
|
+
return input.toLowerCase().replaceAll(/\s+/g, "-").replaceAll("ä", "ae").replaceAll("ö", "oe").replaceAll("ü", "ue").replaceAll("ß", "ss").replaceAll(/[^\w-]+/g, "").replaceAll(/--+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/loader/parse-entry.ts
|
|
390
|
+
/**
|
|
391
|
+
* Parse an entry from PocketBase to match the schema and store it in the store.
|
|
392
|
+
*
|
|
393
|
+
* @param entry Entry to parse.
|
|
394
|
+
* @param context Context of the loader.
|
|
395
|
+
* @param idField Field to use as id for the entry.
|
|
396
|
+
* If not provided, the id of the entry will be used.
|
|
397
|
+
* @param contentFields Field(s) to use as content for the entry.
|
|
398
|
+
* If multiple fields are used, they will be concatenated and wrapped in `<section>` elements.
|
|
399
|
+
*/
|
|
400
|
+
async function parseEntry(entry, { generateDigest, parseData, store, logger }, { idField, contentFields, updatedField }) {
|
|
401
|
+
let id = entry.id;
|
|
402
|
+
if (idField) {
|
|
403
|
+
const customEntryId = entry[idField];
|
|
404
|
+
if (!customEntryId) logger.warn(`The entry "${id}" does not have a value for field ${idField}. Using the default ID instead.`);
|
|
405
|
+
else id = slugify(`${customEntryId}`);
|
|
406
|
+
}
|
|
407
|
+
const oldEntry = store.get(id);
|
|
408
|
+
if (oldEntry && oldEntry.data.id !== entry.id) logger.warn(`The entry "${entry.id}" seems to be a duplicate of "${oldEntry.data.id}". Please make sure to use unique IDs in the column "${idField}".`);
|
|
409
|
+
const data = await parseData({
|
|
410
|
+
id,
|
|
411
|
+
data: entry
|
|
412
|
+
});
|
|
413
|
+
let updated;
|
|
414
|
+
if (updatedField) updated = `${entry[updatedField]}`;
|
|
415
|
+
const digest = generateDigest(updated ?? entry);
|
|
416
|
+
if (!contentFields) {
|
|
417
|
+
store.set({
|
|
418
|
+
id,
|
|
419
|
+
data,
|
|
420
|
+
digest
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
let content;
|
|
425
|
+
if (typeof contentFields === "string") content = `${entry[contentFields]}`;
|
|
426
|
+
else content = contentFields.map((field) => `<section id="${field}">${entry[field]}</section>`).join("");
|
|
427
|
+
store.set({
|
|
428
|
+
id,
|
|
429
|
+
data,
|
|
430
|
+
digest,
|
|
431
|
+
rendered: { html: content }
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region src/loader/handle-realtime-updates.ts
|
|
436
|
+
/**
|
|
437
|
+
* Handles realtime updates for the loader without making any new network requests.
|
|
438
|
+
*
|
|
439
|
+
* Returns `true` if the data was handled and no further action is needed.
|
|
440
|
+
*/
|
|
441
|
+
async function handleRealtimeUpdates(context, options) {
|
|
442
|
+
if (options.filter) return false;
|
|
443
|
+
if (!context.refreshContextData?.data) return false;
|
|
444
|
+
const data = context.refreshContextData.data;
|
|
445
|
+
if (!isRealtimeData(data)) return false;
|
|
446
|
+
if (data.record.collectionName !== options.collectionName) return false;
|
|
447
|
+
if (data.action === "delete") {
|
|
448
|
+
context.logger.info("Removing deleted entry");
|
|
449
|
+
context.store.delete(data.record.id);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
if (data.action === "update") context.logger.info("Updating outdated entry");
|
|
453
|
+
else context.logger.info("Creating new entry");
|
|
454
|
+
await parseEntry(data.record, context, options);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/loader/load-entries.ts
|
|
459
|
+
/**
|
|
460
|
+
* Load (modified) entries from a PocketBase collection.
|
|
461
|
+
*
|
|
462
|
+
* @param options Options for the loader.
|
|
463
|
+
* @param context Context of the loader.
|
|
464
|
+
* @param superuserToken Superuser token to access all resources.
|
|
465
|
+
* @param lastModified Date of the last fetch to only update changed entries.
|
|
466
|
+
*/
|
|
467
|
+
async function loadEntries(options, context, superuserToken, lastModified) {
|
|
468
|
+
context.logger.info(`Fetching${lastModified ? " modified" : ""} data${lastModified ? ` starting at ${lastModified}` : ""}${superuserToken ? " as superuser" : ""}`);
|
|
469
|
+
let numEntries = 0;
|
|
470
|
+
await fetchCollection(options, async (entries) => {
|
|
471
|
+
for (const entry of entries) await parseEntry(entry, context, options);
|
|
472
|
+
numEntries += entries.length;
|
|
473
|
+
}, superuserToken, { lastModified });
|
|
474
|
+
if (lastModified) context.logger.info(`Updated ${numEntries}/${context.store.keys().length} entries.`);
|
|
475
|
+
else context.logger.info(`Fetched ${numEntries} entries.`);
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/loader/loader.ts
|
|
479
|
+
/**
|
|
480
|
+
* Load entries from a PocketBase collection.
|
|
481
|
+
*/
|
|
482
|
+
async function loader(context, options, token) {
|
|
483
|
+
context.logger.label = `pocketbase-loader:${options.collectionName}`;
|
|
484
|
+
const refresh = shouldRefresh(context.refreshContextData, options.collectionName);
|
|
485
|
+
if (refresh === "skip") return;
|
|
486
|
+
if (await handleRealtimeUpdates(context, options)) return;
|
|
487
|
+
let lastModified = context.meta.get("last-modified");
|
|
488
|
+
if (refresh === "force") {
|
|
489
|
+
lastModified = void 0;
|
|
490
|
+
context.store.clear();
|
|
491
|
+
}
|
|
492
|
+
const lastVersion = context.meta.get("version");
|
|
493
|
+
if (lastVersion !== version) {
|
|
494
|
+
if (lastVersion) context.logger.info(`PocketBase loader was updated from ${lastVersion} to ${version}. All entries will be loaded again.`);
|
|
495
|
+
lastModified = void 0;
|
|
496
|
+
context.store.clear();
|
|
497
|
+
}
|
|
498
|
+
if (!options.updatedField) {
|
|
499
|
+
context.logger.info(`No "updatedField" was provided. Incremental builds are disabled.`);
|
|
500
|
+
lastModified = void 0;
|
|
501
|
+
}
|
|
502
|
+
if (context.store.keys().length > 0) await cleanupEntries(options, context, token);
|
|
503
|
+
await loadEntries(options, context, token, lastModified);
|
|
504
|
+
context.meta.set("last-modified", (/* @__PURE__ */ new Date()).toISOString().replace("T", " "));
|
|
505
|
+
context.meta.set("version", version);
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/utils/extract-field-names.ts
|
|
509
|
+
/**
|
|
510
|
+
* Extract field names from fields that may contain modifiers like :excerpt().
|
|
511
|
+
*
|
|
512
|
+
* @param fields Array of field specifications that may contain modifiers
|
|
513
|
+
* @returns Array of clean field names suitable for schema parsing
|
|
514
|
+
*/
|
|
515
|
+
function extractFieldNames(fields) {
|
|
516
|
+
if (!fields) return;
|
|
517
|
+
return fields.map((field) => field.split(":").at(0) ?? field);
|
|
518
|
+
}
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/types/pocketbase-schema.type.ts
|
|
521
|
+
/**
|
|
522
|
+
* Entry for a collections schema in PocketBase.
|
|
523
|
+
*/
|
|
524
|
+
const pocketBaseSchemaEntry = z.object({
|
|
525
|
+
/**
|
|
526
|
+
* Flag to indicate if the field is hidden.
|
|
527
|
+
* Hidden fields are not returned in the API response.
|
|
528
|
+
*/
|
|
529
|
+
hidden: z.optional(z.boolean()),
|
|
530
|
+
/**
|
|
531
|
+
* Unique identifier for the field.
|
|
532
|
+
*/
|
|
533
|
+
id: z.string(),
|
|
534
|
+
/**
|
|
535
|
+
* Name of the field.
|
|
536
|
+
*/
|
|
537
|
+
name: z.string(),
|
|
538
|
+
/**
|
|
539
|
+
* Help text for the field.
|
|
540
|
+
* This is only present if the field has help text defined.
|
|
541
|
+
*/
|
|
542
|
+
help: z.optional(z.string()),
|
|
543
|
+
/**
|
|
544
|
+
* Type of the field.
|
|
545
|
+
*/
|
|
546
|
+
type: z.enum([
|
|
547
|
+
"text",
|
|
548
|
+
"editor",
|
|
549
|
+
"number",
|
|
550
|
+
"bool",
|
|
551
|
+
"email",
|
|
552
|
+
"url",
|
|
553
|
+
"date",
|
|
554
|
+
"autodate",
|
|
555
|
+
"select",
|
|
556
|
+
"file",
|
|
557
|
+
"relation",
|
|
558
|
+
"json",
|
|
559
|
+
"geoPoint",
|
|
560
|
+
"password"
|
|
561
|
+
]),
|
|
562
|
+
/**
|
|
563
|
+
* Whether the field is required.
|
|
564
|
+
*/
|
|
565
|
+
required: z.optional(z.boolean()),
|
|
566
|
+
/**
|
|
567
|
+
* Values for a select field.
|
|
568
|
+
* This is only present if the field type is "select".
|
|
569
|
+
*/
|
|
570
|
+
values: z.optional(z.array(z.string())),
|
|
571
|
+
/**
|
|
572
|
+
* Maximum number of values for a select field.
|
|
573
|
+
* This is only present on "select", "relation", and "file" fields.
|
|
574
|
+
*/
|
|
575
|
+
maxSelect: z.optional(z.number()),
|
|
576
|
+
/**
|
|
577
|
+
* Whether the field is filled when the entry is created.
|
|
578
|
+
* This is only present on "autodate" fields.
|
|
579
|
+
*/
|
|
580
|
+
onCreate: z.optional(z.boolean()),
|
|
581
|
+
/**
|
|
582
|
+
* Whether the field is updated when the entry is updated.
|
|
583
|
+
* This is only present on "autodate" fields.
|
|
584
|
+
*/
|
|
585
|
+
onUpdate: z.optional(z.boolean())
|
|
586
|
+
});
|
|
587
|
+
/**
|
|
588
|
+
* Schema for a PocketBase collection.
|
|
589
|
+
*/
|
|
590
|
+
const pocketBaseCollection = z.object({
|
|
591
|
+
/**
|
|
592
|
+
* Name of the collection.
|
|
593
|
+
*/
|
|
594
|
+
name: z.string(),
|
|
595
|
+
/**
|
|
596
|
+
* Type of the collection.
|
|
597
|
+
*/
|
|
598
|
+
type: z.enum([
|
|
599
|
+
"base",
|
|
600
|
+
"view",
|
|
601
|
+
"auth"
|
|
602
|
+
]),
|
|
603
|
+
/**
|
|
604
|
+
* Schema of the collection.
|
|
605
|
+
*/
|
|
606
|
+
fields: z.array(pocketBaseSchemaEntry)
|
|
607
|
+
});
|
|
608
|
+
/**
|
|
609
|
+
* Schema for an entire PocketBase database.
|
|
610
|
+
*/
|
|
611
|
+
const pocketBaseDatabase = z.array(pocketBaseCollection);
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/schema/get-remote-schema.ts
|
|
614
|
+
/**
|
|
615
|
+
* Fetches the schema for the specified collection from the PocketBase instance.
|
|
616
|
+
*
|
|
617
|
+
* @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
|
|
618
|
+
* @param token The superuser token to authenticate the request.
|
|
619
|
+
*/
|
|
620
|
+
async function getRemoteSchema(options, token) {
|
|
621
|
+
const schemaUrl = new URL(`api/collections/${options.collectionName}`, options.url).href;
|
|
622
|
+
const schemaHeaders = new Headers();
|
|
623
|
+
schemaHeaders.set("Authorization", token);
|
|
624
|
+
const schemaRequest = await fetch(schemaUrl, { headers: schemaHeaders });
|
|
625
|
+
if (!schemaRequest.ok) {
|
|
626
|
+
const errorResponse = pocketBaseErrorResponse.parse(await schemaRequest.json());
|
|
627
|
+
const errorMessage = `Fetching schema from ${options.collectionName} failed with status code ${schemaRequest.status}.\nReason: ${errorResponse.message}`;
|
|
628
|
+
console.error(errorMessage);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
return pocketBaseCollection.parse(await schemaRequest.json());
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/schema/parse-schema.ts
|
|
635
|
+
/**
|
|
636
|
+
* Converts PocketBase collection fields into Zod types, handling field types, required status, and custom schemas.
|
|
637
|
+
*/
|
|
638
|
+
function parseSchema(collection, customSchemas, options) {
|
|
639
|
+
const fields = {};
|
|
640
|
+
for (const field of collection.fields) {
|
|
641
|
+
if (options.fieldsToInclude && !options.fieldsToInclude.includes(field.name)) continue;
|
|
642
|
+
if (field.hidden && !options.hasSuperuserRights) {
|
|
643
|
+
if (options.fieldsToInclude) console.warn(`"${field.name}" is requested but hidden. Provide superuser credentials to include this field.`);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
let fieldType;
|
|
647
|
+
switch (field.type) {
|
|
648
|
+
case "number":
|
|
649
|
+
fieldType = z.number();
|
|
650
|
+
break;
|
|
651
|
+
case "bool":
|
|
652
|
+
fieldType = z.boolean();
|
|
653
|
+
break;
|
|
654
|
+
case "date":
|
|
655
|
+
case "autodate":
|
|
656
|
+
if (options.experimentalLiveTypesOnly) {
|
|
657
|
+
fieldType = z.string();
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
fieldType = z.coerce.date();
|
|
661
|
+
break;
|
|
662
|
+
case "geoPoint":
|
|
663
|
+
fieldType = z.object({
|
|
664
|
+
lon: z.number(),
|
|
665
|
+
lat: z.number()
|
|
666
|
+
});
|
|
667
|
+
break;
|
|
668
|
+
case "select":
|
|
669
|
+
if (!field.values) throw new Error(`Field ${field.name} is of type "select" but has no values defined.`);
|
|
670
|
+
fieldType = parseSingleOrMultipleValues(field, z.enum(field.values));
|
|
671
|
+
break;
|
|
672
|
+
case "relation":
|
|
673
|
+
case "file":
|
|
674
|
+
fieldType = parseSingleOrMultipleValues(field, z.string());
|
|
675
|
+
break;
|
|
676
|
+
case "json":
|
|
677
|
+
fieldType = customSchemas?.[field.name] ?? z.unknown();
|
|
678
|
+
break;
|
|
679
|
+
default:
|
|
680
|
+
fieldType = z.string();
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
if (!(field.required || field.type === "autodate" && field.onCreate || field.type === "number" || field.type === "bool")) fieldType = z.preprocess((val) => val || void 0, z.optional(fieldType));
|
|
684
|
+
fields[field.name] = fieldType.meta({
|
|
685
|
+
id: field.id,
|
|
686
|
+
title: field.name,
|
|
687
|
+
description: getFieldDescription(field)
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
return fields;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Parse the field type based on the number of values it can have
|
|
694
|
+
*
|
|
695
|
+
* @param field Field to parse
|
|
696
|
+
* @param type Type of each value
|
|
697
|
+
*
|
|
698
|
+
* @returns The parsed field type
|
|
699
|
+
*/
|
|
700
|
+
function parseSingleOrMultipleValues(field, type) {
|
|
701
|
+
if (field.maxSelect === void 0 || field.maxSelect === 1) return type;
|
|
702
|
+
return z.array(type);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Get the description for a field based on its help text and type.
|
|
706
|
+
*/
|
|
707
|
+
function getFieldDescription(field) {
|
|
708
|
+
switch (true) {
|
|
709
|
+
case !!field.help: return field.help;
|
|
710
|
+
case field.type === "autodate" && field.onUpdate: return "Date when the entry was last updated. This field is automatically updated by PocketBase whenever the entry is updated.";
|
|
711
|
+
case field.type === "autodate" && field.onCreate: return "Date when the entry was created. This field is automatically set by PocketBase when the entry is created.";
|
|
712
|
+
case field.name === "id": return "The unique identifier for the entry.";
|
|
713
|
+
case field.hidden: return "This field is hidden and may require superuser credentials to access.";
|
|
714
|
+
default: return;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/schema/read-local-schema.ts
|
|
719
|
+
/**
|
|
720
|
+
* Reads the local PocketBase schema file and returns the schema for the specified collection.
|
|
721
|
+
*
|
|
722
|
+
* @param localSchemaPath Path to the local schema file.
|
|
723
|
+
* @param collectionName Name of the collection to get the schema for.
|
|
724
|
+
*/
|
|
725
|
+
async function readLocalSchema(localSchemaPath, collectionName) {
|
|
726
|
+
const realPath = path.join(process.cwd(), localSchemaPath);
|
|
727
|
+
try {
|
|
728
|
+
const schemaFile = await fs.readFile(realPath, "utf-8");
|
|
729
|
+
const fileContent = pocketBaseDatabase.safeParse(JSON.parse(schemaFile));
|
|
730
|
+
if (!fileContent.success) throw new Error("Invalid schema file");
|
|
731
|
+
const schema = fileContent.data.find((collection) => collection.name === collectionName);
|
|
732
|
+
if (!schema) throw new Error(`Collection "${collectionName}" not found in schema file`);
|
|
733
|
+
return schema;
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.error(`Failed to read local schema from ${localSchemaPath}. No types will be generated.\nReason: ${error}`);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/schema/transform-files.ts
|
|
741
|
+
/**
|
|
742
|
+
* Transforms file names in a PocketBase entry to file URLs.
|
|
743
|
+
*
|
|
744
|
+
* @param baseUrl URL of the PocketBase instance.
|
|
745
|
+
* @param collection Collection of the entry.
|
|
746
|
+
* @param entry Entry to transform.
|
|
747
|
+
*/
|
|
748
|
+
function transformFiles(baseUrl, fileFields, entry) {
|
|
749
|
+
for (const field of fileFields) {
|
|
750
|
+
const fieldName = field.name;
|
|
751
|
+
if (field.maxSelect === 1) {
|
|
752
|
+
const fileName = z.optional(z.string()).parse(entry[fieldName]);
|
|
753
|
+
if (!fileName) continue;
|
|
754
|
+
entry[fieldName] = transformFileUrl(baseUrl, entry.collectionName, entry.id, fileName);
|
|
755
|
+
} else {
|
|
756
|
+
const fileNames = z.optional(z.array(z.string())).parse(entry[fieldName]);
|
|
757
|
+
if (!fileNames) continue;
|
|
758
|
+
entry[fieldName] = fileNames.map((file) => transformFileUrl(baseUrl, entry.collectionName, entry.id, file));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return entry;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Transforms a file name to a PocketBase file URL.
|
|
765
|
+
*
|
|
766
|
+
* @param base Base URL of the PocketBase instance.
|
|
767
|
+
* @param collectionName Name of the collection.
|
|
768
|
+
* @param entryId ID of the entry.
|
|
769
|
+
* @param file Name of the file.
|
|
770
|
+
*/
|
|
771
|
+
function transformFileUrl(base, collectionName, entryId, file) {
|
|
772
|
+
return new URL(`api/files/${collectionName}/${entryId}/${file}`, base).href;
|
|
773
|
+
}
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/schema/generate-schema.ts
|
|
776
|
+
/**
|
|
777
|
+
* Basic schema for every PocketBase collection.
|
|
778
|
+
*/
|
|
779
|
+
const BASIC_SCHEMA = z.object({
|
|
780
|
+
id: z.string().meta({
|
|
781
|
+
title: "id",
|
|
782
|
+
description: "The unique identifier for the entry."
|
|
783
|
+
}),
|
|
784
|
+
collectionId: z.string().meta({
|
|
785
|
+
title: "collectionId",
|
|
786
|
+
description: "The unique identifier for the collection the entity belongs to."
|
|
787
|
+
}),
|
|
788
|
+
collectionName: z.string().meta({
|
|
789
|
+
title: "collectionName",
|
|
790
|
+
description: "The name of the collection the entity belongs to."
|
|
791
|
+
})
|
|
792
|
+
});
|
|
793
|
+
/**
|
|
794
|
+
* Types of fields that can be used as an ID.
|
|
795
|
+
*/
|
|
796
|
+
const VALID_ID_TYPES = [
|
|
797
|
+
"text",
|
|
798
|
+
"number",
|
|
799
|
+
"email",
|
|
800
|
+
"url",
|
|
801
|
+
"date"
|
|
802
|
+
];
|
|
803
|
+
/**
|
|
804
|
+
* Generate a schema for the collection based on the collection's schema in PocketBase.
|
|
805
|
+
* By default, a basic schema is returned if no other schema is available.
|
|
806
|
+
* If superuser credentials are provided, the schema is fetched from the PocketBase API.
|
|
807
|
+
* If a path to a local schema file is provided, the schema is read from the file.
|
|
808
|
+
*
|
|
809
|
+
* @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
|
|
810
|
+
* @param token The superuser token to authenticate the request.
|
|
811
|
+
*/
|
|
812
|
+
async function generateSchema(options, token) {
|
|
813
|
+
let collection;
|
|
814
|
+
if (token) collection = await getRemoteSchema(options, token);
|
|
815
|
+
const hasSuperuserRights = !!collection || !!options.superuserCredentials;
|
|
816
|
+
if (!collection && options.localSchema) collection = await readLocalSchema(options.localSchema, options.collectionName);
|
|
817
|
+
if (!collection) {
|
|
818
|
+
console.error(`No schema available for "${options.collectionName}". Only basic types are available. Please check your configuration and provide a valid schema file or superuser credentials.`);
|
|
819
|
+
return BASIC_SCHEMA;
|
|
820
|
+
}
|
|
821
|
+
const fieldsToInclude = combineFieldsForRequest(extractFieldNames(formatFields(options.fields)), options);
|
|
822
|
+
const fields = parseSchema(collection, options.jsonSchemas, {
|
|
823
|
+
hasSuperuserRights,
|
|
824
|
+
fieldsToInclude,
|
|
825
|
+
experimentalLiveTypesOnly: options.experimental?.liveTypesOnly
|
|
826
|
+
});
|
|
827
|
+
checkCustomIdField(collection, options);
|
|
828
|
+
checkContentField(fields, options);
|
|
829
|
+
checkUpdatedField(fields, collection, options);
|
|
830
|
+
const schema = z.object({
|
|
831
|
+
...BASIC_SCHEMA.shape,
|
|
832
|
+
...fields
|
|
833
|
+
});
|
|
834
|
+
const fileFields = collection.fields.filter((field) => field.type === "file").filter((field) => !field.hidden || hasSuperuserRights);
|
|
835
|
+
if (fileFields.length === 0) return schema;
|
|
836
|
+
return schema.transform((entry) => transformFiles(options.url, fileFields, entry));
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Check if the custom id field is present
|
|
840
|
+
*/
|
|
841
|
+
function checkCustomIdField(collection, options) {
|
|
842
|
+
if (!options.idField) return;
|
|
843
|
+
const idField = collection.fields.find((field) => field.name === options.idField);
|
|
844
|
+
if (!idField) console.error(`The id field "${options.idField}" is not present in the schema of the collection "${options.collectionName}".`);
|
|
845
|
+
else if (!VALID_ID_TYPES.includes(idField.type)) console.error(`The id field "${options.idField}" for collection "${options.collectionName}" is of type "${idField.type}" which is not recommended. Please use one of the following types: ${VALID_ID_TYPES.join(", ")}.`);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Check if the content field(s) are present
|
|
849
|
+
*/
|
|
850
|
+
function checkContentField(fields, options) {
|
|
851
|
+
if (typeof options.contentFields === "string" && !fields[options.contentFields]) console.error(`The content field "${options.contentFields}" is not present in the schema of the collection "${options.collectionName}".`);
|
|
852
|
+
else if (Array.isArray(options.contentFields)) {
|
|
853
|
+
for (const field of options.contentFields) if (!fields[field]) console.error(`The content field "${field}" is not present in the schema of the collection "${options.collectionName}".`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Check if the updated field is present
|
|
858
|
+
*/
|
|
859
|
+
function checkUpdatedField(fields, collection, options) {
|
|
860
|
+
if (!options.updatedField) return;
|
|
861
|
+
if (!fields[options.updatedField]) console.error(`The field "${options.updatedField}" is not present in the schema of the collection "${options.collectionName}".\nThis will lead to errors when trying to fetch only updated entries.`);
|
|
862
|
+
else {
|
|
863
|
+
const updatedField = collection.fields.find((field) => field.name === options.updatedField);
|
|
864
|
+
if (updatedField?.type !== "autodate" || !updatedField.onUpdate) console.warn(`The field "${options.updatedField}" is not of type "autodate" with the value "Update" or "Create/Update".\nMake sure that the field is automatically updated when the entry is updated!`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
//#endregion
|
|
868
|
+
//#region src/schema/generate-type.ts
|
|
869
|
+
/**
|
|
870
|
+
* Generate a TypeScript type from a Zod schema.
|
|
871
|
+
*/
|
|
872
|
+
function generateType(schema) {
|
|
873
|
+
const { node } = zodToTs(schema.type === "pipe" ? schema.in : schema, { auxiliaryTypeStore: createAuxiliaryTypeStore() });
|
|
874
|
+
return `export ${printNode(createTypeAlias(node, "Entry"))}`;
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/utils/get-superuser-token.ts
|
|
878
|
+
/**
|
|
879
|
+
* This function will get a superuser token from the given PocketBase instance.
|
|
880
|
+
*
|
|
881
|
+
* @param url URL of the PocketBase instance
|
|
882
|
+
* @param superuserCredentials Credentials of the superuser
|
|
883
|
+
*
|
|
884
|
+
* @returns A superuser token to access all resources of the PocketBase instance.
|
|
885
|
+
*/
|
|
886
|
+
async function getSuperuserToken(url, superuserCredentials, logger) {
|
|
887
|
+
const loginUrl = new URL(`api/collections/_superusers/auth-with-password`, url).href;
|
|
888
|
+
const loginData = new FormData();
|
|
889
|
+
loginData.set("identity", superuserCredentials.email);
|
|
890
|
+
loginData.set("password", superuserCredentials.password);
|
|
891
|
+
const loginRequest = await fetch(loginUrl, {
|
|
892
|
+
method: "POST",
|
|
893
|
+
body: loginData
|
|
894
|
+
});
|
|
895
|
+
if (!loginRequest.ok) {
|
|
896
|
+
if (loginRequest.status === 429) {
|
|
897
|
+
const info = "A rate limit was hit while trying to authenticate with PocketBase. Consider using an `impersonateToken` as credentials to avoid this issue.";
|
|
898
|
+
if (logger) logger.info(info);
|
|
899
|
+
else console.info(info);
|
|
900
|
+
const retryAfter = Math.random() * 5 + 3;
|
|
901
|
+
await new Promise((resolve) => {
|
|
902
|
+
setTimeout(resolve, retryAfter * 1e3);
|
|
903
|
+
});
|
|
904
|
+
return getSuperuserToken(url, superuserCredentials, logger);
|
|
905
|
+
}
|
|
906
|
+
const errorMessage = `The given email / password for ${url} was not correct. Astro can't generate type definitions automatically and may not have access to all resources.\nReason: ${pocketBaseErrorResponse.parse(await loginRequest.json()).message}`;
|
|
907
|
+
if (logger) logger.error(errorMessage);
|
|
908
|
+
else console.error(errorMessage);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
return pocketBaseLoginResponse.parse(await loginRequest.json()).token;
|
|
912
|
+
}
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/utils/create-token-promise.ts
|
|
915
|
+
/**
|
|
916
|
+
* Creates a promise that resolves to a superuser token or undefined.
|
|
917
|
+
*/
|
|
918
|
+
async function createTokenPromise(options) {
|
|
919
|
+
if (options.superuserCredentials) {
|
|
920
|
+
if ("impersonateToken" in options.superuserCredentials) return options.superuserCredentials.impersonateToken;
|
|
921
|
+
return await getSuperuserToken(options.url, options.superuserCredentials);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/pocketbase-loader.ts
|
|
926
|
+
/**
|
|
927
|
+
* Loader for collections stored in PocketBase.
|
|
928
|
+
*
|
|
929
|
+
* @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
|
|
930
|
+
*/
|
|
931
|
+
function pocketbaseLoader(options) {
|
|
932
|
+
const tokenPromise = createTokenPromise(options);
|
|
933
|
+
return {
|
|
934
|
+
name: "pocketbase-loader",
|
|
935
|
+
load: async (context) => {
|
|
936
|
+
if (options.experimental?.liveTypesOnly) {
|
|
937
|
+
context.logger.label = `pocketbase-loader:${options.collectionName}`;
|
|
938
|
+
context.logger.info("Experimental live types only mode enabled. No data will be loaded, only types will be generated.");
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
await loader(context, options, await tokenPromise);
|
|
942
|
+
},
|
|
943
|
+
createSchema: async () => {
|
|
944
|
+
const schema = await generateSchema(options, await tokenPromise);
|
|
945
|
+
return {
|
|
946
|
+
schema,
|
|
947
|
+
types: generateType(schema)
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Live loader for collections stored in PocketBase.
|
|
954
|
+
*
|
|
955
|
+
* @param options Options for the live loader. See {@link PocketBaseLiveLoaderOptions} for more details.
|
|
956
|
+
*/
|
|
957
|
+
function pocketbaseLiveLoader(options) {
|
|
958
|
+
const tokenPromise = createTokenPromise(options);
|
|
959
|
+
return {
|
|
960
|
+
name: "pocketbase-live-loader",
|
|
961
|
+
loadCollection: async ({ filter }) => {
|
|
962
|
+
return liveCollectionLoader(filter, options, await tokenPromise);
|
|
963
|
+
},
|
|
964
|
+
loadEntry: async ({ filter }) => {
|
|
965
|
+
const token = await tokenPromise;
|
|
966
|
+
return liveEntryLoader(filter.id, options, token);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
//#endregion
|
|
971
|
+
export { PocketBaseAuthenticationError, pocketbaseLiveLoader, pocketbaseLoader, transformFileUrl };
|
|
972
|
+
|
|
973
|
+
//# sourceMappingURL=index.mjs.map
|