@bygghammar/cms-platform 0.45.0 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * One config contract for a whole site (Phase C).
3
+ *
4
+ * A site calls `defineSite({ config, seo, content, trips })` once and the
5
+ * package validates the shape at module-evaluation time, so a fresh copy fails
6
+ * immediately and with a readable message rather than half-way through a
7
+ * backoffice screen. No third-party validator is used: the checks are explicit
8
+ * so the error text can name the exact site file that is wrong.
9
+ */
10
+
11
+ import {
12
+ defineSiteConfig,
13
+ registerSiteConfig,
14
+ type SiteConfig,
15
+ type SiteConfigInput,
16
+ } from "./site-config";
17
+
18
+ /** SEO/route registry entry for one page key. */
19
+ export type SiteRouteEntry = {
20
+ /** Slug per locale, without a leading slash. Empty string means the home page. */
21
+ slug: Record<string, string>;
22
+ /** Optional per-locale title/description defaults. */
23
+ title?: Record<string, string>;
24
+ description?: Record<string, string>;
25
+ /** Excluded from the sitemap when false. */
26
+ indexable?: boolean;
27
+ };
28
+
29
+ /** The full site definition a host passes in. */
30
+ export type SiteDefinitionInput = {
31
+ config: SiteConfigInput;
32
+ /** Page key → route/SEO entry. */
33
+ seo: Record<string, SiteRouteEntry>;
34
+ /** Code defaults for the shared content sections. */
35
+ content: Record<string, unknown>;
36
+ /** Trips module configuration; required when `features.trips` is on. */
37
+ trips?: Record<string, unknown>;
38
+ };
39
+
40
+ /** The validated, registered site definition. */
41
+ export type SiteDefinition = {
42
+ config: SiteConfig;
43
+ seo: Record<string, SiteRouteEntry>;
44
+ content: Record<string, unknown>;
45
+ trips: Record<string, unknown>;
46
+ };
47
+
48
+ let current: SiteDefinition | null = null;
49
+
50
+ function fail(message: string): never {
51
+ throw new Error(`Invalid site definition — ${message} (check your src/site.config.ts).`);
52
+ }
53
+
54
+ /**
55
+ * Validates and registers the site definition. Returns it so the calling module
56
+ * has a real runtime export and cannot be tree-shaken away.
57
+ */
58
+ export function defineSite(input: SiteDefinitionInput): SiteDefinition {
59
+ const config = defineSiteConfig(input.config);
60
+
61
+ if (!input.seo || typeof input.seo !== "object") fail("`seo` must be a page-key → entry map");
62
+ const locales = config.locales;
63
+
64
+ const home = Object.entries(input.seo).find(([, entry]) =>
65
+ Object.values(entry.slug ?? {}).some((s) => s === ""),
66
+ );
67
+ if (Object.keys(input.seo).length > 0 && !home)
68
+ fail("no page in `seo` has an empty slug, so the site has no home page");
69
+
70
+ for (const [key, entry] of Object.entries(input.seo)) {
71
+ if (!entry || typeof entry !== "object") fail(`seo["${key}"] is not an object`);
72
+ if (!entry.slug || typeof entry.slug !== "object") fail(`seo["${key}"].slug is missing`);
73
+ for (const locale of locales) {
74
+ if (typeof entry.slug[locale] !== "string")
75
+ fail(`seo["${key}"].slug is missing the "${locale}" locale`);
76
+ }
77
+ }
78
+
79
+ if (!input.content || typeof input.content !== "object")
80
+ fail("`content` must be an object of shared-section defaults");
81
+
82
+ if (config.features.trips && !input.trips)
83
+ fail("`features.trips` is enabled but no `trips` config was supplied");
84
+
85
+ const definition: SiteDefinition = {
86
+ config: registerSiteConfig(config),
87
+ seo: input.seo,
88
+ content: input.content,
89
+ trips: input.trips ?? {},
90
+ };
91
+ current = definition;
92
+ return definition;
93
+ }
94
+
95
+ /** Reads the registered site definition, failing loudly when absent. */
96
+ export function siteDefinition(): SiteDefinition {
97
+ if (!current)
98
+ throw new Error(
99
+ "Site definition not registered — call defineSite() from src/site.config.ts before rendering.",
100
+ );
101
+ return current;
102
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Canonical schema baseline shipped with this package version.
3
+ *
4
+ * `SCHEMA_MIGRATIONS` is the ordered list of migration files under
5
+ * `@bygghammar/cms-platform/migrations`. A site compares it to the migrations
6
+ * its database has actually applied so drift is visible in the backoffice
7
+ * version screen instead of surfacing as a runtime error months later.
8
+ */
9
+
10
+ /** Every canonical migration filename, oldest first. */
11
+ export const SCHEMA_MIGRATIONS: readonly string[] = [
12
+ "20260728113741_f9744b19-078b-46e5-b00f-31c84aaf71ab.sql",
13
+ "20260728113757_4156a886-21fb-4a1f-9c12-acb7bb145102.sql",
14
+ "20260728113823_6e0237e1-99e3-4e1a-8a68-804b7bcdbbb8.sql",
15
+ "20260728115305_506d92b4-129f-4d2b-961f-cf76e592677e.sql",
16
+ "20260728115320_717d4447-f433-4a1d-89d2-fc8f028d4ac6.sql",
17
+ "20260728115347_c77f5a56-19db-4117-bb60-9ad692b817ab.sql",
18
+ "20260728132052_b692dcfe-a944-4a19-afd0-187f7600feb8.sql",
19
+ "20260728183318_05b05a47-ba05-4a66-ab8f-69f2688fea84.sql",
20
+ "20260728193035_84b68d27-c3f3-4516-a3a9-15fc6f476163.sql",
21
+ "20260728193707_8438ee2d-d589-4048-86e2-464fd209af80.sql",
22
+ "20260729055312_08862d7e-0279-4d4f-8709-a59b9f068c55.sql",
23
+ "20260729055348_418b7e36-4877-4304-831f-290ca73e56f3.sql",
24
+ "20260729055358_20b467e2-4efd-42c1-bd34-773ed6b77fc5.sql",
25
+ "20260729061039_306859eb-a38c-44ef-af1a-6c02a8fd84db.sql",
26
+ "20260730065600_54c25861-3355-4600-a72f-3d67b457ecb1.sql",
27
+ "20260730071745_d4904314-15ff-4c58-ac66-85e7371e04b5.sql",
28
+ "20260730071914_48163aa3-830e-4dcc-bdd4-c35be282ab87.sql",
29
+ "20260730085949_bcfc3270-ea80-4617-86a3-2555ed34a280.sql",
30
+ "20260730095256_c977c30e-b285-4041-8804-c0bd72b6d9b3.sql",
31
+ "20260730100057_e341b887-eb0e-4a74-850e-0ee5ffb70d7a.sql",
32
+ "20260730103703_d5c4e1b7-1e2c-4dd0-be4e-185c159ade69.sql",
33
+ "20260730105635_3557b7da-919e-48db-8c92-8221d68d3cf9.sql",
34
+ "20260730120308_12ff8cfb-43f9-4070-abde-61531ad3b75f.sql",
35
+ "20260730125024_1baf8926-12a2-48f4-8edc-7309941309e9.sql",
36
+ "20260730173510_feb974d1-510e-4f52-9e20-5fd0582d8952.sql",
37
+ "20260730175409_9f60743f-f5e1-418c-a700-c91fb3704db8.sql",
38
+ "20260730181224_1c52ff16-c7a2-4ef1-a0a8-e493c2c9106f.sql",
39
+ "20260730183129_933adb75-f985-4d82-ad5d-213b69fd42e5.sql",
40
+ "20260730183918_7bc7a827-2100-49f1-bc6e-24509249350b.sql",
41
+ "20260730202628_7ceade5e-644c-4bc2-91b8-594133329f31.sql",
42
+ "20260730211056_57f42049-00b4-473b-a2cd-65a13570b8d2.sql",
43
+ "20260730214340_98af2e55-9031-4c11-9659-c47a37a8d6b7.sql",
44
+ "20260731091630_8a6f28ce-5833-4901-ab17-27648cef32ec.sql",
45
+ "20260731094716_4c68a1df-83ed-4ba8-8019-fa2fe2b9681e.sql",
46
+ "20260731191655_fef97c3c-1397-47b4-8b56-afb020298e97.sql",
47
+ "20260731192212_95772a2f-19a1-4013-b463-2eb161e96737.sql",
48
+ "20260731192815_c490653e-1232-4ad9-a150-3c83d2b90f5e.sql",
49
+ "20260731193135_7966e740-5e86-4b14-8e1d-5119069666ba.sql",
50
+ "20260731193245_599790d0-ca5e-4b5d-a3e7-d69b2a79e1a3.sql",
51
+ "20260731212959_a748cb0d-4dc7-45dc-be90-f6bacbfab26c.sql",
52
+ "20260801003057_1195f4c1-fb54-42dc-9718-638a25e6a82e.sql",
53
+ "20260801003300_fc591960-cb41-41cf-837d-2eef23b29ca9.sql",
54
+ "20260802151445_a56b0023-eaf7-41e2-a7fb-d4beddfc587c.sql",
55
+ "20260802202621_d661af8a-8513-4b54-b375-f963ae824c8f.sql",
56
+ "20260802202702_c30cd837-744c-4641-85f1-edd3a7ebbcb6.sql",
57
+ "20260802202720_4418d18d-c851-42dc-9345-78fc330128ae.sql",
58
+ "20260803132514_78ba1f53-c85b-4457-96b1-7d49c5c8b494.sql",
59
+ "20260803181201_28c8aa09-baa8-431c-8338-90b1af9c8172.sql",
60
+ "20260803201518_6e8a6028-b70f-4a7d-8bc9-95c86304e7ae.sql",
61
+ "20260803201535_32a9a213-cb92-4035-b8cd-8211ca34af68.sql",
62
+ "20260811103505_29df753d-14fa-4a05-8fea-f4207785fba3.sql",
63
+ "20260811130318_7cb95e95-9baa-40af-8f3f-de1d4e09cec4.sql",
64
+ "20260818040722_1bdf1b8f-7fb0-4ae8-8ecf-73f1fadff3c2.sql",
65
+ "20260818040808_69a5e832-57e8-4aa7-aff8-7fd7777faa7c.sql",
66
+ "20260818040839_e6523163-bbf5-4a39-9233-91759059569a.sql",
67
+ ];
68
+
69
+ /** Schema baseline version = the newest canonical migration. */
70
+ export const SCHEMA_BASELINE = SCHEMA_MIGRATIONS[SCHEMA_MIGRATIONS.length - 1] ?? "";
71
+
72
+ /** The result of comparing a database against the packaged baseline. */
73
+ export type SchemaDrift = {
74
+ /** True when the database matches the packaged baseline exactly. */
75
+ inSync: boolean;
76
+ /** Canonical migrations the database has not applied. */
77
+ missing: string[];
78
+ /** Migrations applied in the database that this package does not know about. */
79
+ extra: string[];
80
+ expectedCount: number;
81
+ appliedCount: number;
82
+ baseline: string;
83
+ };
84
+
85
+ /**
86
+ * Compares applied migration names (as recorded by the migration tooling)
87
+ * against the packaged baseline. Version suffixes are ignored so a site that
88
+ * records `20260728113741` matches `20260728113741_<uuid>.sql`.
89
+ */
90
+ export function compareSchema(applied: readonly string[]): SchemaDrift {
91
+ const norm = (n: string) => n.replace(/\.sql$/, "").split("_")[0] ?? n;
92
+ const appliedIds = new Set(applied.map(norm));
93
+ const expectedIds = new Map(SCHEMA_MIGRATIONS.map((n) => [norm(n), n]));
94
+
95
+ const missing = [...expectedIds]
96
+ .filter(([id]) => !appliedIds.has(id))
97
+ .map(([, name]) => name);
98
+ const extra = applied.filter((n) => !expectedIds.has(norm(n)));
99
+
100
+ return {
101
+ inSync: missing.length === 0 && extra.length === 0,
102
+ missing,
103
+ extra,
104
+ expectedCount: SCHEMA_MIGRATIONS.length,
105
+ appliedCount: applied.length,
106
+ baseline: SCHEMA_BASELINE,
107
+ };
108
+ }