@nexpress/core 0.3.9 → 0.3.10
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/auth.js +2 -2
- package/dist/{can-U5F4JBZ7.js → can-2CHRJ2FK.js} +3 -3
- package/dist/{chunk-3SW4L3DL.js → chunk-2LNXTEBM.js} +9 -9
- package/dist/{chunk-XPD7EQML.js → chunk-5NCTIJEN.js} +3 -3
- package/dist/{chunk-EWVXP3GP.js → chunk-A3DPN2SP.js} +2 -2
- package/dist/{chunk-XU2GJJ6Z.js → chunk-BGEPMNVB.js} +2 -2
- package/dist/{chunk-QZ52U4ET.js → chunk-IT4JBQK4.js} +3 -3
- package/dist/{chunk-6OUWW6JF.js → chunk-JMNDARCQ.js} +3 -3
- package/dist/{chunk-TSCXXBOM.js → chunk-K4IYOAZJ.js} +15 -15
- package/dist/{chunk-TSCXXBOM.js.map → chunk-K4IYOAZJ.js.map} +1 -1
- package/dist/{chunk-6MRTH734.js → chunk-KNSOZ2NN.js} +2 -2
- package/dist/{chunk-YEOQJ7WW.js → chunk-KS3S6TJS.js} +2 -2
- package/dist/{chunk-TIWJVQOO.js → chunk-LDY26JKB.js} +2 -2
- package/dist/{chunk-2X3GBJOT.js → chunk-MLQKHRW2.js} +2 -2
- package/dist/{chunk-U4QCCLAW.js → chunk-MYUCR3SE.js} +4 -3
- package/dist/{chunk-U4QCCLAW.js.map → chunk-MYUCR3SE.js.map} +1 -1
- package/dist/{chunk-EAYUAXW3.js → chunk-PXVCVEXV.js} +2 -2
- package/dist/{chunk-2OWUHCFY.js → chunk-RCK73HJI.js} +2 -2
- package/dist/{chunk-SJ7M2VCC.js → chunk-UVLDAY4U.js} +7 -7
- package/dist/{chunk-K4CJ3KXB.js → chunk-X43DI2QJ.js} +2 -2
- package/dist/community.js +11 -11
- package/dist/{config-YDGNUDKP.js → config-WD2IMHKB.js} +5 -5
- package/dist/{digest-IWHMJPXI.js → digest-U4X45I74.js} +3 -3
- package/dist/{host-HG4QGD3L.js → host-6EAJQRCL.js} +4 -4
- package/dist/i18n.js +2 -2
- package/dist/index.js +99 -18
- package/dist/index.js.map +1 -1
- package/dist/jobs.js +1 -1
- package/dist/media.js +2 -2
- package/dist/{mentions-LQRZWAGO.js → mentions-FRPM6LOO.js} +3 -3
- package/dist/{mutes-PQA6U5X7.js → mutes-2JEKADIG.js} +3 -3
- package/dist/{registry-WZVL5HH6.js → registry-JMMSOTJI.js} +2 -2
- package/dist/{scheduled-C2IKVZVK.js → scheduled-N34EKDIJ.js} +5 -5
- package/dist/seo.js +4 -4
- package/dist/{settings-NBAP7E5E.js → settings-5PH4SNIX.js} +2 -2
- package/dist/{strings-O2M7VSKV.js → strings-CKOSVH45.js} +3 -3
- package/package.json +1 -1
- /package/dist/{can-U5F4JBZ7.js.map → can-2CHRJ2FK.js.map} +0 -0
- /package/dist/{chunk-3SW4L3DL.js.map → chunk-2LNXTEBM.js.map} +0 -0
- /package/dist/{chunk-XPD7EQML.js.map → chunk-5NCTIJEN.js.map} +0 -0
- /package/dist/{chunk-EWVXP3GP.js.map → chunk-A3DPN2SP.js.map} +0 -0
- /package/dist/{chunk-XU2GJJ6Z.js.map → chunk-BGEPMNVB.js.map} +0 -0
- /package/dist/{chunk-QZ52U4ET.js.map → chunk-IT4JBQK4.js.map} +0 -0
- /package/dist/{chunk-6OUWW6JF.js.map → chunk-JMNDARCQ.js.map} +0 -0
- /package/dist/{chunk-6MRTH734.js.map → chunk-KNSOZ2NN.js.map} +0 -0
- /package/dist/{chunk-YEOQJ7WW.js.map → chunk-KS3S6TJS.js.map} +0 -0
- /package/dist/{chunk-TIWJVQOO.js.map → chunk-LDY26JKB.js.map} +0 -0
- /package/dist/{chunk-2X3GBJOT.js.map → chunk-MLQKHRW2.js.map} +0 -0
- /package/dist/{chunk-EAYUAXW3.js.map → chunk-PXVCVEXV.js.map} +0 -0
- /package/dist/{chunk-2OWUHCFY.js.map → chunk-RCK73HJI.js.map} +0 -0
- /package/dist/{chunk-SJ7M2VCC.js.map → chunk-UVLDAY4U.js.map} +0 -0
- /package/dist/{chunk-K4CJ3KXB.js.map → chunk-X43DI2QJ.js.map} +0 -0
- /package/dist/{config-YDGNUDKP.js.map → config-WD2IMHKB.js.map} +0 -0
- /package/dist/{digest-IWHMJPXI.js.map → digest-U4X45I74.js.map} +0 -0
- /package/dist/{host-HG4QGD3L.js.map → host-6EAJQRCL.js.map} +0 -0
- /package/dist/{mentions-LQRZWAGO.js.map → mentions-FRPM6LOO.js.map} +0 -0
- /package/dist/{mutes-PQA6U5X7.js.map → mutes-2JEKADIG.js.map} +0 -0
- /package/dist/{registry-WZVL5HH6.js.map → registry-JMMSOTJI.js.map} +0 -0
- /package/dist/{scheduled-C2IKVZVK.js.map → scheduled-N34EKDIJ.js.map} +0 -0
- /package/dist/{settings-NBAP7E5E.js.map → settings-5PH4SNIX.js.map} +0 -0
- /package/dist/{strings-O2M7VSKV.js.map → strings-CKOSVH45.js.map} +0 -0
|
@@ -41,7 +41,7 @@ function readQuota(raw) {
|
|
|
41
41
|
async function getCommunitySettings() {
|
|
42
42
|
const db = getDb();
|
|
43
43
|
const { getCurrentSiteId } = await import("./context-MNZ4QXPC.js");
|
|
44
|
-
const { NP_DEFAULT_SITE_ID } = await import("./registry-
|
|
44
|
+
const { NP_DEFAULT_SITE_ID } = await import("./registry-JMMSOTJI.js");
|
|
45
45
|
const siteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
|
|
46
46
|
const [row] = await db.select().from(npSettings).where(and(eq(npSettings.siteId, siteId), eq(npSettings.key, SETTINGS_KEY))).limit(1);
|
|
47
47
|
return mergeWithDefaults(row?.value);
|
|
@@ -168,4 +168,4 @@ export {
|
|
|
168
168
|
validateCommunitySettingsPatch,
|
|
169
169
|
updateCommunitySettings
|
|
170
170
|
};
|
|
171
|
-
//# sourceMappingURL=chunk-
|
|
171
|
+
//# sourceMappingURL=chunk-KNSOZ2NN.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NP_DEFAULT_SITE_ID
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-MYUCR3SE.js";
|
|
4
4
|
import {
|
|
5
5
|
getCurrentSiteId,
|
|
6
6
|
requireSiteId
|
|
@@ -98,4 +98,4 @@ export {
|
|
|
98
98
|
getMutedTargetIds,
|
|
99
99
|
listMutes
|
|
100
100
|
};
|
|
101
|
-
//# sourceMappingURL=chunk-
|
|
101
|
+
//# sourceMappingURL=chunk-KS3S6TJS.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-5C22NDW4.js";
|
|
4
4
|
import {
|
|
5
5
|
getCommunitySettings
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-KNSOZ2NN.js";
|
|
7
7
|
import {
|
|
8
8
|
NpAuthError,
|
|
9
9
|
NpForbiddenError,
|
|
@@ -855,4 +855,4 @@ export {
|
|
|
855
855
|
requestMemberPasswordReset,
|
|
856
856
|
consumeMemberPasswordReset
|
|
857
857
|
};
|
|
858
|
-
//# sourceMappingURL=chunk-
|
|
858
|
+
//# sourceMappingURL=chunk-LDY26JKB.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-4ZLMEKFX.js";
|
|
4
4
|
import {
|
|
5
5
|
NP_DEFAULT_SITE_ID
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-MYUCR3SE.js";
|
|
7
7
|
import {
|
|
8
8
|
getCurrentSiteId
|
|
9
9
|
} from "./chunk-SBCVAC2Z.js";
|
|
@@ -222,4 +222,4 @@ export {
|
|
|
222
222
|
tSync,
|
|
223
223
|
resetTranslationCache
|
|
224
224
|
};
|
|
225
|
-
//# sourceMappingURL=chunk-
|
|
225
|
+
//# sourceMappingURL=chunk-MLQKHRW2.js.map
|
|
@@ -29,11 +29,12 @@ import { eq, asc, sql } from "drizzle-orm";
|
|
|
29
29
|
// src/collections/registry.ts
|
|
30
30
|
var registry = /* @__PURE__ */ new Map();
|
|
31
31
|
function registerCollection(slug, table, config, opts) {
|
|
32
|
+
const existing = registry.get(slug);
|
|
32
33
|
registry.set(slug, {
|
|
33
34
|
config,
|
|
34
35
|
table,
|
|
35
|
-
childTables: opts?.childTables,
|
|
36
|
-
joinTables: opts?.joinTables
|
|
36
|
+
childTables: opts?.childTables ?? existing?.childTables,
|
|
37
|
+
joinTables: opts?.joinTables ?? existing?.joinTables
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
function getCollectionConfig(slug) {
|
|
@@ -302,4 +303,4 @@ export {
|
|
|
302
303
|
deleteSite,
|
|
303
304
|
NP_DEFAULT_SITE_ID
|
|
304
305
|
};
|
|
305
|
-
//# sourceMappingURL=chunk-
|
|
306
|
+
//# sourceMappingURL=chunk-MYUCR3SE.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sites/registry.ts","../src/collections/registry.ts"],"sourcesContent":["import { type and, eq, asc, sql } from \"drizzle-orm\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\n\nimport {\n getAllCollectionSlugs,\n getCollectionConfig,\n getCollectionTable,\n} from \"../collections/registry.js\";\nimport { getDb } from \"../db/runtime.js\";\nimport {\n npAuditEvents,\n npBans,\n npComments,\n npFollows,\n npMemberMutes,\n npMemberRoles,\n npNotifications,\n npReactions,\n npReports,\n} from \"../db/schema/community.js\";\nimport {\n npNavigation,\n npPluginStorage,\n npSettings,\n npSiteMemberships,\n npSites,\n npStringOverrides,\n} from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\n\n/**\n * Phase 15.1 — multi-site registry. The framework treats\n * sites as long-lived rows in `np_sites`; the bootstrap calls\n * `ensureDefaultSite()` at boot to guarantee at least one row\n * exists so single-tenant installs (the existing reference\n * app shape) keep working without operator intervention.\n *\n * 15.1 ships the model + lookup helpers; 15.2 wires\n * collection queries through `siteId`; 15.3 ships the\n * super-admin UI for creating / managing sites. Until 15.2\n * lands, nothing in the existing pipeline knows or cares\n * about which site a row belongs to — the columns just\n * exist and the default site backfills.\n */\n\nexport interface NpSite {\n id: string;\n name: string;\n hostname: string | null;\n description: string | null;\n settings: Record<string, unknown>;\n isDefault: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nconst DEFAULT_SITE_ID = \"default\";\n\nfunction rowToSite(row: typeof npSites.$inferSelect): NpSite {\n return {\n id: row.id,\n name: row.name,\n hostname: row.hostname,\n description: row.description,\n settings: row.settings ?? {},\n isDefault: row.isDefault,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/**\n * Idempotently create the default site if no sites exist.\n * Bootstrap calls this once during framework init; tests\n * that truncate `np_sites` between cases re-trigger it.\n */\nexport async function ensureDefaultSite(): Promise<NpSite> {\n const db = getDb();\n const existingDefault = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, DEFAULT_SITE_ID))\n .limit(1);\n if (existingDefault[0]) return rowToSite(existingDefault[0]);\n\n const now = new Date();\n const [created] = await db\n .insert(npSites)\n .values({\n id: DEFAULT_SITE_ID,\n name: \"Default site\",\n hostname: null,\n isDefault: true,\n settings: {},\n createdAt: now,\n updatedAt: now,\n })\n .onConflictDoNothing()\n .returning();\n if (created) return rowToSite(created);\n\n // Conflict path: another worker raced us. Re-read.\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, DEFAULT_SITE_ID))\n .limit(1);\n if (!row) {\n throw new Error(\"Failed to create or read the default site\");\n }\n return rowToSite(row);\n}\n\nexport async function listSites(): Promise<NpSite[]> {\n const db = getDb();\n const rows = await db.select().from(npSites).orderBy(asc(npSites.createdAt));\n return rows.map(rowToSite);\n}\n\nexport async function getSiteById(id: string): Promise<NpSite | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, id))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\n/**\n * Hostname-based lookup. Returns the matching site, or the\n * default site when no row matches (so a request hitting\n * an unconfigured host still gets served by the canonical\n * site rather than 404'ing). Case-insensitive on the host\n * string.\n */\nexport async function getSiteByHostname(\n hostname: string,\n): Promise<NpSite | null> {\n const db = getDb();\n const lower = hostname.toLowerCase();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.hostname, lower))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\nexport async function getDefaultSite(): Promise<NpSite | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.isDefault, true))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\n/**\n * Resolve which site a request belongs to. Tries hostname\n * lookup first; falls back to the default site. Returns\n * `null` only when the database has no sites at all (which\n * shouldn't happen post-bootstrap).\n */\nexport async function resolveSiteForHostname(\n hostname: string | null | undefined,\n): Promise<NpSite | null> {\n if (hostname) {\n const matched = await getSiteByHostname(hostname);\n if (matched) return matched;\n }\n return getDefaultSite();\n}\n\nexport interface CreateSiteInput {\n id: string;\n name: string;\n hostname?: string | null;\n description?: string | null;\n settings?: Record<string, unknown>;\n}\n\nexport async function createSite(input: CreateSiteInput): Promise<NpSite> {\n if (!/^[a-z][a-z0-9-]*$/.test(input.id)) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"id\",\n message:\n \"Site id must be lowercase alphanumeric + hyphens, starting with a letter\",\n },\n ]);\n }\n const db = getDb();\n const now = new Date();\n const [row] = await db\n .insert(npSites)\n .values({\n id: input.id,\n name: input.name,\n hostname: input.hostname?.toLowerCase() ?? null,\n description: input.description ?? null,\n settings: input.settings ?? {},\n isDefault: false,\n createdAt: now,\n updatedAt: now,\n })\n .returning();\n if (!row) {\n throw new Error(\"Failed to create site\");\n }\n return rowToSite(row);\n}\n\nexport async function updateSite(\n id: string,\n patch: Partial<Pick<NpSite, \"name\" | \"hostname\" | \"description\" | \"settings\">>,\n): Promise<NpSite> {\n const db = getDb();\n const updates: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) updates.name = patch.name;\n if (patch.hostname !== undefined) {\n updates.hostname = patch.hostname ? patch.hostname.toLowerCase() : null;\n }\n if (patch.description !== undefined) updates.description = patch.description;\n if (patch.settings !== undefined) updates.settings = patch.settings;\n const [row] = await db\n .update(npSites)\n .set(updates)\n .where(eq(npSites.id, id))\n .returning();\n if (!row) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"id\", message: `Site \"${id}\" not found` },\n ]);\n }\n return rowToSite(row);\n}\n\n/**\n * Phase 15.9 — count of every site-scoped row attached to a\n * given site. Surfaces in the admin delete-site dialog so\n * operators see what they're about to nuke (or leave behind\n * as orphans, in the cascade=false path).\n *\n * Includes:\n * - per-collection row counts (codegen'd `np_c_*` tables)\n * - system tables that carry `site_id`: settings,\n * navigation, memberships, string overrides, plugin\n * storage (Issue #220)\n * - community tables that carry `site_id`: comments,\n * reactions, follows, mutes, notifications, reports,\n * audit events, bans, member roles (Issue #220)\n *\n * Does NOT include things that aren't site-scoped:\n * - users (`np_users` is global)\n * - members (`np_members` is global; per-site enrollment\n * happens through the site-scoped `bans` / `member_roles`\n * tables which DO appear in usage)\n * - media (`np_media` is global)\n * - audit events with `site_id IS NULL` — those are\n * intentional super-admin / background-job events that\n * don't belong to any tenant.\n */\nexport interface NpSiteUsage {\n collections: Record<string, number>;\n settings: number;\n navigation: number;\n memberships: number;\n stringOverrides: number;\n /** Issue #220 — newly-included site-scoped tables. */\n pluginStorage: number;\n comments: number;\n reactions: number;\n follows: number;\n mutes: number;\n notifications: number;\n reports: number;\n auditEvents: number;\n bans: number;\n memberRoles: number;\n /** Sum of every count above. Convenience for \"is anything here?\" checks. */\n total: number;\n}\n\nexport async function getSiteUsageSummary(id: string): Promise<NpSiteUsage> {\n const db = getDb();\n const collections: Record<string, number> = {};\n for (const slug of getAllCollectionSlugs()) {\n try {\n const config = getCollectionConfig(slug);\n void config;\n const table = getCollectionTable(slug) as PgTable;\n const idCol = (table as unknown as Record<string, unknown>).siteId;\n if (!idCol) continue;\n const [row] = (await db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(eq(idCol as never, id))) as Array<{ count: number }>;\n collections[slug] = row?.count ?? 0;\n } catch {\n // Collection without a registered table — skip silently.\n }\n }\n\n const countWhere = async (\n table: PgTable,\n where: ReturnType<typeof eq> | ReturnType<typeof and>,\n ): Promise<number> => {\n const [row] = (await db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(where)) as Array<{ count: number }>;\n return row?.count ?? 0;\n };\n\n const settings = await countWhere(npSettings, eq(npSettings.siteId, id));\n const navigation = await countWhere(npNavigation, eq(npNavigation.siteId, id));\n const memberships = await countWhere(\n npSiteMemberships,\n eq(npSiteMemberships.siteId, id),\n );\n const stringOverrides = await countWhere(\n npStringOverrides,\n eq(npStringOverrides.siteId, id),\n );\n // Issue #220 — include the tables that landed after Phase 15.9\n // shipped. Without them a site looks \"empty\" in the admin\n // even though it owns thousands of community rows; deleting\n // it would silently leave them orphaned.\n const pluginStorage = await countWhere(\n npPluginStorage,\n eq(npPluginStorage.siteId, id),\n );\n const comments = await countWhere(npComments, eq(npComments.siteId, id));\n const reactions = await countWhere(npReactions, eq(npReactions.siteId, id));\n const follows = await countWhere(npFollows, eq(npFollows.siteId, id));\n const mutes = await countWhere(npMemberMutes, eq(npMemberMutes.siteId, id));\n const notifications = await countWhere(\n npNotifications,\n eq(npNotifications.siteId, id),\n );\n const reports = await countWhere(npReports, eq(npReports.siteId, id));\n // Audit events with `site_id IS NULL` are the cross-tenant /\n // background-job rows; we deliberately don't count them here.\n const auditEvents = await countWhere(\n npAuditEvents,\n eq(npAuditEvents.siteId, id),\n );\n const bans = await countWhere(npBans, eq(npBans.siteId, id));\n const memberRoles = await countWhere(npMemberRoles, eq(npMemberRoles.siteId, id));\n\n const collectionsTotal = Object.values(collections).reduce(\n (sum, n) => sum + n,\n 0,\n );\n\n return {\n collections,\n settings,\n navigation,\n memberships,\n stringOverrides,\n pluginStorage,\n comments,\n reactions,\n follows,\n mutes,\n notifications,\n reports,\n auditEvents,\n bans,\n memberRoles,\n total:\n collectionsTotal +\n settings +\n navigation +\n memberships +\n stringOverrides +\n pluginStorage +\n comments +\n reactions +\n follows +\n mutes +\n notifications +\n reports +\n auditEvents +\n bans +\n memberRoles,\n };\n}\n\nexport interface NpDeleteSiteOptions {\n /**\n * Phase 15.9 — when `true`, cascade-delete every site-scoped\n * row (collection content, settings, navigation, memberships,\n * string overrides) before dropping the `np_sites` row.\n *\n * When `false` (default, safe), the call refuses if any\n * site-scoped data still exists. The admin UI uses this to\n * force operators to confirm cascade explicitly so an\n * accidental delete can't quietly orphan thousands of rows.\n */\n cascade?: boolean;\n}\n\n/**\n * Delete a non-default site. The default site can't be\n * deleted (the framework's invariant is \"at least one site\n * always exists\"); operators who want to retire the default\n * promote a different site to default first.\n *\n * Phase 15.9 — `options.cascade` controls whether site-scoped\n * data is deleted alongside. Defaults to `false` for safety;\n * the admin UI surfaces a usage summary first so the operator\n * sees what cascade would touch.\n */\nexport async function deleteSite(\n id: string,\n options?: NpDeleteSiteOptions,\n): Promise<void> {\n const db = getDb();\n const [target] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, id))\n .limit(1);\n if (!target) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"id\", message: `Site \"${id}\" not found` },\n ]);\n }\n if (target.isDefault) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"id\",\n message:\n \"Cannot delete the default site. Promote another site to default first.\",\n },\n ]);\n }\n\n const usage = await getSiteUsageSummary(id);\n if (usage.total > 0 && !options?.cascade) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"cascade\",\n message: `Site \"${id}\" has ${usage.total} attached row(s). Pass cascade=true to delete them, or clear them manually first.`,\n },\n ]);\n }\n\n if (options?.cascade) {\n // Order: collection content first, then community rows that\n // reference comments / members polymorphically (so we don't\n // leave orphan reactions pointing at deleted comments mid-\n // sweep), then community parent tables, then system tables.\n // Collection deletes go through the raw table (no hook\n // firing) — site teardown isn't a pipeline write and there's\n // no doc-level afterDelete hook expected here.\n for (const slug of Object.keys(usage.collections)) {\n try {\n const table = getCollectionTable(slug) as PgTable;\n const siteIdCol = (table as unknown as Record<string, unknown>).siteId;\n if (!siteIdCol) continue;\n await db.delete(table).where(eq(siteIdCol as never, id));\n } catch {\n // Ignore — the collection might have been\n // unregistered between the usage scan and the delete.\n }\n }\n // Issue #220 — community rows. Order:\n // reactions/follows/mutes/notifications/reports/audit/bans/\n // member_roles → comments → string_overrides/navigation/\n // settings/plugin_storage/memberships → np_sites.\n // Reactions reference comment ids polymorphically, so they\n // go before comments to keep the DB clean even though there's\n // no FK to enforce ordering.\n await db.delete(npReactions).where(eq(npReactions.siteId, id));\n await db.delete(npFollows).where(eq(npFollows.siteId, id));\n await db.delete(npMemberMutes).where(eq(npMemberMutes.siteId, id));\n await db.delete(npNotifications).where(eq(npNotifications.siteId, id));\n await db.delete(npReports).where(eq(npReports.siteId, id));\n await db.delete(npAuditEvents).where(eq(npAuditEvents.siteId, id));\n await db.delete(npBans).where(eq(npBans.siteId, id));\n await db.delete(npMemberRoles).where(eq(npMemberRoles.siteId, id));\n await db.delete(npComments).where(eq(npComments.siteId, id));\n\n await db.delete(npStringOverrides).where(eq(npStringOverrides.siteId, id));\n await db.delete(npNavigation).where(eq(npNavigation.siteId, id));\n await db.delete(npSettings).where(eq(npSettings.siteId, id));\n await db.delete(npPluginStorage).where(eq(npPluginStorage.siteId, id));\n await db.delete(npSiteMemberships).where(eq(npSiteMemberships.siteId, id));\n }\n\n await db.delete(npSites).where(eq(npSites.id, id));\n}\n\nexport const NP_DEFAULT_SITE_ID = DEFAULT_SITE_ID;\n","import type { NpCollectionConfig } from \"../config/types.js\";\n\nimport { NpNotFoundError } from \"../errors.js\";\n\nexport interface CollectionRegistration {\n config: NpCollectionConfig;\n table: unknown;\n childTables?: Record<string, unknown>;\n joinTables?: Record<string, unknown>;\n}\n\nconst registry = new Map<string, CollectionRegistration>();\n\nexport function registerCollection(\n slug: string,\n table: unknown,\n config: NpCollectionConfig,\n opts?: { childTables?: Record<string, unknown>; joinTables?: Record<string, unknown> },\n): void {\n registry.set(slug, {\n config,\n table,\n childTables: opts?.childTables,\n joinTables: opts?.joinTables,\n });\n}\n\nexport function getCollectionConfig(slug: string): NpCollectionConfig {\n return getCollectionRegistration(slug).config;\n}\n\nexport function getCollectionTable(slug: string): unknown {\n return getCollectionRegistration(slug).table;\n}\n\nexport function getCollectionRegistration(slug: string): CollectionRegistration {\n const registration = registry.get(slug);\n\n if (!registration) {\n throw new NpNotFoundError(\"collection\", slug);\n }\n\n return registration;\n}\n\nexport function getAllCollectionSlugs(): string[] {\n return [...registry.keys()];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/sites/registry.ts","../src/collections/registry.ts"],"sourcesContent":["import { type and, eq, asc, sql } from \"drizzle-orm\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\n\nimport {\n getAllCollectionSlugs,\n getCollectionConfig,\n getCollectionTable,\n} from \"../collections/registry.js\";\nimport { getDb } from \"../db/runtime.js\";\nimport {\n npAuditEvents,\n npBans,\n npComments,\n npFollows,\n npMemberMutes,\n npMemberRoles,\n npNotifications,\n npReactions,\n npReports,\n} from \"../db/schema/community.js\";\nimport {\n npNavigation,\n npPluginStorage,\n npSettings,\n npSiteMemberships,\n npSites,\n npStringOverrides,\n} from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\n\n/**\n * Phase 15.1 — multi-site registry. The framework treats\n * sites as long-lived rows in `np_sites`; the bootstrap calls\n * `ensureDefaultSite()` at boot to guarantee at least one row\n * exists so single-tenant installs (the existing reference\n * app shape) keep working without operator intervention.\n *\n * 15.1 ships the model + lookup helpers; 15.2 wires\n * collection queries through `siteId`; 15.3 ships the\n * super-admin UI for creating / managing sites. Until 15.2\n * lands, nothing in the existing pipeline knows or cares\n * about which site a row belongs to — the columns just\n * exist and the default site backfills.\n */\n\nexport interface NpSite {\n id: string;\n name: string;\n hostname: string | null;\n description: string | null;\n settings: Record<string, unknown>;\n isDefault: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nconst DEFAULT_SITE_ID = \"default\";\n\nfunction rowToSite(row: typeof npSites.$inferSelect): NpSite {\n return {\n id: row.id,\n name: row.name,\n hostname: row.hostname,\n description: row.description,\n settings: row.settings ?? {},\n isDefault: row.isDefault,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/**\n * Idempotently create the default site if no sites exist.\n * Bootstrap calls this once during framework init; tests\n * that truncate `np_sites` between cases re-trigger it.\n */\nexport async function ensureDefaultSite(): Promise<NpSite> {\n const db = getDb();\n const existingDefault = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, DEFAULT_SITE_ID))\n .limit(1);\n if (existingDefault[0]) return rowToSite(existingDefault[0]);\n\n const now = new Date();\n const [created] = await db\n .insert(npSites)\n .values({\n id: DEFAULT_SITE_ID,\n name: \"Default site\",\n hostname: null,\n isDefault: true,\n settings: {},\n createdAt: now,\n updatedAt: now,\n })\n .onConflictDoNothing()\n .returning();\n if (created) return rowToSite(created);\n\n // Conflict path: another worker raced us. Re-read.\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, DEFAULT_SITE_ID))\n .limit(1);\n if (!row) {\n throw new Error(\"Failed to create or read the default site\");\n }\n return rowToSite(row);\n}\n\nexport async function listSites(): Promise<NpSite[]> {\n const db = getDb();\n const rows = await db.select().from(npSites).orderBy(asc(npSites.createdAt));\n return rows.map(rowToSite);\n}\n\nexport async function getSiteById(id: string): Promise<NpSite | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, id))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\n/**\n * Hostname-based lookup. Returns the matching site, or the\n * default site when no row matches (so a request hitting\n * an unconfigured host still gets served by the canonical\n * site rather than 404'ing). Case-insensitive on the host\n * string.\n */\nexport async function getSiteByHostname(\n hostname: string,\n): Promise<NpSite | null> {\n const db = getDb();\n const lower = hostname.toLowerCase();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.hostname, lower))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\nexport async function getDefaultSite(): Promise<NpSite | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.isDefault, true))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\n/**\n * Resolve which site a request belongs to. Tries hostname\n * lookup first; falls back to the default site. Returns\n * `null` only when the database has no sites at all (which\n * shouldn't happen post-bootstrap).\n */\nexport async function resolveSiteForHostname(\n hostname: string | null | undefined,\n): Promise<NpSite | null> {\n if (hostname) {\n const matched = await getSiteByHostname(hostname);\n if (matched) return matched;\n }\n return getDefaultSite();\n}\n\nexport interface CreateSiteInput {\n id: string;\n name: string;\n hostname?: string | null;\n description?: string | null;\n settings?: Record<string, unknown>;\n}\n\nexport async function createSite(input: CreateSiteInput): Promise<NpSite> {\n if (!/^[a-z][a-z0-9-]*$/.test(input.id)) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"id\",\n message:\n \"Site id must be lowercase alphanumeric + hyphens, starting with a letter\",\n },\n ]);\n }\n const db = getDb();\n const now = new Date();\n const [row] = await db\n .insert(npSites)\n .values({\n id: input.id,\n name: input.name,\n hostname: input.hostname?.toLowerCase() ?? null,\n description: input.description ?? null,\n settings: input.settings ?? {},\n isDefault: false,\n createdAt: now,\n updatedAt: now,\n })\n .returning();\n if (!row) {\n throw new Error(\"Failed to create site\");\n }\n return rowToSite(row);\n}\n\nexport async function updateSite(\n id: string,\n patch: Partial<Pick<NpSite, \"name\" | \"hostname\" | \"description\" | \"settings\">>,\n): Promise<NpSite> {\n const db = getDb();\n const updates: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) updates.name = patch.name;\n if (patch.hostname !== undefined) {\n updates.hostname = patch.hostname ? patch.hostname.toLowerCase() : null;\n }\n if (patch.description !== undefined) updates.description = patch.description;\n if (patch.settings !== undefined) updates.settings = patch.settings;\n const [row] = await db\n .update(npSites)\n .set(updates)\n .where(eq(npSites.id, id))\n .returning();\n if (!row) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"id\", message: `Site \"${id}\" not found` },\n ]);\n }\n return rowToSite(row);\n}\n\n/**\n * Phase 15.9 — count of every site-scoped row attached to a\n * given site. Surfaces in the admin delete-site dialog so\n * operators see what they're about to nuke (or leave behind\n * as orphans, in the cascade=false path).\n *\n * Includes:\n * - per-collection row counts (codegen'd `np_c_*` tables)\n * - system tables that carry `site_id`: settings,\n * navigation, memberships, string overrides, plugin\n * storage (Issue #220)\n * - community tables that carry `site_id`: comments,\n * reactions, follows, mutes, notifications, reports,\n * audit events, bans, member roles (Issue #220)\n *\n * Does NOT include things that aren't site-scoped:\n * - users (`np_users` is global)\n * - members (`np_members` is global; per-site enrollment\n * happens through the site-scoped `bans` / `member_roles`\n * tables which DO appear in usage)\n * - media (`np_media` is global)\n * - audit events with `site_id IS NULL` — those are\n * intentional super-admin / background-job events that\n * don't belong to any tenant.\n */\nexport interface NpSiteUsage {\n collections: Record<string, number>;\n settings: number;\n navigation: number;\n memberships: number;\n stringOverrides: number;\n /** Issue #220 — newly-included site-scoped tables. */\n pluginStorage: number;\n comments: number;\n reactions: number;\n follows: number;\n mutes: number;\n notifications: number;\n reports: number;\n auditEvents: number;\n bans: number;\n memberRoles: number;\n /** Sum of every count above. Convenience for \"is anything here?\" checks. */\n total: number;\n}\n\nexport async function getSiteUsageSummary(id: string): Promise<NpSiteUsage> {\n const db = getDb();\n const collections: Record<string, number> = {};\n for (const slug of getAllCollectionSlugs()) {\n try {\n const config = getCollectionConfig(slug);\n void config;\n const table = getCollectionTable(slug) as PgTable;\n const idCol = (table as unknown as Record<string, unknown>).siteId;\n if (!idCol) continue;\n const [row] = (await db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(eq(idCol as never, id))) as Array<{ count: number }>;\n collections[slug] = row?.count ?? 0;\n } catch {\n // Collection without a registered table — skip silently.\n }\n }\n\n const countWhere = async (\n table: PgTable,\n where: ReturnType<typeof eq> | ReturnType<typeof and>,\n ): Promise<number> => {\n const [row] = (await db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(where)) as Array<{ count: number }>;\n return row?.count ?? 0;\n };\n\n const settings = await countWhere(npSettings, eq(npSettings.siteId, id));\n const navigation = await countWhere(npNavigation, eq(npNavigation.siteId, id));\n const memberships = await countWhere(\n npSiteMemberships,\n eq(npSiteMemberships.siteId, id),\n );\n const stringOverrides = await countWhere(\n npStringOverrides,\n eq(npStringOverrides.siteId, id),\n );\n // Issue #220 — include the tables that landed after Phase 15.9\n // shipped. Without them a site looks \"empty\" in the admin\n // even though it owns thousands of community rows; deleting\n // it would silently leave them orphaned.\n const pluginStorage = await countWhere(\n npPluginStorage,\n eq(npPluginStorage.siteId, id),\n );\n const comments = await countWhere(npComments, eq(npComments.siteId, id));\n const reactions = await countWhere(npReactions, eq(npReactions.siteId, id));\n const follows = await countWhere(npFollows, eq(npFollows.siteId, id));\n const mutes = await countWhere(npMemberMutes, eq(npMemberMutes.siteId, id));\n const notifications = await countWhere(\n npNotifications,\n eq(npNotifications.siteId, id),\n );\n const reports = await countWhere(npReports, eq(npReports.siteId, id));\n // Audit events with `site_id IS NULL` are the cross-tenant /\n // background-job rows; we deliberately don't count them here.\n const auditEvents = await countWhere(\n npAuditEvents,\n eq(npAuditEvents.siteId, id),\n );\n const bans = await countWhere(npBans, eq(npBans.siteId, id));\n const memberRoles = await countWhere(npMemberRoles, eq(npMemberRoles.siteId, id));\n\n const collectionsTotal = Object.values(collections).reduce(\n (sum, n) => sum + n,\n 0,\n );\n\n return {\n collections,\n settings,\n navigation,\n memberships,\n stringOverrides,\n pluginStorage,\n comments,\n reactions,\n follows,\n mutes,\n notifications,\n reports,\n auditEvents,\n bans,\n memberRoles,\n total:\n collectionsTotal +\n settings +\n navigation +\n memberships +\n stringOverrides +\n pluginStorage +\n comments +\n reactions +\n follows +\n mutes +\n notifications +\n reports +\n auditEvents +\n bans +\n memberRoles,\n };\n}\n\nexport interface NpDeleteSiteOptions {\n /**\n * Phase 15.9 — when `true`, cascade-delete every site-scoped\n * row (collection content, settings, navigation, memberships,\n * string overrides) before dropping the `np_sites` row.\n *\n * When `false` (default, safe), the call refuses if any\n * site-scoped data still exists. The admin UI uses this to\n * force operators to confirm cascade explicitly so an\n * accidental delete can't quietly orphan thousands of rows.\n */\n cascade?: boolean;\n}\n\n/**\n * Delete a non-default site. The default site can't be\n * deleted (the framework's invariant is \"at least one site\n * always exists\"); operators who want to retire the default\n * promote a different site to default first.\n *\n * Phase 15.9 — `options.cascade` controls whether site-scoped\n * data is deleted alongside. Defaults to `false` for safety;\n * the admin UI surfaces a usage summary first so the operator\n * sees what cascade would touch.\n */\nexport async function deleteSite(\n id: string,\n options?: NpDeleteSiteOptions,\n): Promise<void> {\n const db = getDb();\n const [target] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, id))\n .limit(1);\n if (!target) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"id\", message: `Site \"${id}\" not found` },\n ]);\n }\n if (target.isDefault) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"id\",\n message:\n \"Cannot delete the default site. Promote another site to default first.\",\n },\n ]);\n }\n\n const usage = await getSiteUsageSummary(id);\n if (usage.total > 0 && !options?.cascade) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"cascade\",\n message: `Site \"${id}\" has ${usage.total} attached row(s). Pass cascade=true to delete them, or clear them manually first.`,\n },\n ]);\n }\n\n if (options?.cascade) {\n // Order: collection content first, then community rows that\n // reference comments / members polymorphically (so we don't\n // leave orphan reactions pointing at deleted comments mid-\n // sweep), then community parent tables, then system tables.\n // Collection deletes go through the raw table (no hook\n // firing) — site teardown isn't a pipeline write and there's\n // no doc-level afterDelete hook expected here.\n for (const slug of Object.keys(usage.collections)) {\n try {\n const table = getCollectionTable(slug) as PgTable;\n const siteIdCol = (table as unknown as Record<string, unknown>).siteId;\n if (!siteIdCol) continue;\n await db.delete(table).where(eq(siteIdCol as never, id));\n } catch {\n // Ignore — the collection might have been\n // unregistered between the usage scan and the delete.\n }\n }\n // Issue #220 — community rows. Order:\n // reactions/follows/mutes/notifications/reports/audit/bans/\n // member_roles → comments → string_overrides/navigation/\n // settings/plugin_storage/memberships → np_sites.\n // Reactions reference comment ids polymorphically, so they\n // go before comments to keep the DB clean even though there's\n // no FK to enforce ordering.\n await db.delete(npReactions).where(eq(npReactions.siteId, id));\n await db.delete(npFollows).where(eq(npFollows.siteId, id));\n await db.delete(npMemberMutes).where(eq(npMemberMutes.siteId, id));\n await db.delete(npNotifications).where(eq(npNotifications.siteId, id));\n await db.delete(npReports).where(eq(npReports.siteId, id));\n await db.delete(npAuditEvents).where(eq(npAuditEvents.siteId, id));\n await db.delete(npBans).where(eq(npBans.siteId, id));\n await db.delete(npMemberRoles).where(eq(npMemberRoles.siteId, id));\n await db.delete(npComments).where(eq(npComments.siteId, id));\n\n await db.delete(npStringOverrides).where(eq(npStringOverrides.siteId, id));\n await db.delete(npNavigation).where(eq(npNavigation.siteId, id));\n await db.delete(npSettings).where(eq(npSettings.siteId, id));\n await db.delete(npPluginStorage).where(eq(npPluginStorage.siteId, id));\n await db.delete(npSiteMemberships).where(eq(npSiteMemberships.siteId, id));\n }\n\n await db.delete(npSites).where(eq(npSites.id, id));\n}\n\nexport const NP_DEFAULT_SITE_ID = DEFAULT_SITE_ID;\n","import type { NpCollectionConfig } from \"../config/types.js\";\n\nimport { NpNotFoundError } from \"../errors.js\";\n\nexport interface CollectionRegistration {\n config: NpCollectionConfig;\n table: unknown;\n childTables?: Record<string, unknown>;\n joinTables?: Record<string, unknown>;\n}\n\nconst registry = new Map<string, CollectionRegistration>();\n\nexport function registerCollection(\n slug: string,\n table: unknown,\n config: NpCollectionConfig,\n opts?: { childTables?: Record<string, unknown>; joinTables?: Record<string, unknown> },\n): void {\n const existing = registry.get(slug);\n registry.set(slug, {\n config,\n table,\n childTables: opts?.childTables ?? existing?.childTables,\n joinTables: opts?.joinTables ?? existing?.joinTables,\n });\n}\n\nexport function getCollectionConfig(slug: string): NpCollectionConfig {\n return getCollectionRegistration(slug).config;\n}\n\nexport function getCollectionTable(slug: string): unknown {\n return getCollectionRegistration(slug).table;\n}\n\nexport function getCollectionRegistration(slug: string): CollectionRegistration {\n const registration = registry.get(slug);\n\n if (!registration) {\n throw new NpNotFoundError(\"collection\", slug);\n }\n\n return registration;\n}\n\nexport function getAllCollectionSlugs(): string[] {\n return [...registry.keys()];\n}\n"],"mappings":"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","names":[]}
|
|
@@ -170,7 +170,7 @@ async function uploadMedia(file, uploader, folderId) {
|
|
|
170
170
|
return { id, status: "processing" };
|
|
171
171
|
}
|
|
172
172
|
async function assertMemberUploadQuota(memberId, txDb) {
|
|
173
|
-
const { getCommunitySettings } = await import("./settings-
|
|
173
|
+
const { getCommunitySettings } = await import("./settings-5PH4SNIX.js");
|
|
174
174
|
const { NpRateLimitError } = await import("./errors-5OS3S2J3.js");
|
|
175
175
|
const settings = await getCommunitySettings();
|
|
176
176
|
const { perDay, total } = settings.memberUploadQuota;
|
|
@@ -535,4 +535,4 @@ export {
|
|
|
535
535
|
listMedia,
|
|
536
536
|
cleanupDeletedMedia
|
|
537
537
|
};
|
|
538
|
-
//# sourceMappingURL=chunk-
|
|
538
|
+
//# sourceMappingURL=chunk-PXVCVEXV.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getPluginRegistration
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-K4IYOAZJ.js";
|
|
4
4
|
import {
|
|
5
5
|
getCurrentSiteId
|
|
6
6
|
} from "./chunk-SBCVAC2Z.js";
|
|
@@ -319,4 +319,4 @@ export {
|
|
|
319
319
|
setPluginConfig,
|
|
320
320
|
pluginConfigCacheTag
|
|
321
321
|
};
|
|
322
|
-
//# sourceMappingURL=chunk-
|
|
322
|
+
//# sourceMappingURL=chunk-RCK73HJI.js.map
|
|
@@ -145,7 +145,7 @@ function registerBuiltinHandlers() {
|
|
|
145
145
|
registerJobHandler("notifications:sendDigest", handleNotificationsSendDigest);
|
|
146
146
|
}
|
|
147
147
|
async function handleContentPublishScheduled(_) {
|
|
148
|
-
const { publishScheduledDocuments } = await import("./scheduled-
|
|
148
|
+
const { publishScheduledDocuments } = await import("./scheduled-N34EKDIJ.js");
|
|
149
149
|
const result = await publishScheduledDocuments();
|
|
150
150
|
if (result.published > 0) {
|
|
151
151
|
console.info(
|
|
@@ -193,7 +193,7 @@ async function handleMediaCleanup(data) {
|
|
|
193
193
|
async function handlePluginScheduledTask(data) {
|
|
194
194
|
if (isRecord(data) && typeof data.pluginId === "string" && typeof data.taskId === "string") {
|
|
195
195
|
try {
|
|
196
|
-
const { runPluginScheduledTask } = await import("./host-
|
|
196
|
+
const { runPluginScheduledTask } = await import("./host-6EAJQRCL.js");
|
|
197
197
|
await runPluginScheduledTask(data.pluginId, data.taskId);
|
|
198
198
|
return;
|
|
199
199
|
} catch (err) {
|
|
@@ -387,7 +387,7 @@ async function handleMemberSendPasswordReset(data) {
|
|
|
387
387
|
async function handleNotificationsSendDigest(data) {
|
|
388
388
|
const cadence = isRecord(data) && (data.cadence === "daily" || data.cadence === "weekly") ? data.cadence : "daily";
|
|
389
389
|
const siteName = isRecord(data) && typeof data.siteName === "string" ? data.siteName : void 0;
|
|
390
|
-
const { runDigestSweep } = await import("./digest-
|
|
390
|
+
const { runDigestSweep } = await import("./digest-U4X45I74.js");
|
|
391
391
|
const result = await runDigestSweep({ cadence, siteName });
|
|
392
392
|
console.info(
|
|
393
393
|
`[nexpress] notifications:sendDigest cadence=${cadence} considered=${result.considered} sent=${result.sent} skipped=${result.skipped} failed=${result.failed}`
|
|
@@ -673,7 +673,7 @@ var PgBossAdapter = class {
|
|
|
673
673
|
this.workRegistrations.push({ queueName, register });
|
|
674
674
|
await register();
|
|
675
675
|
}
|
|
676
|
-
const { getRegisteredPluginSchedules, runPluginScheduledTask } = await import("./host-
|
|
676
|
+
const { getRegisteredPluginSchedules, runPluginScheduledTask } = await import("./host-6EAJQRCL.js");
|
|
677
677
|
for (const schedule of getRegisteredPluginSchedules()) {
|
|
678
678
|
const queueName = `${toQueueName("plugin:scheduledTask")}.${schedule.pluginId}.${schedule.taskId}`;
|
|
679
679
|
await this.boss.createQueue(queueName);
|
|
@@ -775,7 +775,7 @@ var PgBossAdapter = class {
|
|
|
775
775
|
});
|
|
776
776
|
await this.boss.schedule(digestQueue, "0 8 * * *", { cadence: "daily" }, { key: "daily" });
|
|
777
777
|
await this.boss.schedule(digestQueue, "0 8 * * 1", { cadence: "weekly" }, { key: "weekly" });
|
|
778
|
-
const { getRegisteredPluginSchedules } = await import("./host-
|
|
778
|
+
const { getRegisteredPluginSchedules } = await import("./host-6EAJQRCL.js");
|
|
779
779
|
for (const schedule of getRegisteredPluginSchedules()) {
|
|
780
780
|
const pgBossName = `${toQueueName("plugin:scheduledTask")}.${schedule.pluginId}.${schedule.taskId}`;
|
|
781
781
|
await this.boss.schedule(pgBossName, schedule.cron, {
|
|
@@ -939,7 +939,7 @@ var PgBossAdapter = class {
|
|
|
939
939
|
* `workerOwnsRegistrations` so the admin UI can warn the operator.
|
|
940
940
|
*/
|
|
941
941
|
async reconcilePluginSchedules() {
|
|
942
|
-
const { getRegisteredPluginSchedules } = await import("./host-
|
|
942
|
+
const { getRegisteredPluginSchedules } = await import("./host-6EAJQRCL.js");
|
|
943
943
|
const wantedList = getRegisteredPluginSchedules();
|
|
944
944
|
const wantedByName = /* @__PURE__ */ new Map();
|
|
945
945
|
for (const schedule of wantedList) {
|
|
@@ -1267,4 +1267,4 @@ export {
|
|
|
1267
1267
|
stopWorker,
|
|
1268
1268
|
stopProducer
|
|
1269
1269
|
};
|
|
1270
|
-
//# sourceMappingURL=chunk-
|
|
1270
|
+
//# sourceMappingURL=chunk-UVLDAY4U.js.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
NP_DEFAULT_SITE_ID,
|
|
6
6
|
listSites
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-MYUCR3SE.js";
|
|
8
8
|
import {
|
|
9
9
|
getEmailAdapter
|
|
10
10
|
} from "./chunk-LSHHRDVR.js";
|
|
@@ -191,4 +191,4 @@ export {
|
|
|
191
191
|
buildDigestEmail,
|
|
192
192
|
runDigestSweep
|
|
193
193
|
};
|
|
194
|
-
//# sourceMappingURL=chunk-
|
|
194
|
+
//# sourceMappingURL=chunk-X43DI2QJ.js.map
|
package/dist/community.js
CHANGED
|
@@ -33,12 +33,12 @@ import {
|
|
|
33
33
|
unfollow,
|
|
34
34
|
unresolvedReportCount,
|
|
35
35
|
updateComment
|
|
36
|
-
} from "./chunk-
|
|
37
|
-
import "./chunk-
|
|
36
|
+
} from "./chunk-2LNXTEBM.js";
|
|
37
|
+
import "./chunk-JMNDARCQ.js";
|
|
38
38
|
import {
|
|
39
39
|
buildDigestEmail,
|
|
40
40
|
runDigestSweep
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-X43DI2QJ.js";
|
|
42
42
|
import {
|
|
43
43
|
assertNotBanned,
|
|
44
44
|
getCommunityRole,
|
|
@@ -47,14 +47,14 @@ import {
|
|
|
47
47
|
registerCommunityRole,
|
|
48
48
|
resetCommunityRoles,
|
|
49
49
|
withMemberWrite
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-BGEPMNVB.js";
|
|
51
51
|
import {
|
|
52
52
|
getMutedTargetIds,
|
|
53
53
|
isMuted,
|
|
54
54
|
listMutes,
|
|
55
55
|
muteMember,
|
|
56
56
|
unmuteMember
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-KS3S6TJS.js";
|
|
58
58
|
import {
|
|
59
59
|
getMemberNotificationPrefs,
|
|
60
60
|
isNotificationKindEnabled,
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
markNotificationsRead,
|
|
77
77
|
resolveMentionedMembers,
|
|
78
78
|
unreadNotificationCount
|
|
79
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-5NCTIJEN.js";
|
|
80
80
|
import {
|
|
81
81
|
applyReputation,
|
|
82
82
|
getReputationAdapter,
|
|
@@ -93,9 +93,9 @@ import {
|
|
|
93
93
|
resetProfanityAdapter,
|
|
94
94
|
setProfanityAdapter
|
|
95
95
|
} from "./chunk-KU5M27ZC.js";
|
|
96
|
-
import "./chunk-
|
|
97
|
-
import "./chunk-
|
|
98
|
-
import "./chunk-
|
|
96
|
+
import "./chunk-A3DPN2SP.js";
|
|
97
|
+
import "./chunk-K4IYOAZJ.js";
|
|
98
|
+
import "./chunk-PXVCVEXV.js";
|
|
99
99
|
import "./chunk-EQ2Z3KMD.js";
|
|
100
100
|
import {
|
|
101
101
|
listAuditEvents,
|
|
@@ -106,10 +106,10 @@ import {
|
|
|
106
106
|
getCommunitySettings,
|
|
107
107
|
updateCommunitySettings,
|
|
108
108
|
validateCommunitySettingsPatch
|
|
109
|
-
} from "./chunk-
|
|
109
|
+
} from "./chunk-KNSOZ2NN.js";
|
|
110
110
|
import "./chunk-EFZH6UPY.js";
|
|
111
111
|
import "./chunk-4ZLMEKFX.js";
|
|
112
|
-
import "./chunk-
|
|
112
|
+
import "./chunk-MYUCR3SE.js";
|
|
113
113
|
import "./chunk-SBCVAC2Z.js";
|
|
114
114
|
import "./chunk-ZCINJSS4.js";
|
|
115
115
|
import "./chunk-LSHHRDVR.js";
|
|
@@ -5,14 +5,14 @@ import {
|
|
|
5
5
|
isVersionedPluginConfig,
|
|
6
6
|
pluginConfigCacheTag,
|
|
7
7
|
setPluginConfig
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-RCK73HJI.js";
|
|
9
|
+
import "./chunk-K4IYOAZJ.js";
|
|
10
|
+
import "./chunk-PXVCVEXV.js";
|
|
11
11
|
import "./chunk-LMPYQLMH.js";
|
|
12
12
|
import "./chunk-2KNG5KMM.js";
|
|
13
13
|
import "./chunk-EFZH6UPY.js";
|
|
14
14
|
import "./chunk-4ZLMEKFX.js";
|
|
15
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-MYUCR3SE.js";
|
|
16
16
|
import "./chunk-SBCVAC2Z.js";
|
|
17
17
|
import "./chunk-ZCINJSS4.js";
|
|
18
18
|
import "./chunk-V2UNHGAP.js";
|
|
@@ -30,4 +30,4 @@ export {
|
|
|
30
30
|
pluginConfigCacheTag,
|
|
31
31
|
setPluginConfig
|
|
32
32
|
};
|
|
33
|
-
//# sourceMappingURL=config-
|
|
33
|
+
//# sourceMappingURL=config-WD2IMHKB.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildDigestEmail,
|
|
3
3
|
runDigestSweep
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-X43DI2QJ.js";
|
|
5
5
|
import "./chunk-I4FSVEJK.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-MYUCR3SE.js";
|
|
7
7
|
import "./chunk-ZCINJSS4.js";
|
|
8
8
|
import "./chunk-LSHHRDVR.js";
|
|
9
9
|
import "./chunk-Q7MK5ZKG.js";
|
|
@@ -14,4 +14,4 @@ export {
|
|
|
14
14
|
buildDigestEmail,
|
|
15
15
|
runDigestSweep
|
|
16
16
|
};
|
|
17
|
-
//# sourceMappingURL=digest-
|
|
17
|
+
//# sourceMappingURL=digest-U4X45I74.js.map
|
|
@@ -16,11 +16,11 @@ import {
|
|
|
16
16
|
runHookAndCollect,
|
|
17
17
|
runPluginScheduledTask,
|
|
18
18
|
schedulePluginTask
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import "./chunk-
|
|
19
|
+
} from "./chunk-K4IYOAZJ.js";
|
|
20
|
+
import "./chunk-PXVCVEXV.js";
|
|
21
21
|
import "./chunk-EFZH6UPY.js";
|
|
22
22
|
import "./chunk-4ZLMEKFX.js";
|
|
23
|
-
import "./chunk-
|
|
23
|
+
import "./chunk-MYUCR3SE.js";
|
|
24
24
|
import "./chunk-SBCVAC2Z.js";
|
|
25
25
|
import "./chunk-ZCINJSS4.js";
|
|
26
26
|
import "./chunk-V2UNHGAP.js";
|
|
@@ -49,4 +49,4 @@ export {
|
|
|
49
49
|
runPluginScheduledTask,
|
|
50
50
|
schedulePluginTask
|
|
51
51
|
};
|
|
52
|
-
//# sourceMappingURL=host-
|
|
52
|
+
//# sourceMappingURL=host-6EAJQRCL.js.map
|
package/dist/i18n.js
CHANGED
|
@@ -24,13 +24,13 @@ import {
|
|
|
24
24
|
setStrings,
|
|
25
25
|
t,
|
|
26
26
|
tSync
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-MLQKHRW2.js";
|
|
28
28
|
import {
|
|
29
29
|
getI18nConfig,
|
|
30
30
|
resetI18nConfig,
|
|
31
31
|
setI18nConfig
|
|
32
32
|
} from "./chunk-4ZLMEKFX.js";
|
|
33
|
-
import "./chunk-
|
|
33
|
+
import "./chunk-MYUCR3SE.js";
|
|
34
34
|
import "./chunk-SBCVAC2Z.js";
|
|
35
35
|
import "./chunk-ZCINJSS4.js";
|
|
36
36
|
import "./chunk-Q7MK5ZKG.js";
|