emdash 0.5.0 → 0.7.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.
- package/dist/{adapters-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-5uslYdUu.mjs} +197 -25
- package/dist/apply-5uslYdUu.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +203 -33
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +30 -4
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +11 -4
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +467 -186
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +17 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +12 -11
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/{content-BsBoyj8G.mjs → content-D7J5y73J.mjs} +27 -1
- package/dist/{content-BsBoyj8G.mjs.map → content-D7J5y73J.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db-errors-D0UT85nC.mjs +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CCWzlriB.d.mts → index-De6_Xv3v.d.mts} +209 -19
- package/dist/index-De6_Xv3v.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-B6Vu0d2i.mjs → query-g4Ug-9j9.mjs} +79 -12
- package/dist/query-g4Ug-9j9.mjs.map +1 -0
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-CN0Rt9Ob.mjs} +66 -10
- package/dist/redirect-CN0Rt9Ob.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-BR2xKwhn.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-BR2xKwhn.d.mts.map} +1 -1
- package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
- package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-Cn1SYvYF.mjs → search-B0effn3j.mjs} +210 -226
- package/dist/search-B0effn3j.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -9
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-K2z0Uhnj.mjs +308 -0
- package/dist/taxonomies-K2z0Uhnj.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-C2v0c34j.d.mts} +10 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-C2v0c34j.d.mts.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-DeG21anB.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-C3ronwXb.d.mts → types-DgrIP0tF.d.mts} +102 -4
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/{validate-Db1yNL3i.d.mts → validate-kM8Pjuf7.d.mts} +5 -52
- package/dist/validate-kM8Pjuf7.d.mts.map +1 -0
- package/dist/version-BnTKdfam.mjs +7 -0
- package/dist/{version-CMMjTuqu.mjs.map → version-BnTKdfam.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/content.ts +2 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +178 -0
- package/src/astro/integration/index.ts +44 -0
- package/src/astro/integration/routes.ts +6 -0
- package/src/astro/integration/runtime.ts +117 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +33 -1
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +21 -10
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/auth/signup/request.ts +26 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +19 -1
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +5 -4
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +32 -8
- package/src/astro/routes/api/setup/index.ts +5 -2
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +1 -1
- package/src/astro/types.ts +9 -0
- package/src/auth/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -0
- package/src/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +39 -0
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/redirect.ts +111 -8
- package/src/database/types.ts +9 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +312 -92
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/mcp/server.ts +76 -3
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +41 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CCWzlriB.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-Cn1SYvYF.mjs.map +0 -1
- package/dist/types-C3ronwXb.d.mts.map +0 -1
- package/dist/types-DeG21anB.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-Db1yNL3i.d.mts.map +0 -1
- package/dist/version-CMMjTuqu.mjs +0 -7
|
@@ -5,6 +5,24 @@ import { sql } from "kysely";
|
|
|
5
5
|
import { ulid } from "ulidx";
|
|
6
6
|
|
|
7
7
|
//#region src/database/repositories/redirect.ts
|
|
8
|
+
/**
|
|
9
|
+
* Hard cap on rows stored in `_emdash_404_log`. When exceeded, the oldest
|
|
10
|
+
* rows (by `last_seen_at`) are evicted on insert. Prevents an unauthenticated
|
|
11
|
+
* attacker from growing the table without bound by requesting unique URLs.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_404_LOG_ROWS = 1e4;
|
|
14
|
+
/** Max stored length for the `Referer` header — truncated on insert. */
|
|
15
|
+
const REFERRER_MAX_LENGTH = 512;
|
|
16
|
+
/** Max stored length for the `User-Agent` header — truncated on insert. */
|
|
17
|
+
const USER_AGENT_MAX_LENGTH = 256;
|
|
18
|
+
/**
|
|
19
|
+
* Truncate a header-derived string to `max` chars, preserving `null`/`undefined`
|
|
20
|
+
* as `null`. Empty strings stay empty (the caller decides whether to coerce).
|
|
21
|
+
*/
|
|
22
|
+
function truncateOrNull(value, max) {
|
|
23
|
+
if (value === null || value === void 0) return null;
|
|
24
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
25
|
+
}
|
|
8
26
|
function rowToRedirect(row) {
|
|
9
27
|
return {
|
|
10
28
|
id: row.id,
|
|
@@ -168,15 +186,55 @@ var RedirectRepository = class {
|
|
|
168
186
|
}).where("destination", "=", oldDestination).executeTakeFirst();
|
|
169
187
|
return Number(result.numUpdatedRows);
|
|
170
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Record a 404 hit for `entry.path`.
|
|
191
|
+
*
|
|
192
|
+
* Dedups by path: repeat hits increment `hits` and refresh `last_seen_at`
|
|
193
|
+
* on the existing row instead of inserting a new one. Referrer and
|
|
194
|
+
* user-agent are truncated to bounded lengths so a malicious client can't
|
|
195
|
+
* blow up storage with huge headers. When the table would exceed
|
|
196
|
+
* MAX_404_LOG_ROWS, the oldest entries (by `last_seen_at`) are evicted.
|
|
197
|
+
*
|
|
198
|
+
* This is called from the public redirect middleware on every 404 and
|
|
199
|
+
* must never throw for an unauthenticated caller — failures bubble up to
|
|
200
|
+
* the middleware, which swallows them.
|
|
201
|
+
*/
|
|
171
202
|
async log404(entry) {
|
|
203
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
204
|
+
const referrer = truncateOrNull(entry.referrer, REFERRER_MAX_LENGTH);
|
|
205
|
+
const userAgent = truncateOrNull(entry.userAgent, USER_AGENT_MAX_LENGTH);
|
|
206
|
+
const ip = entry.ip ?? null;
|
|
172
207
|
await this.db.insertInto("_emdash_404_log").values({
|
|
173
208
|
id: ulid(),
|
|
174
209
|
path: entry.path,
|
|
175
|
-
referrer
|
|
176
|
-
user_agent:
|
|
177
|
-
ip
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
referrer,
|
|
211
|
+
user_agent: userAgent,
|
|
212
|
+
ip,
|
|
213
|
+
hits: 1,
|
|
214
|
+
last_seen_at: now,
|
|
215
|
+
created_at: now
|
|
216
|
+
}).onConflict((oc) => oc.column("path").doUpdateSet({
|
|
217
|
+
hits: sql`hits + 1`,
|
|
218
|
+
last_seen_at: now,
|
|
219
|
+
referrer,
|
|
220
|
+
user_agent: userAgent,
|
|
221
|
+
ip
|
|
222
|
+
})).execute();
|
|
223
|
+
await this.enforce404Cap();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Delete the oldest rows from `_emdash_404_log` if the row count exceeds
|
|
227
|
+
* MAX_404_LOG_ROWS. "Oldest" is by `last_seen_at`, so a path that keeps
|
|
228
|
+
* getting hit stays in the table even if it was first seen long ago.
|
|
229
|
+
*
|
|
230
|
+
* Private — callers use `log404`, which invokes this after every upsert.
|
|
231
|
+
*/
|
|
232
|
+
async enforce404Cap() {
|
|
233
|
+
const countRow = await this.db.selectFrom("_emdash_404_log").select((eb) => eb.fn.countAll().as("c")).executeTakeFirst();
|
|
234
|
+
const count = Number(countRow?.c ?? 0);
|
|
235
|
+
if (count <= MAX_404_LOG_ROWS) return;
|
|
236
|
+
const excess = count - MAX_404_LOG_ROWS;
|
|
237
|
+
await this.db.deleteFrom("_emdash_404_log").where("id", "in", this.db.selectFrom("_emdash_404_log").select("id").orderBy("last_seen_at", "asc").orderBy("id", "asc").limit(excess)).execute();
|
|
180
238
|
}
|
|
181
239
|
async find404s(opts) {
|
|
182
240
|
const limit = Math.min(Math.max(opts.limit ?? 50, 1), 100);
|
|
@@ -206,14 +264,12 @@ var RedirectRepository = class {
|
|
|
206
264
|
return (await sql`
|
|
207
265
|
SELECT
|
|
208
266
|
path,
|
|
209
|
-
|
|
210
|
-
MAX(
|
|
267
|
+
SUM(hits) as count,
|
|
268
|
+
MAX(last_seen_at) as last_seen,
|
|
211
269
|
(
|
|
212
270
|
SELECT referrer FROM _emdash_404_log AS inner_log
|
|
213
271
|
WHERE inner_log.path = _emdash_404_log.path
|
|
214
272
|
AND referrer IS NOT NULL AND referrer != ''
|
|
215
|
-
GROUP BY referrer
|
|
216
|
-
ORDER BY COUNT(*) DESC
|
|
217
273
|
LIMIT 1
|
|
218
274
|
) as top_referrer
|
|
219
275
|
FROM _emdash_404_log
|
|
@@ -243,4 +299,4 @@ var RedirectRepository = class {
|
|
|
243
299
|
|
|
244
300
|
//#endregion
|
|
245
301
|
export { RedirectRepository as t };
|
|
246
|
-
//# sourceMappingURL=redirect-
|
|
302
|
+
//# sourceMappingURL=redirect-CN0Rt9Ob.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-CN0Rt9Ob.mjs","names":[],"sources":["../src/database/repositories/redirect.ts"],"sourcesContent":["import { sql, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport {\n\tcompilePattern,\n\tmatchPattern,\n\tinterpolateDestination,\n\tisPattern,\n} from \"../../redirects/patterns.js\";\nimport { currentTimestampValue } from \"../dialect-helpers.js\";\nimport type { Database, RedirectTable } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Bounded 404 logging\n// ---------------------------------------------------------------------------\n\n/**\n * Hard cap on rows stored in `_emdash_404_log`. When exceeded, the oldest\n * rows (by `last_seen_at`) are evicted on insert. Prevents an unauthenticated\n * attacker from growing the table without bound by requesting unique URLs.\n */\nexport const MAX_404_LOG_ROWS = 10_000;\n\n/** Max stored length for the `Referer` header — truncated on insert. */\nexport const REFERRER_MAX_LENGTH = 512;\n\n/** Max stored length for the `User-Agent` header — truncated on insert. */\nexport const USER_AGENT_MAX_LENGTH = 256;\n\n/**\n * Truncate a header-derived string to `max` chars, preserving `null`/`undefined`\n * as `null`. Empty strings stay empty (the caller decides whether to coerce).\n */\nfunction truncateOrNull(value: string | null | undefined, max: number): string | null {\n\tif (value === null || value === undefined) return null;\n\treturn value.length > max ? value.slice(0, max) : value;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface Redirect {\n\tid: string;\n\tsource: string;\n\tdestination: string;\n\ttype: number;\n\tisPattern: boolean;\n\tenabled: boolean;\n\thits: number;\n\tlastHitAt: string | null;\n\tgroupName: string | null;\n\tauto: boolean;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface CreateRedirectInput {\n\tsource: string;\n\tdestination: string;\n\ttype?: number;\n\tisPattern?: boolean;\n\tenabled?: boolean;\n\tgroupName?: string | null;\n\tauto?: boolean;\n}\n\nexport interface UpdateRedirectInput {\n\tsource?: string;\n\tdestination?: string;\n\ttype?: number;\n\tisPattern?: boolean;\n\tenabled?: boolean;\n\tgroupName?: string | null;\n}\n\nexport interface NotFoundEntry {\n\tid: string;\n\tpath: string;\n\treferrer: string | null;\n\tuserAgent: string | null;\n\tip: string | null;\n\tcreatedAt: string;\n}\n\nexport interface NotFoundSummary {\n\tpath: string;\n\tcount: number;\n\tlastSeen: string;\n\ttopReferrer: string | null;\n}\n\nexport interface RedirectMatch {\n\tredirect: Redirect;\n\tresolvedDestination: string;\n}\n\n// ---------------------------------------------------------------------------\n// Row mapping\n// ---------------------------------------------------------------------------\n\nfunction rowToRedirect(row: RedirectTable): Redirect {\n\treturn {\n\t\tid: row.id,\n\t\tsource: row.source,\n\t\tdestination: row.destination,\n\t\ttype: row.type,\n\t\tisPattern: row.is_pattern === 1,\n\t\tenabled: row.enabled === 1,\n\t\thits: row.hits,\n\t\tlastHitAt: row.last_hit_at,\n\t\tgroupName: row.group_name,\n\t\tauto: row.auto === 1,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class RedirectRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t// --- CRUD ---------------------------------------------------------------\n\n\tasync findById(id: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findBySource(source: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", source)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findMany(opts: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tsearch?: string;\n\t\tgroup?: string;\n\t\tenabled?: boolean;\n\t\tauto?: boolean;\n\t}): Promise<FindManyResult<Redirect>> {\n\t\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\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\tif (opts.search) {\n\t\t\tconst term = `%${opts.search}%`;\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([eb(\"source\", \"like\", term), eb(\"destination\", \"like\", term)]),\n\t\t\t);\n\t\t}\n\n\t\tif (opts.group !== undefined) {\n\t\t\tquery = query.where(\"group_name\", \"=\", opts.group);\n\t\t}\n\n\t\tif (opts.enabled !== undefined) {\n\t\t\tquery = query.where(\"enabled\", \"=\", opts.enabled ? 1 : 0);\n\t\t}\n\n\t\tif (opts.auto !== undefined) {\n\t\t\tquery = query.where(\"auto\", \"=\", opts.auto ? 1 : 0);\n\t\t}\n\n\t\tif (opts.cursor) {\n\t\t\tconst decoded = decodeCursor(opts.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst items = rows.slice(0, limit).map(rowToRedirect);\n\t\tconst result: FindManyResult<Redirect> = { items };\n\n\t\tif (rows.length > limit) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync create(input: CreateRedirectInput): Promise<Redirect> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\t\tconst patternFlag = input.isPattern ?? isPattern(input.source);\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_redirects\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tsource: input.source,\n\t\t\t\tdestination: input.destination,\n\t\t\t\ttype: input.type ?? 301,\n\t\t\t\tis_pattern: patternFlag ? 1 : 0,\n\t\t\t\tenabled: input.enabled !== false ? 1 : 0,\n\t\t\t\thits: 0,\n\t\t\t\tlast_hit_at: null,\n\t\t\t\tgroup_name: input.groupName ?? null,\n\t\t\t\tauto: input.auto ? 1 : 0,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\treturn (await this.findById(id))!;\n\t}\n\n\tasync update(id: string, input: UpdateRedirectInput): Promise<Redirect | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) return null;\n\n\t\tconst now = new Date().toISOString();\n\t\tconst values: Record<string, unknown> = { updated_at: now };\n\n\t\tif (input.source !== undefined) {\n\t\t\tvalues.source = input.source;\n\t\t\tvalues.is_pattern =\n\t\t\t\tinput.isPattern !== undefined ? (input.isPattern ? 1 : 0) : isPattern(input.source) ? 1 : 0;\n\t\t} else if (input.isPattern !== undefined) {\n\t\t\tvalues.is_pattern = input.isPattern ? 1 : 0;\n\t\t}\n\n\t\tif (input.destination !== undefined) values.destination = input.destination;\n\t\tif (input.type !== undefined) values.type = input.type;\n\t\tif (input.enabled !== undefined) values.enabled = input.enabled ? 1 : 0;\n\t\tif (input.groupName !== undefined) values.group_name = input.groupName;\n\n\t\tawait this.db.updateTable(\"_emdash_redirects\").set(values).where(\"id\", \"=\", id).execute();\n\n\t\treturn (await this.findById(id))!;\n\t}\n\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_redirects\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn BigInt(result.numDeletedRows) > 0n;\n\t}\n\n\t/**\n\t * Fetch all enabled redirects (for loop detection graph building).\n\t * Not paginated — returns the full set.\n\t */\n\tasync findAllEnabled(): Promise<Redirect[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.execute();\n\t\treturn rows.map(rowToRedirect);\n\t}\n\n\t// --- Matching -----------------------------------------------------------\n\n\tasync findExactMatch(path: string): Promise<Redirect | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", path)\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.where(\"is_pattern\", \"=\", 0)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? rowToRedirect(row) : null;\n\t}\n\n\tasync findEnabledPatternRules(): Promise<Redirect[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_redirects\")\n\t\t\t.selectAll()\n\t\t\t.where(\"enabled\", \"=\", 1)\n\t\t\t.where(\"is_pattern\", \"=\", 1)\n\t\t\t.execute();\n\t\treturn rows.map(rowToRedirect);\n\t}\n\n\t/**\n\t * Match a request path against all enabled redirect rules.\n\t * Checks exact matches first (indexed), then pattern rules.\n\t * Returns the matched redirect and the resolved destination URL.\n\t */\n\tasync matchPath(path: string): Promise<RedirectMatch | null> {\n\t\t// 1. Exact match (fast, indexed)\n\t\tconst exact = await this.findExactMatch(path);\n\t\tif (exact) {\n\t\t\treturn { redirect: exact, resolvedDestination: exact.destination };\n\t\t}\n\n\t\t// 2. Pattern match\n\t\tconst patterns = await this.findEnabledPatternRules();\n\t\tfor (const redirect of patterns) {\n\t\t\tconst compiled = compilePattern(redirect.source);\n\t\t\tconst params = matchPattern(compiled, path);\n\t\t\tif (params) {\n\t\t\t\tconst resolved = interpolateDestination(redirect.destination, params);\n\t\t\t\treturn { redirect, resolvedDestination: resolved };\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t// --- Hit tracking -------------------------------------------------------\n\n\tasync recordHit(id: string): Promise<void> {\n\t\tawait sql`\n\t\t\tUPDATE _emdash_redirects\n\t\t\tSET hits = hits + 1, last_hit_at = ${currentTimestampValue(this.db)}, updated_at = ${currentTimestampValue(this.db)}\n\t\t\tWHERE id = ${id}\n\t\t`.execute(this.db);\n\t}\n\n\t// --- Auto-redirects (slug change) ---------------------------------------\n\n\t/**\n\t * Create an auto-redirect when a content slug changes.\n\t * Uses the collection's URL pattern to compute old/new URLs.\n\t * Collapses existing redirect chains pointing to the old URL.\n\t */\n\tasync createAutoRedirect(\n\t\tcollection: string,\n\t\toldSlug: string,\n\t\tnewSlug: string,\n\t\tcontentId: string,\n\t\turlPattern: string | null,\n\t): Promise<Redirect> {\n\t\tconst oldUrl = urlPattern\n\t\t\t? urlPattern.replace(\"{slug}\", oldSlug).replace(\"{id}\", contentId)\n\t\t\t: `/${collection}/${oldSlug}`;\n\t\tconst newUrl = urlPattern\n\t\t\t? urlPattern.replace(\"{slug}\", newSlug).replace(\"{id}\", contentId)\n\t\t\t: `/${collection}/${newSlug}`;\n\n\t\t// Collapse chains: update any existing redirects pointing to the old URL\n\t\tawait this.collapseChains(oldUrl, newUrl);\n\n\t\t// Check if a redirect from this source already exists\n\t\tconst existing = await this.findBySource(oldUrl);\n\t\tif (existing) {\n\t\t\t// Update the existing redirect to point to the new URL\n\t\t\treturn (await this.update(existing.id, { destination: newUrl }))!;\n\t\t}\n\n\t\treturn this.create({\n\t\t\tsource: oldUrl,\n\t\t\tdestination: newUrl,\n\t\t\ttype: 301,\n\t\t\tisPattern: false,\n\t\t\tauto: true,\n\t\t\tgroupName: \"Auto: slug change\",\n\t\t});\n\t}\n\n\t/**\n\t * Update all redirects whose destination matches oldDestination\n\t * to point to newDestination instead. Prevents redirect chains.\n\t * Returns the number of updated rows.\n\t */\n\tasync collapseChains(oldDestination: string, newDestination: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_redirects\")\n\t\t\t.set({\n\t\t\t\tdestination: newDestination,\n\t\t\t\tupdated_at: new Date().toISOString(),\n\t\t\t})\n\t\t\t.where(\"destination\", \"=\", oldDestination)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numUpdatedRows);\n\t}\n\n\t// --- 404 log ------------------------------------------------------------\n\n\t/**\n\t * Record a 404 hit for `entry.path`.\n\t *\n\t * Dedups by path: repeat hits increment `hits` and refresh `last_seen_at`\n\t * on the existing row instead of inserting a new one. Referrer and\n\t * user-agent are truncated to bounded lengths so a malicious client can't\n\t * blow up storage with huge headers. When the table would exceed\n\t * MAX_404_LOG_ROWS, the oldest entries (by `last_seen_at`) are evicted.\n\t *\n\t * This is called from the public redirect middleware on every 404 and\n\t * must never throw for an unauthenticated caller — failures bubble up to\n\t * the middleware, which swallows them.\n\t */\n\tasync log404(entry: {\n\t\tpath: string;\n\t\treferrer?: string | null;\n\t\tuserAgent?: string | null;\n\t\tip?: string | null;\n\t}): Promise<void> {\n\t\tconst now = new Date().toISOString();\n\t\tconst referrer = truncateOrNull(entry.referrer, REFERRER_MAX_LENGTH);\n\t\tconst userAgent = truncateOrNull(entry.userAgent, USER_AGENT_MAX_LENGTH);\n\t\tconst ip = entry.ip ?? null;\n\n\t\t// Atomic upsert by path. The UNIQUE index on `path` makes this safe\n\t\t// under concurrency: two requests for the same new path can't both\n\t\t// insert — the second one hits the conflict branch and increments\n\t\t// hits instead of failing with a uniqueness error.\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_404_log\")\n\t\t\t.values({\n\t\t\t\tid: ulid(),\n\t\t\t\tpath: entry.path,\n\t\t\t\treferrer,\n\t\t\t\tuser_agent: userAgent,\n\t\t\t\tip,\n\t\t\t\thits: 1,\n\t\t\t\tlast_seen_at: now,\n\t\t\t\tcreated_at: now,\n\t\t\t})\n\t\t\t.onConflict((oc) =>\n\t\t\t\toc.column(\"path\").doUpdateSet({\n\t\t\t\t\thits: sql`hits + 1`,\n\t\t\t\t\tlast_seen_at: now,\n\t\t\t\t\treferrer,\n\t\t\t\t\tuser_agent: userAgent,\n\t\t\t\t\tip,\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.execute();\n\n\t\t// Enforce the row cap. Cheap when the table is under cap (single\n\t\t// COUNT(*) query); evicts oldest rows if we're over. Updates (dedup\n\t\t// hits) don't grow the table so this is a no-op for repeat paths.\n\t\tawait this.enforce404Cap();\n\t}\n\n\t/**\n\t * Delete the oldest rows from `_emdash_404_log` if the row count exceeds\n\t * MAX_404_LOG_ROWS. \"Oldest\" is by `last_seen_at`, so a path that keeps\n\t * getting hit stays in the table even if it was first seen long ago.\n\t *\n\t * Private — callers use `log404`, which invokes this after every upsert.\n\t */\n\tprivate async enforce404Cap(): Promise<void> {\n\t\tconst countRow = await this.db\n\t\t\t.selectFrom(\"_emdash_404_log\")\n\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"c\"))\n\t\t\t.executeTakeFirst();\n\t\tconst count = Number(countRow?.c ?? 0);\n\t\tif (count <= MAX_404_LOG_ROWS) return;\n\n\t\tconst excess = count - MAX_404_LOG_ROWS;\n\n\t\t// Evict the oldest rows in a single SQL statement. Using a subquery\n\t\t// (rather than materialising the victim IDs in JS and passing them\n\t\t// back as bind parameters) keeps the statement bounded regardless of\n\t\t// how far over cap the table is — important for existing installs\n\t\t// that crossed the threshold before this cap was introduced.\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\n\t\t\t\t\"id\",\n\t\t\t\t\"in\",\n\t\t\t\tthis.db\n\t\t\t\t\t.selectFrom(\"_emdash_404_log\")\n\t\t\t\t\t.select(\"id\")\n\t\t\t\t\t.orderBy(\"last_seen_at\", \"asc\")\n\t\t\t\t\t.orderBy(\"id\", \"asc\")\n\t\t\t\t\t.limit(excess),\n\t\t\t)\n\t\t\t.execute();\n\t}\n\n\tasync find404s(opts: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tsearch?: string;\n\t}): Promise<FindManyResult<NotFoundEntry>> {\n\t\tconst limit = Math.min(Math.max(opts.limit ?? 50, 1), 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_404_log\")\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\tif (opts.search) {\n\t\t\tquery = query.where(\"path\", \"like\", `%${opts.search}%`);\n\t\t}\n\n\t\tif (opts.cursor) {\n\t\t\tconst decoded = decodeCursor(opts.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst items: NotFoundEntry[] = rows.slice(0, limit).map((row) => ({\n\t\t\tid: row.id,\n\t\t\tpath: row.path,\n\t\t\treferrer: row.referrer,\n\t\t\tuserAgent: row.user_agent,\n\t\t\tip: row.ip,\n\t\t\tcreatedAt: row.created_at,\n\t\t}));\n\n\t\tconst result: FindManyResult<NotFoundEntry> = { items };\n\t\tif (rows.length > limit) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync get404Summary(limit = 50): Promise<NotFoundSummary[]> {\n\t\t// Since rows are now deduped by path, each path has exactly one row\n\t\t// with `hits` as the running count and `last_seen_at` as the latest\n\t\t// timestamp. The subquery for `top_referrer` collapses to a simple\n\t\t// pick of the row's stored referrer (the most recent one seen).\n\t\tconst rows = await sql<{\n\t\t\tpath: string;\n\t\t\tcount: number;\n\t\t\tlast_seen: string;\n\t\t\ttop_referrer: string | null;\n\t\t}>`\n\t\t\tSELECT\n\t\t\t\tpath,\n\t\t\t\tSUM(hits) as count,\n\t\t\t\tMAX(last_seen_at) as last_seen,\n\t\t\t\t(\n\t\t\t\t\tSELECT referrer FROM _emdash_404_log AS inner_log\n\t\t\t\t\tWHERE inner_log.path = _emdash_404_log.path\n\t\t\t\t\t\tAND referrer IS NOT NULL AND referrer != ''\n\t\t\t\t\tLIMIT 1\n\t\t\t\t) as top_referrer\n\t\t\tFROM _emdash_404_log\n\t\t\tGROUP BY path\n\t\t\tORDER BY count DESC\n\t\t\tLIMIT ${limit}\n\t\t`.execute(this.db);\n\n\t\treturn rows.rows.map((row) => ({\n\t\t\tpath: row.path,\n\t\t\tcount: Number(row.count),\n\t\t\tlastSeen: row.last_seen,\n\t\t\ttopReferrer: row.top_referrer,\n\t\t}));\n\t}\n\n\tasync delete404(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn BigInt(result.numDeletedRows) > 0n;\n\t}\n\n\tasync clear404s(): Promise<number> {\n\t\tconst result = await this.db.deleteFrom(\"_emdash_404_log\").executeTakeFirst();\n\t\treturn Number(result.numDeletedRows);\n\t}\n\n\tasync prune404s(olderThan: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_404_log\")\n\t\t\t.where(\"created_at\", \"<\", olderThan)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numDeletedRows);\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./chunk-ClPoSABd.mjs";
|
|
|
2
2
|
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
3
3
|
import { a as isSqlite, c as tableExists, n as currentTimestamp, s as listTablesLike } from "./dialect-helpers-DhTzaUxP.mjs";
|
|
4
4
|
import { t as withTransaction } from "./transaction-Cn2rjY78.mjs";
|
|
5
|
-
import { i as RESERVED_FIELD_SLUGS, n as FIELD_TYPE_TO_COLUMN, r as RESERVED_COLLECTION_SLUGS } from "./types-
|
|
5
|
+
import { i as RESERVED_FIELD_SLUGS, n as FIELD_TYPE_TO_COLUMN, r as RESERVED_COLLECTION_SLUGS } from "./types-DDS4MxsT.mjs";
|
|
6
6
|
import { sql } from "kysely";
|
|
7
7
|
import { ulid } from "ulidx";
|
|
8
8
|
|
|
@@ -222,7 +222,11 @@ var FTSManager = class {
|
|
|
222
222
|
async disableSearch(collectionSlug) {
|
|
223
223
|
if (!isSqlite(this.db)) return;
|
|
224
224
|
await this.dropFtsTable(collectionSlug);
|
|
225
|
-
await this.
|
|
225
|
+
const existing = await this.getSearchConfig(collectionSlug);
|
|
226
|
+
await this.setSearchConfig(collectionSlug, {
|
|
227
|
+
enabled: false,
|
|
228
|
+
weights: existing?.weights
|
|
229
|
+
});
|
|
226
230
|
}
|
|
227
231
|
/**
|
|
228
232
|
* Get index statistics for a collection
|
|
@@ -230,10 +234,10 @@ var FTSManager = class {
|
|
|
230
234
|
async getIndexStats(collectionSlug) {
|
|
231
235
|
if (!isSqlite(this.db)) return null;
|
|
232
236
|
this.validateInputs(collectionSlug);
|
|
233
|
-
const
|
|
237
|
+
const ftsDocsizeTable = `${this.getFtsTableName(collectionSlug)}_docsize`;
|
|
234
238
|
if (!await this.ftsTableExists(collectionSlug)) return null;
|
|
235
239
|
return { indexed: (await sql`
|
|
236
|
-
SELECT COUNT(*) as count FROM "${sql.raw(
|
|
240
|
+
SELECT COUNT(*) as count FROM "${sql.raw(ftsDocsizeTable)}"
|
|
237
241
|
`.execute(this.db)).rows[0]?.count ?? 0 };
|
|
238
242
|
}
|
|
239
243
|
/**
|
|
@@ -246,15 +250,22 @@ var FTSManager = class {
|
|
|
246
250
|
async verifyAndRepairIndex(collectionSlug) {
|
|
247
251
|
if (!isSqlite(this.db)) return false;
|
|
248
252
|
this.validateInputs(collectionSlug);
|
|
249
|
-
const
|
|
253
|
+
const ftsDocsizeTable = `${this.getFtsTableName(collectionSlug)}_docsize`;
|
|
250
254
|
const contentTable = this.getContentTableName(collectionSlug);
|
|
251
|
-
|
|
255
|
+
const fields = await this.getSearchableFields(collectionSlug);
|
|
256
|
+
const config = await this.getSearchConfig(collectionSlug);
|
|
257
|
+
if (!await this.ftsTableExists(collectionSlug)) {
|
|
258
|
+
if (!config?.enabled || fields.length === 0) return false;
|
|
259
|
+
console.warn(`FTS index for "${collectionSlug}" is missing. Rebuilding.`);
|
|
260
|
+
await this.rebuildIndex(collectionSlug, fields, config.weights);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
252
263
|
const contentCount = await sql`
|
|
253
264
|
SELECT COUNT(*) as count FROM ${sql.ref(contentTable)}
|
|
254
265
|
WHERE deleted_at IS NULL
|
|
255
266
|
`.execute(this.db);
|
|
256
267
|
const ftsCount = await sql`
|
|
257
|
-
SELECT COUNT(*) as count FROM "${sql.raw(
|
|
268
|
+
SELECT COUNT(*) as count FROM "${sql.raw(ftsDocsizeTable)}"
|
|
258
269
|
`.execute(this.db);
|
|
259
270
|
const contentRows = contentCount.rows[0]?.count ?? 0;
|
|
260
271
|
const ftsRows = ftsCount.rows[0]?.count ?? 0;
|
|
@@ -406,23 +417,28 @@ var SchemaRegistry = class {
|
|
|
406
417
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
407
418
|
const supportsArray = input.supports ?? existing.supports;
|
|
408
419
|
const hasSeo = input.hasSeo !== void 0 ? input.hasSeo : input.supports !== void 0 ? supportsArray.includes("seo") : existing.hasSeo;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
420
|
+
return withTransaction(this.db, async (trx) => {
|
|
421
|
+
await trx.updateTable("_emdash_collections").set({
|
|
422
|
+
label: input.label ?? existing.label,
|
|
423
|
+
label_singular: input.labelSingular ?? existing.labelSingular ?? null,
|
|
424
|
+
description: input.description ?? existing.description ?? null,
|
|
425
|
+
icon: input.icon ?? existing.icon ?? null,
|
|
426
|
+
supports: input.supports ? JSON.stringify(input.supports) : JSON.stringify(existing.supports),
|
|
427
|
+
url_pattern: input.urlPattern !== void 0 ? input.urlPattern ?? null : existing.urlPattern ?? null,
|
|
428
|
+
has_seo: hasSeo ? 1 : 0,
|
|
429
|
+
comments_enabled: input.commentsEnabled !== void 0 ? input.commentsEnabled ? 1 : 0 : existing.commentsEnabled ? 1 : 0,
|
|
430
|
+
comments_moderation: input.commentsModeration ?? existing.commentsModeration,
|
|
431
|
+
comments_closed_after_days: input.commentsClosedAfterDays !== void 0 ? input.commentsClosedAfterDays : existing.commentsClosedAfterDays,
|
|
432
|
+
comments_auto_approve_users: input.commentsAutoApproveUsers !== void 0 ? input.commentsAutoApproveUsers ? 1 : 0 : existing.commentsAutoApproveUsers ? 1 : 0,
|
|
433
|
+
updated_at: now
|
|
434
|
+
}).where("slug", "=", slug).execute();
|
|
435
|
+
const row = await trx.selectFrom("_emdash_collections").where("slug", "=", slug).selectAll().executeTakeFirst();
|
|
436
|
+
if (!row) throw new SchemaError("Failed to update collection", "UPDATE_FAILED");
|
|
437
|
+
if (input.supports !== void 0) {
|
|
438
|
+
if (existing.supports.includes("search") !== JSON.parse(row.supports ?? "[]").includes("search")) await this.syncSearchState(slug, trx);
|
|
439
|
+
}
|
|
440
|
+
return this.mapCollectionRow(row);
|
|
441
|
+
});
|
|
426
442
|
}
|
|
427
443
|
/**
|
|
428
444
|
* Delete a collection
|
|
@@ -433,8 +449,12 @@ var SchemaRegistry = class {
|
|
|
433
449
|
if (!options?.force) {
|
|
434
450
|
if (await this.collectionHasContent(slug)) throw new SchemaError(`Collection "${slug}" has content. Use force: true to delete.`, "COLLECTION_HAS_CONTENT");
|
|
435
451
|
}
|
|
436
|
-
await this.
|
|
437
|
-
|
|
452
|
+
await withTransaction(this.db, async (trx) => {
|
|
453
|
+
await new FTSManager(trx).dropFtsTable(slug);
|
|
454
|
+
const tableName = this.getTableName(slug);
|
|
455
|
+
await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(trx);
|
|
456
|
+
await trx.deleteFrom("_emdash_collections").where("id", "=", existing.id).execute();
|
|
457
|
+
});
|
|
438
458
|
}
|
|
439
459
|
/**
|
|
440
460
|
* List fields for a collection
|
|
@@ -464,30 +484,34 @@ var SchemaRegistry = class {
|
|
|
464
484
|
const columnType = FIELD_TYPE_TO_COLUMN[input.type];
|
|
465
485
|
const maxSort = await this.db.selectFrom("_emdash_fields").where("collection_id", "=", collection.id).select((eb) => eb.fn.max("sort_order").as("max")).executeTakeFirst();
|
|
466
486
|
const sortOrder = input.sortOrder ?? (maxSort?.max ?? -1) + 1;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
+
return withTransaction(this.db, async (trx) => {
|
|
488
|
+
await trx.insertInto("_emdash_fields").values({
|
|
489
|
+
id,
|
|
490
|
+
collection_id: collection.id,
|
|
491
|
+
slug: input.slug,
|
|
492
|
+
label: input.label,
|
|
493
|
+
type: input.type,
|
|
494
|
+
column_type: columnType,
|
|
495
|
+
required: input.required ? 1 : 0,
|
|
496
|
+
unique: input.unique ? 1 : 0,
|
|
497
|
+
default_value: input.defaultValue !== void 0 ? JSON.stringify(input.defaultValue) : null,
|
|
498
|
+
validation: input.validation ? JSON.stringify(input.validation) : null,
|
|
499
|
+
widget: input.widget ?? null,
|
|
500
|
+
options: input.options ? JSON.stringify(input.options) : null,
|
|
501
|
+
sort_order: sortOrder,
|
|
502
|
+
searchable: input.searchable ? 1 : 0,
|
|
503
|
+
translatable: input.translatable === false ? 0 : 1
|
|
504
|
+
}).execute();
|
|
505
|
+
await this.addColumn(collectionSlug, input.slug, input.type, {
|
|
506
|
+
required: input.required,
|
|
507
|
+
defaultValue: input.defaultValue
|
|
508
|
+
}, trx);
|
|
509
|
+
const fieldRow = await trx.selectFrom("_emdash_fields").where("collection_id", "=", collection.id).where("slug", "=", input.slug).selectAll().executeTakeFirst();
|
|
510
|
+
if (!fieldRow) throw new SchemaError("Failed to create field", "CREATE_FAILED");
|
|
511
|
+
const field = this.mapFieldRow(fieldRow);
|
|
512
|
+
if (input.searchable) await this.syncSearchState(collectionSlug, trx);
|
|
513
|
+
return field;
|
|
487
514
|
});
|
|
488
|
-
const field = await this.getField(collectionSlug, input.slug);
|
|
489
|
-
if (!field) throw new SchemaError("Failed to create field", "CREATE_FAILED");
|
|
490
|
-
return field;
|
|
491
515
|
}
|
|
492
516
|
/**
|
|
493
517
|
* Update a field
|
|
@@ -495,36 +519,51 @@ var SchemaRegistry = class {
|
|
|
495
519
|
async updateField(collectionSlug, fieldSlug, input) {
|
|
496
520
|
const field = await this.getField(collectionSlug, fieldSlug);
|
|
497
521
|
if (!field) throw new SchemaError(`Field "${fieldSlug}" not found in collection "${collectionSlug}"`, "FIELD_NOT_FOUND");
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
522
|
+
return withTransaction(this.db, async (trx) => {
|
|
523
|
+
await trx.updateTable("_emdash_fields").set({
|
|
524
|
+
label: input.label ?? field.label,
|
|
525
|
+
required: input.required !== void 0 ? input.required ? 1 : 0 : field.required ? 1 : 0,
|
|
526
|
+
unique: input.unique !== void 0 ? input.unique ? 1 : 0 : field.unique ? 1 : 0,
|
|
527
|
+
searchable: input.searchable !== void 0 ? input.searchable ? 1 : 0 : field.searchable ? 1 : 0,
|
|
528
|
+
translatable: input.translatable !== void 0 ? input.translatable ? 1 : 0 : field.translatable ? 1 : 0,
|
|
529
|
+
default_value: input.defaultValue !== void 0 ? JSON.stringify(input.defaultValue) : field.defaultValue !== void 0 ? JSON.stringify(field.defaultValue) : null,
|
|
530
|
+
validation: input.validation ? JSON.stringify(input.validation) : field.validation ? JSON.stringify(field.validation) : null,
|
|
531
|
+
widget: input.widget ?? field.widget ?? null,
|
|
532
|
+
options: input.options ? JSON.stringify(input.options) : field.options ? JSON.stringify(field.options) : null,
|
|
533
|
+
sort_order: input.sortOrder ?? field.sortOrder
|
|
534
|
+
}).where("id", "=", field.id).execute();
|
|
535
|
+
const updatedRow = await trx.selectFrom("_emdash_fields").where("collection_id", "=", field.collectionId).where("slug", "=", fieldSlug).selectAll().executeTakeFirst();
|
|
536
|
+
if (!updatedRow) throw new SchemaError("Failed to update field", "UPDATE_FAILED");
|
|
537
|
+
const updated = this.mapFieldRow(updatedRow);
|
|
538
|
+
if (input.searchable !== void 0 && input.searchable !== field.searchable) await this.syncSearchState(collectionSlug, trx);
|
|
539
|
+
return updated;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Synchronize an existing FTS index with the collection's current state.
|
|
544
|
+
*
|
|
545
|
+
* Only rebuilds or disables — never first-time enables. First-time FTS
|
|
546
|
+
* enablement is handled by the seed's explicit enableSearch call (which
|
|
547
|
+
* is try-caught) or the admin UI toggle.
|
|
517
548
|
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
549
|
+
* - FTS active + still has search support and searchable fields → rebuild
|
|
550
|
+
* - FTS active + lost search support or no searchable fields → disable
|
|
551
|
+
* - FTS not active → no-op
|
|
552
|
+
*
|
|
553
|
+
* Pass `db` when calling from within a transaction so FTS operations
|
|
554
|
+
* participate in the same transaction and are rolled back on failure.
|
|
520
555
|
*/
|
|
521
|
-
async
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
|
|
556
|
+
async syncSearchState(collectionSlug, db) {
|
|
557
|
+
const conn = db ?? this.db;
|
|
558
|
+
const ftsManager = new FTSManager(conn);
|
|
559
|
+
const row = await conn.selectFrom("_emdash_collections").where("slug", "=", collectionSlug).select("supports").executeTakeFirst();
|
|
560
|
+
if (!row) return;
|
|
561
|
+
const wantsSearch = JSON.parse(row.supports ?? "[]").includes("search");
|
|
525
562
|
const searchableFields = await ftsManager.getSearchableFields(collectionSlug);
|
|
526
|
-
|
|
527
|
-
|
|
563
|
+
const config = await ftsManager.getSearchConfig(collectionSlug);
|
|
564
|
+
const ftsActive = config?.enabled === true;
|
|
565
|
+
if (wantsSearch && searchableFields.length > 0 && ftsActive) await ftsManager.rebuildIndex(collectionSlug, searchableFields, config?.weights);
|
|
566
|
+
else if (ftsActive && (!wantsSearch || searchableFields.length === 0)) await ftsManager.disableSearch(collectionSlug);
|
|
528
567
|
}
|
|
529
568
|
/**
|
|
530
569
|
* Delete a field
|
|
@@ -532,8 +571,11 @@ var SchemaRegistry = class {
|
|
|
532
571
|
async deleteField(collectionSlug, fieldSlug) {
|
|
533
572
|
const field = await this.getField(collectionSlug, fieldSlug);
|
|
534
573
|
if (!field) throw new SchemaError(`Field "${fieldSlug}" not found in collection "${collectionSlug}"`, "FIELD_NOT_FOUND");
|
|
535
|
-
await this.
|
|
536
|
-
|
|
574
|
+
await withTransaction(this.db, async (trx) => {
|
|
575
|
+
await trx.deleteFrom("_emdash_fields").where("id", "=", field.id).execute();
|
|
576
|
+
if (field.searchable) await this.syncSearchState(collectionSlug, trx);
|
|
577
|
+
await this.dropColumn(collectionSlug, fieldSlug, trx);
|
|
578
|
+
});
|
|
537
579
|
}
|
|
538
580
|
/**
|
|
539
581
|
* Reorder fields
|
|
@@ -601,46 +643,40 @@ var SchemaRegistry = class {
|
|
|
601
643
|
`.execute(conn);
|
|
602
644
|
}
|
|
603
645
|
/**
|
|
604
|
-
* Drop a content table
|
|
605
|
-
*/
|
|
606
|
-
async dropContentTable(slug) {
|
|
607
|
-
const tableName = this.getTableName(slug);
|
|
608
|
-
await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(this.db);
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
646
|
* Add a column to a content table
|
|
612
647
|
*/
|
|
613
|
-
async addColumn(collectionSlug, fieldSlug, fieldType, options) {
|
|
648
|
+
async addColumn(collectionSlug, fieldSlug, fieldType, options, db) {
|
|
649
|
+
const conn = db ?? this.db;
|
|
614
650
|
const tableName = this.getTableName(collectionSlug);
|
|
615
651
|
const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
|
|
616
652
|
const columnName = this.getColumnName(fieldSlug);
|
|
617
653
|
if (options?.required && options?.defaultValue !== void 0) {
|
|
618
654
|
const defaultVal = this.formatDefaultValue(options.defaultValue, fieldType);
|
|
619
655
|
await sql`
|
|
620
|
-
ALTER TABLE ${sql.ref(tableName)}
|
|
656
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
621
657
|
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
|
|
622
|
-
`.execute(
|
|
658
|
+
`.execute(conn);
|
|
623
659
|
} else if (options?.required) {
|
|
624
660
|
const defaultVal = this.getEmptyDefault(fieldType);
|
|
625
661
|
await sql`
|
|
626
|
-
ALTER TABLE ${sql.ref(tableName)}
|
|
662
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
627
663
|
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
|
|
628
|
-
`.execute(
|
|
664
|
+
`.execute(conn);
|
|
629
665
|
} else await sql`
|
|
630
|
-
ALTER TABLE ${sql.ref(tableName)}
|
|
666
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
631
667
|
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)}
|
|
632
|
-
`.execute(
|
|
668
|
+
`.execute(conn);
|
|
633
669
|
}
|
|
634
670
|
/**
|
|
635
671
|
* Drop a column from a content table
|
|
636
672
|
*/
|
|
637
|
-
async dropColumn(collectionSlug, fieldSlug) {
|
|
673
|
+
async dropColumn(collectionSlug, fieldSlug, db) {
|
|
638
674
|
const tableName = this.getTableName(collectionSlug);
|
|
639
675
|
const columnName = this.getColumnName(fieldSlug);
|
|
640
676
|
await sql`
|
|
641
|
-
ALTER TABLE ${sql.ref(tableName)}
|
|
677
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
642
678
|
DROP COLUMN ${sql.ref(columnName)}
|
|
643
|
-
`.execute(this.db);
|
|
679
|
+
`.execute(db ?? this.db);
|
|
644
680
|
}
|
|
645
681
|
/**
|
|
646
682
|
* Check if a collection has any content
|
|
@@ -649,7 +685,7 @@ var SchemaRegistry = class {
|
|
|
649
685
|
const tableName = this.getTableName(slug);
|
|
650
686
|
try {
|
|
651
687
|
return ((await sql`
|
|
652
|
-
SELECT COUNT(*) as count FROM ${sql.ref(tableName)}
|
|
688
|
+
SELECT COUNT(*) as count FROM ${sql.ref(tableName)}
|
|
653
689
|
WHERE deleted_at IS NULL
|
|
654
690
|
`.execute(this.db)).rows[0]?.count ?? 0) > 0;
|
|
655
691
|
} catch {
|
|
@@ -839,4 +875,4 @@ var SchemaRegistry = class {
|
|
|
839
875
|
|
|
840
876
|
//#endregion
|
|
841
877
|
export { FTSManager as i, SchemaRegistry as n, registry_exports as r, SchemaError as t };
|
|
842
|
-
//# sourceMappingURL=registry-
|
|
878
|
+
//# sourceMappingURL=registry-Ci3WxVAr.mjs.map
|