emdash 0.11.1 → 0.12.0

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 (75) hide show
  1. package/dist/{apply-Ded_1vng.mjs → apply-C1ZORgcy.mjs} +6 -226
  2. package/dist/apply-C1ZORgcy.mjs.map +1 -0
  3. package/dist/astro/index.d.mts +3 -3
  4. package/dist/astro/index.mjs +1 -1
  5. package/dist/astro/middleware/auth.d.mts +3 -3
  6. package/dist/astro/middleware/auth.mjs +1 -1
  7. package/dist/astro/middleware.mjs +16 -12
  8. package/dist/astro/middleware.mjs.map +1 -1
  9. package/dist/astro/types.d.mts +3 -4
  10. package/dist/astro/types.d.mts.map +1 -1
  11. package/dist/cli/index.mjs +4 -4
  12. package/dist/{error-DqnRMM5z.mjs → error-D6LuHLw9.mjs} +1 -1
  13. package/dist/{error-DqnRMM5z.mjs.map → error-D6LuHLw9.mjs.map} +1 -1
  14. package/dist/{index-BogfvE-z.d.mts → index-Dlkzhb4C.d.mts} +5 -5
  15. package/dist/index-Dlkzhb4C.d.mts.map +1 -0
  16. package/dist/index.d.mts +4 -4
  17. package/dist/index.mjs +9 -9
  18. package/dist/{manifest-schema-CXAbd1vH.mjs → manifest-schema-Bp6d4d4n.mjs} +1 -1
  19. package/dist/{manifest-schema-CXAbd1vH.mjs.map → manifest-schema-Bp6d4d4n.mjs.map} +1 -1
  20. package/dist/media/local-runtime.d.mts +3 -3
  21. package/dist/media/local-runtime.d.mts.map +1 -1
  22. package/dist/media/local-runtime.mjs +6 -1
  23. package/dist/media/local-runtime.mjs.map +1 -1
  24. package/dist/page/index.d.mts +15 -4
  25. package/dist/page/index.d.mts.map +1 -1
  26. package/dist/page/index.mjs +16 -5
  27. package/dist/page/index.mjs.map +1 -1
  28. package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
  29. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  30. package/dist/{query-8c_meo_K.mjs → query-yA3-rFji.mjs} +13 -2
  31. package/dist/query-yA3-rFji.mjs.map +1 -0
  32. package/dist/runtime.d.mts +3 -3
  33. package/dist/{search-DuWhx4NG.mjs → search-n-ZCMfr3.mjs} +33 -16
  34. package/dist/search-n-ZCMfr3.mjs.map +1 -0
  35. package/dist/seed/index.d.mts +1 -1
  36. package/dist/seed/index.mjs +2 -2
  37. package/dist/settings-nTXPRi3D.mjs +440 -0
  38. package/dist/settings-nTXPRi3D.mjs.map +1 -0
  39. package/dist/storage/local.mjs +1 -1
  40. package/dist/storage/s3.mjs +1 -1
  41. package/dist/{taxonomies-Bw76xAxo.mjs → taxonomies-JmQQZiG1.mjs} +2 -2
  42. package/dist/{taxonomies-Bw76xAxo.mjs.map → taxonomies-JmQQZiG1.mjs.map} +1 -1
  43. package/dist/{types-BTe41zL6.d.mts → types-B1gLSAH2.d.mts} +13 -9
  44. package/dist/{types-BTe41zL6.d.mts.map → types-B1gLSAH2.d.mts.map} +1 -1
  45. package/dist/{types-DiI8NOG_.mjs → types-Cug_RO3W.mjs} +1 -1
  46. package/dist/{types-DiI8NOG_.mjs.map → types-Cug_RO3W.mjs.map} +1 -1
  47. package/dist/{types-IjUrQMVe.d.mts → types-DgSc9Rpc.d.mts} +149 -4
  48. package/dist/types-DgSc9Rpc.d.mts.map +1 -0
  49. package/dist/{types-K-EkEQCI.mjs → types-PafqtQuM.mjs} +1 -1
  50. package/dist/{types-K-EkEQCI.mjs.map → types-PafqtQuM.mjs.map} +1 -1
  51. package/dist/{validate-CcVQQpmH.d.mts → validate-BcC3m2O7.d.mts} +2 -2
  52. package/dist/{validate-CcVQQpmH.d.mts.map → validate-BcC3m2O7.d.mts.map} +1 -1
  53. package/dist/version-BdP--J1g.mjs +7 -0
  54. package/dist/{version-JjSqv90m.mjs.map → version-BdP--J1g.mjs.map} +1 -1
  55. package/package.json +6 -6
  56. package/src/api/schemas/settings.ts +41 -9
  57. package/src/astro/routes/api/media/[id].ts +2 -1
  58. package/src/components/EmDashHead.astro +26 -5
  59. package/src/emdash-runtime.ts +21 -2
  60. package/src/media/local-runtime.ts +7 -0
  61. package/src/page/absolute-url.ts +146 -0
  62. package/src/page/jsonld.ts +10 -2
  63. package/src/page/seo-contributions.ts +17 -6
  64. package/src/plugins/context.ts +11 -1
  65. package/src/query.ts +12 -0
  66. package/src/settings/index.ts +20 -1
  67. package/src/settings/types.ts +12 -8
  68. package/dist/apply-Ded_1vng.mjs.map +0 -1
  69. package/dist/index-BogfvE-z.d.mts.map +0 -1
  70. package/dist/media-1fFhub9c.mjs +0 -209
  71. package/dist/media-1fFhub9c.mjs.map +0 -1
  72. package/dist/query-8c_meo_K.mjs.map +0 -1
  73. package/dist/search-DuWhx4NG.mjs.map +0 -1
  74. package/dist/types-IjUrQMVe.d.mts.map +0 -1
  75. package/dist/version-JjSqv90m.mjs +0 -7
@@ -1,3 +1,3 @@
1
1
  import "../types-BQx6ZXpR.mjs";
2
- import { _ as SeedTaxonomyTerm, a as applySeed, b as ValidationResult, c as SeedCollection, d as SeedFile, f as SeedMenu, g as SeedTaxonomy, h as SeedSection, i as defaultSeed, l as SeedContentEntry, m as SeedRedirect, n as loadSeed, o as SeedApplyOptions, p as SeedMenuItem, r as loadUserSeed, s as SeedApplyResult, t as validateSeed, u as SeedField, v as SeedWidget, y as SeedWidgetArea } from "../validate-CcVQQpmH.mjs";
2
+ import { _ as SeedTaxonomyTerm, a as applySeed, b as ValidationResult, c as SeedCollection, d as SeedFile, f as SeedMenu, g as SeedTaxonomy, h as SeedSection, i as defaultSeed, l as SeedContentEntry, m as SeedRedirect, n as loadSeed, o as SeedApplyOptions, p as SeedMenuItem, r as loadUserSeed, s as SeedApplyResult, t as validateSeed, u as SeedField, v as SeedWidget, y as SeedWidgetArea } from "../validate-BcC3m2O7.mjs";
3
3
  export { type SeedApplyOptions, type SeedApplyResult, type SeedCollection, type SeedContentEntry, type SeedField, type SeedFile, type SeedMenu, type SeedMenuItem, type SeedRedirect, type SeedSection, type SeedTaxonomy, type SeedTaxonomyTerm, type SeedWidget, type SeedWidgetArea, type ValidationResult, applySeed, defaultSeed, loadSeed, loadUserSeed, validateSeed };
@@ -2,7 +2,7 @@ import "../dialect-helpers-BKCvISIQ.mjs";
2
2
  import "../content-CERxPUN0.mjs";
3
3
  import "../base64-MBPo9ozB.mjs";
4
4
  import "../types-BIgulNsW.mjs";
5
- import "../media-1fFhub9c.mjs";
5
+ import "../settings-nTXPRi3D.mjs";
6
6
  import "../taxonomy-D6NvlKo8.mjs";
7
7
  import "../options-nPxWnrya.mjs";
8
8
  import "../redirect-C5H7VGIX.mjs";
@@ -10,7 +10,7 @@ import "../byline-gFn1r0vA.mjs";
10
10
  import "../request-cache-D4I69LeL.mjs";
11
11
  import "../registry-Do34mz_P.mjs";
12
12
  import "../loader-ou_PXAjg.mjs";
13
- import { t as applySeed } from "../apply-Ded_1vng.mjs";
13
+ import { t as applySeed } from "../apply-C1ZORgcy.mjs";
14
14
  import { t as validateSeed } from "../validate-UK4Ja1uo.mjs";
15
15
  import { t as defaultSeed } from "../default-pHuz9WF6.mjs";
16
16
  import { n as loadUserSeed, t as loadSeed } from "../load-DR1VwFXR.mjs";
@@ -0,0 +1,440 @@
1
+ import { i as encodeCursor, r as decodeCursor } from "./types-BIgulNsW.mjs";
2
+ import { t as OptionsRepository } from "./options-nPxWnrya.mjs";
3
+ import { n as requestCached, t as peekRequestCache } from "./request-cache-D4I69LeL.mjs";
4
+ import { r as getDb } from "./loader-ou_PXAjg.mjs";
5
+ import { sql } from "kysely";
6
+ import { ulid } from "ulidx";
7
+
8
+ //#region src/database/repositories/media.ts
9
+ /** Escape LIKE wildcard characters and the escape char itself in user-supplied values */
10
+ function escapeLike(value) {
11
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
12
+ }
13
+ /**
14
+ * Normalize a mimeType filter (string or array) into a clean string[].
15
+ * Entries that are empty strings are dropped.
16
+ */
17
+ function normalizeMimeFilter(input) {
18
+ if (!input) return [];
19
+ return (Array.isArray(input) ? input : [input]).filter((entry) => typeof entry === "string" && entry.length > 0).map((entry) => entry.endsWith("/") ? entry.toLowerCase() : entry.split(";")[0].trim().toLowerCase());
20
+ }
21
+ /**
22
+ * Build a WHERE clause that matches `mime_type` against any of the given
23
+ * filter entries — exact equality for full MIMEs, LIKE prefix for entries
24
+ * ending in "/".
25
+ */
26
+ function mimeMatchExpr(eb, filters) {
27
+ return eb.or(filters.map((entry) => entry.endsWith("/") ? sql`mime_type LIKE ${`${escapeLike(entry)}%`} ESCAPE '\\'` : eb("mime_type", "=", entry)));
28
+ }
29
+ /**
30
+ * Media repository for database operations
31
+ */
32
+ var MediaRepository = class {
33
+ constructor(db) {
34
+ this.db = db;
35
+ }
36
+ /**
37
+ * Create a new media item
38
+ */
39
+ async create(input) {
40
+ const id = ulid();
41
+ const now = (/* @__PURE__ */ new Date()).toISOString();
42
+ const row = {
43
+ id,
44
+ filename: input.filename,
45
+ mime_type: input.mimeType,
46
+ size: input.size ?? null,
47
+ width: input.width ?? null,
48
+ height: input.height ?? null,
49
+ alt: input.alt ?? null,
50
+ caption: input.caption ?? null,
51
+ storage_key: input.storageKey,
52
+ content_hash: input.contentHash ?? null,
53
+ blurhash: input.blurhash ?? null,
54
+ dominant_color: input.dominantColor ?? null,
55
+ status: input.status ?? "ready",
56
+ created_at: now,
57
+ author_id: input.authorId ?? null
58
+ };
59
+ await this.db.insertInto("media").values(row).execute();
60
+ return this.rowToItem(row);
61
+ }
62
+ /**
63
+ * Create a pending media item (for signed URL upload flow)
64
+ */
65
+ async createPending(input) {
66
+ return this.create({
67
+ ...input,
68
+ status: "pending"
69
+ });
70
+ }
71
+ /**
72
+ * Confirm upload (mark as ready)
73
+ */
74
+ async confirmUpload(id, metadata) {
75
+ if (!await this.findById(id)) return null;
76
+ const updates = { status: "ready" };
77
+ if (metadata?.width !== void 0) updates.width = metadata.width;
78
+ if (metadata?.height !== void 0) updates.height = metadata.height;
79
+ if (metadata?.size !== void 0) updates.size = metadata.size;
80
+ await this.db.updateTable("media").set(updates).where("id", "=", id).execute();
81
+ return this.findById(id);
82
+ }
83
+ /**
84
+ * Mark upload as failed
85
+ */
86
+ async markFailed(id) {
87
+ if (!await this.findById(id)) return null;
88
+ await this.db.updateTable("media").set({ status: "failed" }).where("id", "=", id).execute();
89
+ return this.findById(id);
90
+ }
91
+ /**
92
+ * Find media by ID
93
+ */
94
+ async findById(id) {
95
+ const row = await this.db.selectFrom("media").selectAll().where("id", "=", id).executeTakeFirst();
96
+ return row ? this.rowToItem(row) : null;
97
+ }
98
+ /**
99
+ * Find media by filename
100
+ * Useful for idempotent imports
101
+ */
102
+ async findByFilename(filename) {
103
+ const row = await this.db.selectFrom("media").selectAll().where("filename", "=", filename).executeTakeFirst();
104
+ return row ? this.rowToItem(row) : null;
105
+ }
106
+ /**
107
+ * Find media by content hash
108
+ * Used for deduplication - same content = same hash
109
+ */
110
+ async findByContentHash(contentHash) {
111
+ const row = await this.db.selectFrom("media").selectAll().where("content_hash", "=", contentHash).where("status", "=", "ready").executeTakeFirst();
112
+ return row ? this.rowToItem(row) : null;
113
+ }
114
+ /**
115
+ * Find many media items with cursor pagination
116
+ *
117
+ * Uses keyset pagination (cursor-based) for consistent results.
118
+ * The cursor encodes the created_at and id of the last item.
119
+ */
120
+ async findMany(options = {}) {
121
+ const limit = Math.min(options.limit || 50, 100);
122
+ let query = this.db.selectFrom("media").selectAll().orderBy("created_at", "desc").orderBy("id", "desc").limit(limit + 1);
123
+ if (options.cursor) {
124
+ const { orderValue: createdAt, id: cursorId } = decodeCursor(options.cursor);
125
+ query = query.where((eb) => eb.or([eb("created_at", "<", createdAt), eb.and([eb("created_at", "=", createdAt), eb("id", "<", cursorId)])]));
126
+ }
127
+ const mimeFilters = normalizeMimeFilter(options.mimeType);
128
+ if (mimeFilters.length > 0) query = query.where((eb) => mimeMatchExpr(eb, mimeFilters));
129
+ if (options.status !== "all") query = query.where("status", "=", options.status ?? "ready");
130
+ const rows = await query.execute();
131
+ const hasMore = rows.length > limit;
132
+ const items = rows.slice(0, limit).map((row) => this.rowToItem(row));
133
+ let nextCursor;
134
+ if (hasMore && items.length > 0) {
135
+ const lastItem = items.at(-1);
136
+ nextCursor = encodeCursor(lastItem.createdAt, lastItem.id);
137
+ }
138
+ return {
139
+ items,
140
+ nextCursor
141
+ };
142
+ }
143
+ /**
144
+ * Update media metadata
145
+ */
146
+ async update(id, input) {
147
+ if (!await this.findById(id)) return null;
148
+ const updates = {};
149
+ if (input.alt !== void 0) updates.alt = input.alt;
150
+ if (input.caption !== void 0) updates.caption = input.caption;
151
+ if (input.width !== void 0) updates.width = input.width;
152
+ if (input.height !== void 0) updates.height = input.height;
153
+ if (Object.keys(updates).length > 0) await this.db.updateTable("media").set(updates).where("id", "=", id).execute();
154
+ return this.findById(id);
155
+ }
156
+ /**
157
+ * Delete media item
158
+ */
159
+ async delete(id) {
160
+ return ((await this.db.deleteFrom("media").where("id", "=", id).executeTakeFirst()).numDeletedRows ?? 0) > 0;
161
+ }
162
+ /**
163
+ * Count media items
164
+ */
165
+ async count(mimeType) {
166
+ const filters = normalizeMimeFilter(mimeType);
167
+ let query = this.db.selectFrom("media").select((eb) => eb.fn.count("id").as("count"));
168
+ if (filters.length > 0) query = query.where((eb) => mimeMatchExpr(eb, filters));
169
+ const result = await query.executeTakeFirst();
170
+ return Number(result?.count || 0);
171
+ }
172
+ /**
173
+ * Delete pending uploads older than the given age.
174
+ * Pending uploads that were never confirmed indicate abandoned upload flows.
175
+ *
176
+ * Returns the storage keys of deleted rows so callers can remove the
177
+ * corresponding files from object storage.
178
+ */
179
+ async cleanupPendingUploads(maxAgeMs = 3600 * 1e3) {
180
+ const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
181
+ const rows = await this.db.selectFrom("media").select("storage_key").where("status", "=", "pending").where("created_at", "<", cutoff).execute();
182
+ if (rows.length === 0) return [];
183
+ await this.db.deleteFrom("media").where("status", "=", "pending").where("created_at", "<", cutoff).execute();
184
+ return rows.map((r) => r.storage_key);
185
+ }
186
+ /**
187
+ * Convert database row to MediaItem
188
+ */
189
+ rowToItem(row) {
190
+ return {
191
+ id: row.id,
192
+ filename: row.filename,
193
+ mimeType: row.mime_type,
194
+ size: row.size,
195
+ width: row.width,
196
+ height: row.height,
197
+ alt: row.alt,
198
+ caption: row.caption,
199
+ storageKey: row.storage_key,
200
+ contentHash: row.content_hash,
201
+ blurhash: row.blurhash,
202
+ dominantColor: row.dominant_color,
203
+ status: row.status,
204
+ createdAt: row.created_at,
205
+ authorId: row.author_id
206
+ };
207
+ }
208
+ };
209
+
210
+ //#endregion
211
+ //#region src/settings/index.ts
212
+ /** Prefix for site settings in the options table */
213
+ const SETTINGS_PREFIX = "site:";
214
+ const SITE_SETTINGS_CACHE_KEY = Symbol.for("emdash:site-settings");
215
+ const g = globalThis;
216
+ const holder = g[SITE_SETTINGS_CACHE_KEY] ?? (() => {
217
+ const h = {
218
+ version: 0,
219
+ cached: null,
220
+ cachedVersion: -1
221
+ };
222
+ g[SITE_SETTINGS_CACHE_KEY] = h;
223
+ return h;
224
+ })();
225
+ /**
226
+ * Bump the isolate-wide site-settings cache version, forcing the next
227
+ * `getSiteSettings()` to re-query the database.
228
+ *
229
+ * Called from every `site:*` write path. Other isolates still serve their
230
+ * own cached copy until they expire — staleness bounded by isolate lifetime.
231
+ */
232
+ function invalidateSiteSettingsCache() {
233
+ holder.version++;
234
+ holder.cached = null;
235
+ holder.cachedVersion = -1;
236
+ }
237
+ /**
238
+ * Type guard for MediaReference values
239
+ */
240
+ function isMediaReference(value) {
241
+ return typeof value === "object" && value !== null && "mediaId" in value;
242
+ }
243
+ /**
244
+ * Resolve a media reference to include the full URL plus content metadata.
245
+ *
246
+ * Pulls `mimeType` and intrinsic dimensions from the media row so callers
247
+ * can emit correct head tags (e.g. `<link rel="icon" type="image/svg+xml">`,
248
+ * which Chromium requires when the URL has no `.svg` extension) without
249
+ * a second round-trip to the media table.
250
+ */
251
+ async function resolveMediaReference(mediaRef, db, _storage) {
252
+ if (!mediaRef?.mediaId) return mediaRef;
253
+ try {
254
+ const media = await new MediaRepository(db).findById(mediaRef.mediaId);
255
+ if (media) return {
256
+ ...mediaRef,
257
+ url: `/_emdash/api/media/file/${media.storageKey}`,
258
+ contentType: media.mimeType,
259
+ ...media.width !== null ? { width: media.width } : {},
260
+ ...media.height !== null ? { height: media.height } : {}
261
+ };
262
+ } catch {}
263
+ return mediaRef;
264
+ }
265
+ /**
266
+ * Get a single site setting by key
267
+ *
268
+ * Returns `undefined` if the setting has not been configured.
269
+ * For media settings (logo, favicon), the URL is resolved automatically.
270
+ *
271
+ * @param key - The setting key (e.g., "title", "logo", "social")
272
+ * @returns The setting value, or undefined if not set
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * import { getSiteSetting } from "emdash";
277
+ *
278
+ * const title = await getSiteSetting("title");
279
+ * const logo = await getSiteSetting("logo");
280
+ * console.log(logo?.url); // Resolved URL
281
+ * ```
282
+ */
283
+ async function getSiteSetting(key) {
284
+ const primed = peekRequestCache("siteSettings");
285
+ if (primed) return (await primed)[key];
286
+ return requestCached(`siteSetting:${key}`, async () => {
287
+ return getSiteSettingWithDb(key, await getDb());
288
+ });
289
+ }
290
+ /**
291
+ * Get a single site setting by key (with explicit db)
292
+ *
293
+ * @internal Use `getSiteSetting()` in templates. This variant is for admin routes
294
+ * that already have a database handle.
295
+ */
296
+ async function getSiteSettingWithDb(key, db, storage = null) {
297
+ const value = await new OptionsRepository(db).get(`${SETTINGS_PREFIX}${key}`);
298
+ if (!value) return;
299
+ if ((key === "logo" || key === "favicon") && isMediaReference(value)) return await resolveMediaReference(value, db, storage);
300
+ if (key === "seo" && value && typeof value === "object") {
301
+ const seo = value;
302
+ if (seo.defaultOgImage) return {
303
+ ...seo,
304
+ defaultOgImage: await resolveMediaReference(seo.defaultOgImage, db, storage)
305
+ };
306
+ }
307
+ return value;
308
+ }
309
+ /**
310
+ * Get all site settings
311
+ *
312
+ * Returns all configured settings. Unset values are undefined.
313
+ * Media references (logo/favicon) are resolved to include URLs.
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * import { getSiteSettings } from "emdash";
318
+ *
319
+ * const settings = await getSiteSettings();
320
+ * console.log(settings.title); // "My Site"
321
+ * console.log(settings.logo?.url); // "/_emdash/api/media/file/abc123"
322
+ * ```
323
+ */
324
+ function getSiteSettings() {
325
+ return requestCached("siteSettings", () => {
326
+ const versionAtCall = holder.version;
327
+ if (holder.cached && holder.cachedVersion === versionAtCall) return holder.cached;
328
+ const fetchPromise = (async () => {
329
+ return getSiteSettingsWithDb(await getDb());
330
+ })().catch((error) => {
331
+ if (holder.cached === fetchPromise) {
332
+ holder.cached = null;
333
+ holder.cachedVersion = -1;
334
+ }
335
+ throw error;
336
+ });
337
+ holder.cached = fetchPromise;
338
+ holder.cachedVersion = versionAtCall;
339
+ return fetchPromise;
340
+ });
341
+ }
342
+ /**
343
+ * Get all site settings (with explicit db)
344
+ *
345
+ * @internal Use `getSiteSettings()` in templates. This variant is for admin routes
346
+ * that already have a database handle.
347
+ */
348
+ async function getSiteSettingsWithDb(db, storage = null) {
349
+ const allOptions = await new OptionsRepository(db).getByPrefix(SETTINGS_PREFIX);
350
+ const settings = {};
351
+ for (const [key, value] of allOptions) {
352
+ const settingKey = key.replace(SETTINGS_PREFIX, "");
353
+ settings[settingKey] = value;
354
+ }
355
+ const typedSettings = settings;
356
+ if (typedSettings.logo) typedSettings.logo = await resolveMediaReference(typedSettings.logo, db, storage);
357
+ if (typedSettings.favicon) typedSettings.favicon = await resolveMediaReference(typedSettings.favicon, db, storage);
358
+ if (typedSettings.seo?.defaultOgImage) typedSettings.seo = {
359
+ ...typedSettings.seo,
360
+ defaultOgImage: await resolveMediaReference(typedSettings.seo.defaultOgImage, db, storage)
361
+ };
362
+ return typedSettings;
363
+ }
364
+ /**
365
+ * Set site settings (internal function used by admin API)
366
+ *
367
+ * Merges provided settings with existing ones. Only provided fields are updated.
368
+ * Media references should include just the mediaId; URLs are resolved on read.
369
+ *
370
+ * @param settings - Partial settings object with values to update
371
+ * @param db - Kysely database instance
372
+ * @returns Promise that resolves when settings are saved
373
+ *
374
+ * @internal
375
+ *
376
+ * @example
377
+ * ```ts
378
+ * // Update multiple settings at once
379
+ * await setSiteSettings({
380
+ * title: "My Site",
381
+ * tagline: "Welcome",
382
+ * logo: { mediaId: "med_123", alt: "Logo" }
383
+ * }, db);
384
+ * ```
385
+ */
386
+ async function setSiteSettings(settings, db) {
387
+ const options = new OptionsRepository(db);
388
+ const updates = {};
389
+ for (const [key, value] of Object.entries(settings)) if (value !== void 0) updates[`${SETTINGS_PREFIX}${key}`] = value;
390
+ try {
391
+ await options.setMany(updates);
392
+ } finally {
393
+ invalidateSiteSettingsCache();
394
+ }
395
+ }
396
+ /**
397
+ * Get a single plugin setting by key.
398
+ *
399
+ * Plugin settings are stored in the options table under
400
+ * `plugin:<pluginId>:settings:<key>`.
401
+ */
402
+ async function getPluginSetting(pluginId, key) {
403
+ return getPluginSettingWithDb(pluginId, key, await getDb());
404
+ }
405
+ /**
406
+ * Get a single plugin setting by key (with explicit db).
407
+ *
408
+ * @internal Use `getPluginSetting()` in templates and plugin rendering code.
409
+ */
410
+ async function getPluginSettingWithDb(pluginId, key, db) {
411
+ return await new OptionsRepository(db).get(`plugin:${pluginId}:settings:${key}`) ?? void 0;
412
+ }
413
+ /**
414
+ * Get all persisted plugin settings for a plugin.
415
+ *
416
+ * Defaults declared in `admin.settingsSchema` are not materialized
417
+ * automatically; callers should apply their own fallback defaults.
418
+ */
419
+ async function getPluginSettings(pluginId) {
420
+ return getPluginSettingsWithDb(pluginId, await getDb());
421
+ }
422
+ /**
423
+ * Get all persisted plugin settings for a plugin (with explicit db).
424
+ *
425
+ * @internal Use `getPluginSettings()` in templates and plugin rendering code.
426
+ */
427
+ async function getPluginSettingsWithDb(pluginId, db) {
428
+ const prefix = `plugin:${pluginId}:settings:`;
429
+ const allOptions = await new OptionsRepository(db).getByPrefix(prefix);
430
+ const settings = {};
431
+ for (const [key, value] of allOptions) {
432
+ if (!key.startsWith(prefix)) continue;
433
+ settings[key.slice(prefix.length)] = value;
434
+ }
435
+ return settings;
436
+ }
437
+
438
+ //#endregion
439
+ export { invalidateSiteSettingsCache as a, getSiteSettings as i, getPluginSettings as n, setSiteSettings as o, getSiteSetting as r, MediaRepository as s, getPluginSetting as t };
440
+ //# sourceMappingURL=settings-nTXPRi3D.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-nTXPRi3D.mjs","names":[],"sources":["../src/database/repositories/media.ts","../src/settings/index.ts"],"sourcesContent":["import { sql, type ExpressionBuilder, type Kysely, type SqlBool } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database, MediaRow } from \"../types.js\";\nimport type { FindManyResult } from \"./types.js\";\nimport { encodeCursor, decodeCursor } from \"./types.js\";\n\n/** Escape LIKE wildcard characters and the escape char itself in user-supplied values */\nfunction escapeLike(value: string): string {\n\treturn value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"%\", \"\\\\%\").replaceAll(\"_\", \"\\\\_\");\n}\n\n/**\n * Normalize a mimeType filter (string or array) into a clean string[].\n * Entries that are empty strings are dropped.\n */\nfunction normalizeMimeFilter(input?: string | readonly string[]): string[] {\n\tif (!input) return [];\n\tconst arr = Array.isArray(input) ? input : [input];\n\treturn arr\n\t\t.filter((entry): entry is string => typeof entry === \"string\" && entry.length > 0)\n\t\t.map((entry) =>\n\t\t\tentry.endsWith(\"/\") ? entry.toLowerCase() : entry.split(\";\")[0]!.trim().toLowerCase(),\n\t\t);\n}\n\n/**\n * Build a WHERE clause that matches `mime_type` against any of the given\n * filter entries — exact equality for full MIMEs, LIKE prefix for entries\n * ending in \"/\".\n */\nfunction mimeMatchExpr(eb: ExpressionBuilder<Database, \"media\">, filters: string[]) {\n\treturn eb.or(\n\t\tfilters.map((entry) =>\n\t\t\tentry.endsWith(\"/\")\n\t\t\t\t? sql<SqlBool>`mime_type LIKE ${`${escapeLike(entry)}%`} ESCAPE '\\\\'`\n\t\t\t\t: eb(\"mime_type\", \"=\", entry),\n\t\t),\n\t);\n}\n\nexport type MediaStatus = \"pending\" | \"ready\" | \"failed\";\n\nexport interface MediaItem {\n\tid: string;\n\tfilename: string;\n\tmimeType: string;\n\tsize: number | null;\n\twidth: number | null;\n\theight: number | null;\n\talt: string | null;\n\tcaption: string | null;\n\tstorageKey: string;\n\tstatus: MediaStatus;\n\tcontentHash: string | null;\n\tblurhash: string | null;\n\tdominantColor: string | null;\n\tcreatedAt: string;\n\tauthorId: string | null;\n}\n\nexport interface CreateMediaInput {\n\tfilename: string;\n\tmimeType: string;\n\tsize?: number;\n\twidth?: number;\n\theight?: number;\n\talt?: string;\n\tcaption?: string;\n\tstorageKey: string;\n\tcontentHash?: string;\n\tblurhash?: string;\n\tdominantColor?: string;\n\tstatus?: MediaStatus;\n\tauthorId?: string;\n}\n\nexport interface FindManyMediaOptions {\n\tlimit?: number;\n\tcursor?: string;\n\t/** Filter by MIME type. Pass a string for a single prefix/exact, or an array to match any. Strings ending with \"/\" are treated as LIKE prefix matches; others are exact equality. */\n\tmimeType?: string | readonly string[];\n\tstatus?: MediaStatus | \"all\"; // Filter by status, defaults to \"ready\"\n}\n\n/**\n * Media repository for database operations\n */\nexport class MediaRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new media item\n\t */\n\tasync create(input: CreateMediaInput): Promise<MediaItem> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tconst row: Omit<MediaRow, \"rowid\"> = {\n\t\t\tid,\n\t\t\tfilename: input.filename,\n\t\t\tmime_type: input.mimeType,\n\t\t\tsize: input.size ?? null,\n\t\t\twidth: input.width ?? null,\n\t\t\theight: input.height ?? null,\n\t\t\talt: input.alt ?? null,\n\t\t\tcaption: input.caption ?? null,\n\t\t\tstorage_key: input.storageKey,\n\t\t\tcontent_hash: input.contentHash ?? null,\n\t\t\tblurhash: input.blurhash ?? null,\n\t\t\tdominant_color: input.dominantColor ?? null,\n\t\t\tstatus: input.status ?? \"ready\",\n\t\t\tcreated_at: now,\n\t\t\tauthor_id: input.authorId ?? null,\n\t\t};\n\n\t\tawait this.db.insertInto(\"media\").values(row).execute();\n\n\t\treturn this.rowToItem(row as MediaRow);\n\t}\n\n\t/**\n\t * Create a pending media item (for signed URL upload flow)\n\t */\n\tasync createPending(input: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tauthorId?: string;\n\t}): Promise<MediaItem> {\n\t\treturn this.create({\n\t\t\t...input,\n\t\t\tstatus: \"pending\",\n\t\t});\n\t}\n\n\t/**\n\t * Confirm upload (mark as ready)\n\t */\n\tasync confirmUpload(\n\t\tid: string,\n\t\tmetadata?: { width?: number; height?: number; size?: number },\n\t): Promise<MediaItem | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst updates: Partial<MediaRow> = {\n\t\t\tstatus: \"ready\",\n\t\t};\n\t\tif (metadata?.width !== undefined) updates.width = metadata.width;\n\t\tif (metadata?.height !== undefined) updates.height = metadata.height;\n\t\tif (metadata?.size !== undefined) updates.size = metadata.size;\n\n\t\tawait this.db.updateTable(\"media\").set(updates).where(\"id\", \"=\", id).execute();\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Mark upload as failed\n\t */\n\tasync markFailed(id: string): Promise<MediaItem | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) {\n\t\t\treturn null;\n\t\t}\n\n\t\tawait this.db.updateTable(\"media\").set({ status: \"failed\" }).where(\"id\", \"=\", id).execute();\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Find media by ID\n\t */\n\tasync findById(id: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find media by filename\n\t * Useful for idempotent imports\n\t */\n\tasync findByFilename(filename: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"filename\", \"=\", filename)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find media by content hash\n\t * Used for deduplication - same content = same hash\n\t */\n\tasync findByContentHash(contentHash: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"content_hash\", \"=\", contentHash)\n\t\t\t.where(\"status\", \"=\", \"ready\")\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find many media items with cursor pagination\n\t *\n\t * Uses keyset pagination (cursor-based) for consistent results.\n\t * The cursor encodes the created_at and id of the last item.\n\t */\n\tasync findMany(options: FindManyMediaOptions = {}): Promise<FindManyResult<MediaItem>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\t// Handle cursor-based pagination — throws on invalid cursor.\n\t\tif (options.cursor) {\n\t\t\tconst { orderValue: createdAt, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\t// Keyset pagination: get items where (created_at, id) < cursor\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", createdAt),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", createdAt), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst mimeFilters = normalizeMimeFilter(options.mimeType);\n\t\tif (mimeFilters.length > 0) {\n\t\t\tquery = query.where((eb) => mimeMatchExpr(eb, mimeFilters));\n\t\t}\n\n\t\t// Default to only showing ready items\n\t\tif (options.status !== \"all\") {\n\t\t\tquery = query.where(\"status\", \"=\", options.status ?? \"ready\");\n\t\t}\n\n\t\tconst rows = await query.execute();\n\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((row) => this.rowToItem(row));\n\n\t\tlet nextCursor: string | undefined;\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastItem = items.at(-1)!;\n\t\t\tnextCursor = encodeCursor(lastItem.createdAt, lastItem.id);\n\t\t}\n\n\t\treturn { items, nextCursor };\n\t}\n\n\t/**\n\t * Update media metadata\n\t */\n\tasync update(\n\t\tid: string,\n\t\tinput: Partial<Pick<CreateMediaInput, \"alt\" | \"caption\" | \"width\" | \"height\">>,\n\t): Promise<MediaItem | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst updates: Partial<MediaRow> = {};\n\t\tif (input.alt !== undefined) updates.alt = input.alt;\n\t\tif (input.caption !== undefined) updates.caption = input.caption;\n\t\tif (input.width !== undefined) updates.width = input.width;\n\t\tif (input.height !== undefined) updates.height = input.height;\n\n\t\tif (Object.keys(updates).length > 0) {\n\t\t\tawait this.db.updateTable(\"media\").set(updates).where(\"id\", \"=\", id).execute();\n\t\t}\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Delete media item\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db.deleteFrom(\"media\").where(\"id\", \"=\", id).executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Count media items\n\t */\n\tasync count(mimeType?: string | readonly string[]): Promise<number> {\n\t\tconst filters = normalizeMimeFilter(mimeType);\n\t\tlet query = this.db.selectFrom(\"media\").select((eb) => eb.fn.count<number>(\"id\").as(\"count\"));\n\n\t\tif (filters.length > 0) {\n\t\t\tquery = query.where((eb) => mimeMatchExpr(eb, filters));\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Delete pending uploads older than the given age.\n\t * Pending uploads that were never confirmed indicate abandoned upload flows.\n\t *\n\t * Returns the storage keys of deleted rows so callers can remove the\n\t * corresponding files from object storage.\n\t */\n\tasync cleanupPendingUploads(maxAgeMs: number = 60 * 60 * 1000): Promise<string[]> {\n\t\tconst cutoff = new Date(Date.now() - maxAgeMs).toISOString();\n\n\t\t// Select the storage keys first -- SQLite doesn't support RETURNING\n\t\t// on DELETE in all drivers, and Kysely's RETURNING isn't universal.\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.select(\"storage_key\")\n\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t.where(\"created_at\", \"<\", cutoff)\n\t\t\t.execute();\n\n\t\tif (rows.length === 0) return [];\n\n\t\tawait this.db\n\t\t\t.deleteFrom(\"media\")\n\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t.where(\"created_at\", \"<\", cutoff)\n\t\t\t.execute();\n\n\t\treturn rows.map((r) => r.storage_key);\n\t}\n\n\t/**\n\t * Convert database row to MediaItem\n\t */\n\tprivate rowToItem(row: MediaRow): MediaItem {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tfilename: row.filename,\n\t\t\tmimeType: row.mime_type,\n\t\t\tsize: row.size,\n\t\t\twidth: row.width,\n\t\t\theight: row.height,\n\t\t\talt: row.alt,\n\t\t\tcaption: row.caption,\n\t\t\tstorageKey: row.storage_key,\n\t\t\tcontentHash: row.content_hash,\n\t\t\tblurhash: row.blurhash,\n\t\t\tdominantColor: row.dominant_color,\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DB stores string; validated at insert but linter can't follow\n\t\t\tstatus: row.status as MediaStatus,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tauthorId: row.author_id,\n\t\t};\n\t}\n}\n","/**\n * Site Settings API\n *\n * Functions for getting and setting global site configuration.\n * Settings are stored in the options table with 'site:' prefix.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { MediaRepository } from \"../database/repositories/media.js\";\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport { peekRequestCache, requestCached } from \"../request-cache.js\";\nimport type { Storage } from \"../storage/types.js\";\nimport type { SiteSettings, SiteSettingKey, MediaReference, SeoSettings } from \"./types.js\";\n\n/** Prefix for site settings in the options table */\nconst SETTINGS_PREFIX = \"site:\";\n\n/**\n * Worker-isolate cache for the resolved `site:*` settings.\n *\n * Site settings (title, logo, SEO defaults) change rarely but are read on\n * every public request. Caching across the isolate's lifetime drops the\n * `options WHERE name LIKE 'site:%'` prefix scan from once-per-request to\n * once-per-isolate. Cross-isolate staleness is bounded by isolate lifetime\n * (workerd typically recycles within minutes); acceptable for chrome.\n *\n * Stored on globalThis with a Symbol.for key so Vite SSR chunk duplication\n * doesn't produce two independent caches (same pattern as request-context.ts).\n *\n * Invalidation: every `site:*` write bumps `version`. Reads compare the\n * cached promise's version against the current version and refetch on\n * mismatch. Caching the promise (not the resolved value) lets concurrent\n * cold-isolate readers share the in-flight query.\n */\ninterface SiteSettingsHolder {\n\tversion: number;\n\tcached: Promise<Partial<SiteSettings>> | null;\n\tcachedVersion: number;\n}\n\nconst SITE_SETTINGS_CACHE_KEY = Symbol.for(\"emdash:site-settings\");\nconst g = globalThis as Record<symbol, unknown>;\nconst holder: SiteSettingsHolder =\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts)\n\t(g[SITE_SETTINGS_CACHE_KEY] as SiteSettingsHolder | undefined) ??\n\t(() => {\n\t\tconst h: SiteSettingsHolder = { version: 0, cached: null, cachedVersion: -1 };\n\t\tg[SITE_SETTINGS_CACHE_KEY] = h;\n\t\treturn h;\n\t})();\n\n/**\n * Bump the isolate-wide site-settings cache version, forcing the next\n * `getSiteSettings()` to re-query the database.\n *\n * Called from every `site:*` write path. Other isolates still serve their\n * own cached copy until they expire — staleness bounded by isolate lifetime.\n */\nexport function invalidateSiteSettingsCache(): void {\n\tholder.version++;\n\tholder.cached = null;\n\tholder.cachedVersion = -1;\n}\n\n/**\n * Type guard for MediaReference values\n */\nfunction isMediaReference(value: unknown): value is MediaReference {\n\treturn typeof value === \"object\" && value !== null && \"mediaId\" in value;\n}\n\n/**\n * Resolve a media reference to include the full URL plus content metadata.\n *\n * Pulls `mimeType` and intrinsic dimensions from the media row so callers\n * can emit correct head tags (e.g. `<link rel=\"icon\" type=\"image/svg+xml\">`,\n * which Chromium requires when the URL has no `.svg` extension) without\n * a second round-trip to the media table.\n */\nasync function resolveMediaReference(\n\tmediaRef: MediaReference | undefined,\n\tdb: Kysely<Database>,\n\t_storage: Storage | null,\n): Promise<MediaReference | undefined> {\n\tif (!mediaRef?.mediaId) {\n\t\treturn mediaRef;\n\t}\n\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst media = await mediaRepo.findById(mediaRef.mediaId);\n\n\t\tif (media) {\n\t\t\t// Construct URL using the same pattern as API handlers\n\t\t\treturn {\n\t\t\t\t...mediaRef,\n\t\t\t\turl: `/_emdash/api/media/file/${media.storageKey}`,\n\t\t\t\tcontentType: media.mimeType,\n\t\t\t\t...(media.width !== null ? { width: media.width } : {}),\n\t\t\t\t...(media.height !== null ? { height: media.height } : {}),\n\t\t\t};\n\t\t}\n\t} catch {\n\t\t// If media not found or error, return the reference as-is\n\t}\n\n\treturn mediaRef;\n}\n\n/**\n * Get a single site setting by key\n *\n * Returns `undefined` if the setting has not been configured.\n * For media settings (logo, favicon), the URL is resolved automatically.\n *\n * @param key - The setting key (e.g., \"title\", \"logo\", \"social\")\n * @returns The setting value, or undefined if not set\n *\n * @example\n * ```ts\n * import { getSiteSetting } from \"emdash\";\n *\n * const title = await getSiteSetting(\"title\");\n * const logo = await getSiteSetting(\"logo\");\n * console.log(logo?.url); // Resolved URL\n * ```\n */\nexport async function getSiteSetting<K extends SiteSettingKey>(\n\tkey: K,\n): Promise<SiteSettings[K] | undefined> {\n\t// If `getSiteSettings()` has already been called in this request,\n\t// read from that (request-cached) batch rather than firing a second\n\t// options-table query. Common layout: a Base template pulls the\n\t// whole settings object up-front, then `EmDashHead` or a plugin\n\t// asks for one key — no reason the singular call should round-trip\n\t// again.\n\tconst primed = peekRequestCache<Partial<SiteSettings>>(\"siteSettings\");\n\tif (primed) {\n\t\tconst settings = await primed;\n\t\treturn settings[key];\n\t}\n\n\t// Otherwise cache per-key. Templates that pull several settings\n\t// independently still share the in-flight query for each one.\n\treturn requestCached(`siteSetting:${key}`, async () => {\n\t\tconst db = await getDb();\n\t\treturn getSiteSettingWithDb(key, db);\n\t});\n}\n\n/**\n * Get a single site setting by key (with explicit db)\n *\n * @internal Use `getSiteSetting()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingWithDb<K extends SiteSettingKey>(\n\tkey: K,\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<SiteSettings[K] | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<SiteSettings[K]>(`${SETTINGS_PREFIX}${key}`);\n\n\tif (!value) {\n\t\treturn undefined;\n\t}\n\n\t// Resolve media references if needed.\n\t// TS cannot narrow generic K from key equality checks — this is a known limitation.\n\t// We use the non-generic getSiteSettingsWithDb for media resolution instead.\n\tif ((key === \"logo\" || key === \"favicon\") && isMediaReference(value)) {\n\t\tconst resolved = await resolveMediaReference(value, db, storage);\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- TS can't narrow generic K from key equality; resolved type is correct\n\t\treturn resolved as SiteSettings[K] | undefined;\n\t}\n\n\tif (key === \"seo\" && value && typeof value === \"object\") {\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- TS can't narrow generic K from key equality\n\t\tconst seo = value as SeoSettings;\n\t\tif (seo.defaultOgImage) {\n\t\t\tconst resolved = {\n\t\t\t\t...seo,\n\t\t\t\tdefaultOgImage: await resolveMediaReference(seo.defaultOgImage, db, storage),\n\t\t\t};\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- TS can't narrow generic K from key equality\n\t\t\treturn resolved as SiteSettings[K] | undefined;\n\t\t}\n\t}\n\n\treturn value;\n}\n\n/**\n * Get all site settings\n *\n * Returns all configured settings. Unset values are undefined.\n * Media references (logo/favicon) are resolved to include URLs.\n *\n * @example\n * ```ts\n * import { getSiteSettings } from \"emdash\";\n *\n * const settings = await getSiteSettings();\n * console.log(settings.title); // \"My Site\"\n * console.log(settings.logo?.url); // \"/_emdash/api/media/file/abc123\"\n * ```\n */\nexport function getSiteSettings(): Promise<Partial<SiteSettings>> {\n\treturn requestCached(\"siteSettings\", () => {\n\t\tconst versionAtCall = holder.version;\n\t\tif (holder.cached && holder.cachedVersion === versionAtCall) {\n\t\t\treturn holder.cached;\n\t\t}\n\t\tconst fetchPromise = (async () => {\n\t\t\tconst db = await getDb();\n\t\t\treturn getSiteSettingsWithDb(db);\n\t\t})().catch((error) => {\n\t\t\tif (holder.cached === fetchPromise) {\n\t\t\t\tholder.cached = null;\n\t\t\t\tholder.cachedVersion = -1;\n\t\t\t}\n\t\t\tthrow error;\n\t\t});\n\t\tholder.cached = fetchPromise;\n\t\tholder.cachedVersion = versionAtCall;\n\t\treturn fetchPromise;\n\t});\n}\n\n/**\n * Get all site settings (with explicit db)\n *\n * @internal Use `getSiteSettings()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingsWithDb(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<Partial<SiteSettings>> {\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(SETTINGS_PREFIX);\n\n\tconst settings: Record<string, unknown> = {};\n\n\t// Convert Map to settings object, removing the prefix\n\tfor (const [key, value] of allOptions) {\n\t\tconst settingKey = key.replace(SETTINGS_PREFIX, \"\");\n\t\tsettings[settingKey] = value;\n\t}\n\n\tconst typedSettings = settings as Partial<SiteSettings>;\n\n\t// Resolve media references\n\tif (typedSettings.logo) {\n\t\ttypedSettings.logo = await resolveMediaReference(typedSettings.logo, db, storage);\n\t}\n\tif (typedSettings.favicon) {\n\t\ttypedSettings.favicon = await resolveMediaReference(typedSettings.favicon, db, storage);\n\t}\n\tif (typedSettings.seo?.defaultOgImage) {\n\t\ttypedSettings.seo = {\n\t\t\t...typedSettings.seo,\n\t\t\tdefaultOgImage: await resolveMediaReference(typedSettings.seo.defaultOgImage, db, storage),\n\t\t};\n\t}\n\n\treturn typedSettings;\n}\n\n/**\n * Set site settings (internal function used by admin API)\n *\n * Merges provided settings with existing ones. Only provided fields are updated.\n * Media references should include just the mediaId; URLs are resolved on read.\n *\n * @param settings - Partial settings object with values to update\n * @param db - Kysely database instance\n * @returns Promise that resolves when settings are saved\n *\n * @internal\n *\n * @example\n * ```ts\n * // Update multiple settings at once\n * await setSiteSettings({\n * title: \"My Site\",\n * tagline: \"Welcome\",\n * logo: { mediaId: \"med_123\", alt: \"Logo\" }\n * }, db);\n * ```\n */\nexport async function setSiteSettings(\n\tsettings: Partial<SiteSettings>,\n\tdb: Kysely<Database>,\n): Promise<void> {\n\tconst options = new OptionsRepository(db);\n\n\t// Convert settings to options format\n\tconst updates: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(settings)) {\n\t\tif (value !== undefined) {\n\t\t\tupdates[`${SETTINGS_PREFIX}${key}`] = value;\n\t\t}\n\t}\n\n\ttry {\n\t\tawait options.setMany(updates);\n\t} finally {\n\t\tinvalidateSiteSettingsCache();\n\t}\n}\n\n/**\n * Get a single plugin setting by key.\n *\n * Plugin settings are stored in the options table under\n * `plugin:<pluginId>:settings:<key>`.\n */\nexport async function getPluginSetting<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n): Promise<T | undefined> {\n\tconst db = await getDb();\n\treturn getPluginSettingWithDb<T>(pluginId, key, db);\n}\n\n/**\n * Get a single plugin setting by key (with explicit db).\n *\n * @internal Use `getPluginSetting()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingWithDb<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n\tdb: Kysely<Database>,\n): Promise<T | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<T>(`plugin:${pluginId}:settings:${key}`);\n\treturn value ?? undefined;\n}\n\n/**\n * Get all persisted plugin settings for a plugin.\n *\n * Defaults declared in `admin.settingsSchema` are not materialized\n * automatically; callers should apply their own fallback defaults.\n */\nexport async function getPluginSettings(pluginId: string): Promise<Record<string, unknown>> {\n\tconst db = await getDb();\n\treturn getPluginSettingsWithDb(pluginId, db);\n}\n\n/**\n * Get all persisted plugin settings for a plugin (with explicit db).\n *\n * @internal Use `getPluginSettings()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingsWithDb(\n\tpluginId: string,\n\tdb: Kysely<Database>,\n): Promise<Record<string, unknown>> {\n\tconst prefix = `plugin:${pluginId}:settings:`;\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(prefix);\n\n\tconst settings: Record<string, unknown> = {};\n\tfor (const [key, value] of allOptions) {\n\t\tif (!key.startsWith(prefix)) {\n\t\t\tcontinue;\n\t\t}\n\t\tsettings[key.slice(prefix.length)] = value;\n\t}\n\n\treturn settings;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as EmDashStorageError } from "../types-K-EkEQCI.mjs";
1
+ import { t as EmDashStorageError } from "../types-PafqtQuM.mjs";
2
2
  import mime from "mime/lite";
3
3
  import * as path from "node:path";
4
4
  import { createReadStream, existsSync } from "node:fs";
@@ -1,4 +1,4 @@
1
- import { t as EmDashStorageError } from "../types-K-EkEQCI.mjs";
1
+ import { t as EmDashStorageError } from "../types-PafqtQuM.mjs";
2
2
  import { z } from "zod";
3
3
  import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
4
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -361,7 +361,7 @@ function primeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonom
361
361
  * the content query respect the active locale.
362
362
  */
363
363
  async function getEntriesByTerm(collection, taxonomyName, termSlug, options = {}) {
364
- const { getEmDashCollection } = await import("./query-8c_meo_K.mjs").then((n) => n.o);
364
+ const { getEmDashCollection } = await import("./query-yA3-rFji.mjs").then((n) => n.o);
365
365
  const queryOptions = { where: { [taxonomyName]: termSlug } };
366
366
  if (options.locale !== void 0) queryOptions.locale = options.locale;
367
367
  const { entries } = await getEmDashCollection(collection, queryOptions);
@@ -404,4 +404,4 @@ function buildTree(flatTerms, counts) {
404
404
 
405
405
  //#endregion
406
406
  export { getTaxonomyDefs as a, getTermsForEntries as c, resolveLocale as d, resolveLocaleChain as f, getTaxonomyDef as i, invalidateTermCache as l, getEntriesByTerm as n, getTaxonomyTerms as o, getEntryTerms as r, getTerm as s, getAllTermsForEntries as t, taxonomies_exports as u };
407
- //# sourceMappingURL=taxonomies-Bw76xAxo.mjs.map
407
+ //# sourceMappingURL=taxonomies-JmQQZiG1.mjs.map