@velastack/cms 0.1.0 → 0.2.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.
Files changed (95) hide show
  1. package/README.md +1 -1
  2. package/dist/backend/auth/editors.d.ts +51 -0
  3. package/dist/backend/auth/editors.js +157 -0
  4. package/dist/backend/auth/local-editors.d.ts +22 -0
  5. package/dist/backend/auth/local-editors.js +44 -0
  6. package/dist/backend/auth/scrypt.d.ts +20 -0
  7. package/dist/backend/auth/scrypt.js +80 -0
  8. package/dist/backend/factory.d.ts +25 -0
  9. package/dist/backend/factory.js +213 -0
  10. package/dist/backend/http.d.ts +34 -0
  11. package/dist/backend/http.js +79 -0
  12. package/dist/backend/index.d.ts +45 -0
  13. package/dist/backend/index.js +39 -0
  14. package/dist/backend/router.d.ts +52 -0
  15. package/dist/backend/router.js +57 -0
  16. package/dist/backend/routes/context.d.ts +46 -0
  17. package/dist/backend/routes/context.js +30 -0
  18. package/dist/backend/routes/docs.d.ts +2 -0
  19. package/dist/backend/routes/docs.js +150 -0
  20. package/dist/backend/routes/login.d.ts +19 -0
  21. package/dist/backend/routes/login.js +155 -0
  22. package/dist/backend/routes/media.d.ts +22 -0
  23. package/dist/backend/routes/media.js +119 -0
  24. package/dist/backend/routes/pages.d.ts +19 -0
  25. package/dist/backend/routes/pages.js +221 -0
  26. package/dist/backend/routes/release.d.ts +10 -0
  27. package/dist/backend/routes/release.js +147 -0
  28. package/dist/backend/routes/site.d.ts +2 -0
  29. package/dist/backend/routes/site.js +40 -0
  30. package/dist/backend/routes/table.d.ts +11 -0
  31. package/dist/backend/routes/table.js +72 -0
  32. package/dist/backend/routes/test-reset.d.ts +15 -0
  33. package/dist/backend/routes/test-reset.js +112 -0
  34. package/dist/backend/routes/user.d.ts +14 -0
  35. package/dist/backend/routes/user.js +21 -0
  36. package/dist/backend/storage.d.ts +18 -0
  37. package/dist/backend/storage.js +45 -0
  38. package/dist/backend/store/cache.d.ts +34 -0
  39. package/dist/backend/store/cache.js +38 -0
  40. package/dist/backend/store/db.d.ts +10 -0
  41. package/dist/backend/store/db.js +29 -0
  42. package/dist/backend/store/migrate.d.ts +10 -0
  43. package/dist/backend/store/migrate.js +30 -0
  44. package/dist/backend/store/migrations/0001-init.d.ts +1 -0
  45. package/dist/backend/store/migrations/0001-init.js +129 -0
  46. package/dist/backend/store/migrations/0002-site.d.ts +1 -0
  47. package/dist/backend/store/migrations/0002-site.js +86 -0
  48. package/dist/backend/store/migrations/0003-editors.d.ts +1 -0
  49. package/dist/backend/store/migrations/0003-editors.js +44 -0
  50. package/dist/backend/store/migrations/index.d.ts +15 -0
  51. package/dist/backend/store/migrations/index.js +18 -0
  52. package/dist/backend/store/queries.d.ts +67 -0
  53. package/dist/backend/store/queries.js +1207 -0
  54. package/dist/backend/store/sqlite.d.ts +22 -0
  55. package/dist/backend/store/sqlite.js +29 -0
  56. package/dist/backend/testing/fixtures.d.ts +39 -0
  57. package/dist/backend/testing/fixtures.js +61 -0
  58. package/dist/backend/testing/harness.d.ts +35 -0
  59. package/dist/backend/testing/harness.js +149 -0
  60. package/dist/backend/types.d.ts +106 -0
  61. package/dist/cms.d.ts +3 -3
  62. package/dist/cms.js +1 -1
  63. package/dist/components/admin-bar/publish-dialog.svelte +1 -1
  64. package/dist/components/cms/cms-image-editable.svelte +1 -1
  65. package/dist/components/cms/cms-store.svelte.d.ts +3 -68
  66. package/dist/components/cms/cms-store.svelte.js +25 -10
  67. package/dist/components/cms/locale-merge.d.ts +1 -1
  68. package/dist/components/cms/locale-merge.js +1 -1
  69. package/dist/components/cms/scope.d.ts +1 -1
  70. package/dist/core/page-entry.d.ts +43 -0
  71. package/dist/core/page-entry.js +21 -0
  72. package/dist/{components/cms → core}/path.d.ts +13 -0
  73. package/dist/{components/cms → core}/path.js +36 -0
  74. package/dist/core/wire.d.ts +182 -0
  75. package/dist/core/wire.js +1 -0
  76. package/dist/{srv → server}/api-adapter.js +2 -2
  77. package/dist/{srv → server}/load-cms.js +1 -1
  78. package/dist/{srv → server}/mock-adapter.d.ts +3 -24
  79. package/dist/{srv → server}/mock-adapter.js +5 -22
  80. package/dist/{srv → server}/types.d.ts +2 -6
  81. package/dist/server/types.js +1 -0
  82. package/package.json +13 -4
  83. package/plugin/dist/index.js +7 -0
  84. package/plugin/dist/index.js.map +1 -1
  85. package/plugin/dist/media.d.ts +16 -7
  86. package/plugin/dist/media.js +23 -10
  87. package/plugin/dist/media.js.map +1 -1
  88. /package/dist/{srv → backend}/types.js +0 -0
  89. /package/dist/{srv → server}/README.md +0 -0
  90. /package/dist/{srv → server}/api-adapter.d.ts +0 -0
  91. /package/dist/{srv → server}/cms.d.ts +0 -0
  92. /package/dist/{srv → server}/cms.js +0 -0
  93. /package/dist/{srv → server}/index.d.ts +0 -0
  94. /package/dist/{srv → server}/index.js +0 -0
  95. /package/dist/{srv → server}/load-cms.d.ts +0 -0
@@ -0,0 +1 @@
1
+ export declare const sql = "\n-- Live published state for pages. One row per (project, locale, route, params).\n-- Hot-path read: GET /docs?kind=page&... is one PK lookup against this table.\nCREATE TABLE published_pages (\n project_id TEXT NOT NULL,\n locale TEXT NOT NULL,\n route_id TEXT NOT NULL,\n params_hash TEXT NOT NULL,\n params TEXT NOT NULL,\n tree TEXT NOT NULL,\n tombstone TEXT,\n updated_at TEXT NOT NULL,\n updated_by_release TEXT NOT NULL,\n PRIMARY KEY (project_id, locale, route_id, params_hash)\n) WITHOUT ROWID;\nCREATE INDEX idx_pp_listing ON published_pages (project_id, locale, route_id);\nCREATE INDEX idx_pp_param_values ON published_pages (project_id, route_id);\n\n-- Live published state for layouts. One row per (project, locale, route).\nCREATE TABLE published_layouts (\n project_id TEXT NOT NULL,\n locale TEXT NOT NULL,\n route_id TEXT NOT NULL,\n tree TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n updated_by_release TEXT NOT NULL,\n PRIMARY KEY (project_id, locale, route_id)\n) WITHOUT ROWID;\n\n-- Published release headers, in monotonic order (`seq` per project).\nCREATE TABLE releases (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n name TEXT,\n published_by TEXT NOT NULL,\n published_at TEXT NOT NULL,\n preview_key TEXT NOT NULL,\n reverted_at TEXT\n);\nCREATE UNIQUE INDEX idx_releases_seq ON releases (project_id, seq DESC);\nCREATE UNIQUE INDEX idx_releases_preview ON releases (project_id, preview_key);\nCREATE INDEX idx_releases_history ON releases (project_id, published_at DESC);\n\n-- Append-only log of every published item. Drives ?version=<key> reads,\n-- revert (capture priorTree at publish, replay to undo), and history view.\nCREATE TABLE release_items (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n release_id TEXT NOT NULL REFERENCES releases(id) ON DELETE CASCADE,\n project_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('page', 'layout', 'page-delete')),\n route_id TEXT NOT NULL,\n locale TEXT NOT NULL,\n params_hash TEXT,\n params TEXT,\n tree TEXT,\n prior_tree TEXT,\n outcome TEXT,\n prior_tombstone TEXT,\n added_at TEXT NOT NULL\n);\nCREATE INDEX idx_ri_release ON release_items (release_id);\nCREATE INDEX idx_ri_replay ON release_items (project_id, seq DESC);\nCREATE INDEX idx_ri_scope ON release_items\n (project_id, locale, route_id, params_hash, seq DESC);\n\n-- One open (draft) release per (project, user). Drafts overlay published reads\n-- when a request carries ?preview=<open_releases.preview_key>.\nCREATE TABLE open_releases (\n project_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n name TEXT,\n created_at TEXT NOT NULL,\n preview_key TEXT NOT NULL,\n PRIMARY KEY (project_id, user_id)\n);\nCREATE UNIQUE INDEX idx_or_preview ON open_releases (project_id, preview_key);\n\n-- Items in open releases. UNIQUE on scope so addReleaseItems can UPSERT-merge.\n-- params_hash is '' (canonical empty) for layouts to fit the unique constraint.\nCREATE TABLE open_release_items (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('page', 'layout', 'page-delete')),\n route_id TEXT NOT NULL,\n locale TEXT NOT NULL,\n params_hash TEXT NOT NULL DEFAULT '',\n params TEXT,\n tree TEXT,\n outcome TEXT,\n added_at TEXT NOT NULL,\n FOREIGN KEY (project_id, user_id)\n REFERENCES open_releases(project_id, user_id) ON DELETE CASCADE\n);\nCREATE INDEX idx_ori_release ON open_release_items (project_id, user_id);\nCREATE UNIQUE INDEX idx_ori_scope ON open_release_items\n (project_id, user_id, kind, route_id, locale, params_hash);\n\n-- Media library metadata. Files themselves live on disk under static/uploads/.\nCREATE TABLE media_items (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n filename TEXT NOT NULL,\n original_name TEXT NOT NULL,\n mime TEXT NOT NULL,\n size INTEGER NOT NULL,\n url TEXT NOT NULL,\n uploaded_at TEXT NOT NULL,\n uploaded_by TEXT NOT NULL\n);\nCREATE INDEX idx_media_listing ON media_items (project_id, uploaded_at DESC, id);\n\n-- Per-project version stamp. Bumped on every publish. Used as an ETag basis\n-- and to invalidate the in-process LRU cache.\nCREATE TABLE project_state (\n project_id TEXT PRIMARY KEY,\n cms_version INTEGER NOT NULL DEFAULT 0,\n last_release_seq INTEGER NOT NULL DEFAULT 0\n);\n";
@@ -0,0 +1,129 @@
1
+ // Verbatim copy of the original `0001_init.sql`, inlined as a template literal.
2
+ //
3
+ // The runner used `import.meta.glob('./migrations/*.sql', { query: '?raw' })`,
4
+ // which is a Vite-only API: it resolves to nothing once this code is a compiled
5
+ // dependency inside a consumer's node_modules. Inlining is what makes the
6
+ // migrations portable. Escaping: backticks and `${` must be escaped here, and
7
+ // nothing else changes.
8
+ export const sql = `
9
+ -- Live published state for pages. One row per (project, locale, route, params).
10
+ -- Hot-path read: GET /docs?kind=page&... is one PK lookup against this table.
11
+ CREATE TABLE published_pages (
12
+ project_id TEXT NOT NULL,
13
+ locale TEXT NOT NULL,
14
+ route_id TEXT NOT NULL,
15
+ params_hash TEXT NOT NULL,
16
+ params TEXT NOT NULL,
17
+ tree TEXT NOT NULL,
18
+ tombstone TEXT,
19
+ updated_at TEXT NOT NULL,
20
+ updated_by_release TEXT NOT NULL,
21
+ PRIMARY KEY (project_id, locale, route_id, params_hash)
22
+ ) WITHOUT ROWID;
23
+ CREATE INDEX idx_pp_listing ON published_pages (project_id, locale, route_id);
24
+ CREATE INDEX idx_pp_param_values ON published_pages (project_id, route_id);
25
+
26
+ -- Live published state for layouts. One row per (project, locale, route).
27
+ CREATE TABLE published_layouts (
28
+ project_id TEXT NOT NULL,
29
+ locale TEXT NOT NULL,
30
+ route_id TEXT NOT NULL,
31
+ tree TEXT NOT NULL,
32
+ updated_at TEXT NOT NULL,
33
+ updated_by_release TEXT NOT NULL,
34
+ PRIMARY KEY (project_id, locale, route_id)
35
+ ) WITHOUT ROWID;
36
+
37
+ -- Published release headers, in monotonic order (\`seq\` per project).
38
+ CREATE TABLE releases (
39
+ id TEXT PRIMARY KEY,
40
+ project_id TEXT NOT NULL,
41
+ seq INTEGER NOT NULL,
42
+ name TEXT,
43
+ published_by TEXT NOT NULL,
44
+ published_at TEXT NOT NULL,
45
+ preview_key TEXT NOT NULL,
46
+ reverted_at TEXT
47
+ );
48
+ CREATE UNIQUE INDEX idx_releases_seq ON releases (project_id, seq DESC);
49
+ CREATE UNIQUE INDEX idx_releases_preview ON releases (project_id, preview_key);
50
+ CREATE INDEX idx_releases_history ON releases (project_id, published_at DESC);
51
+
52
+ -- Append-only log of every published item. Drives ?version=<key> reads,
53
+ -- revert (capture priorTree at publish, replay to undo), and history view.
54
+ CREATE TABLE release_items (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ release_id TEXT NOT NULL REFERENCES releases(id) ON DELETE CASCADE,
57
+ project_id TEXT NOT NULL,
58
+ seq INTEGER NOT NULL,
59
+ kind TEXT NOT NULL CHECK (kind IN ('page', 'layout', 'page-delete')),
60
+ route_id TEXT NOT NULL,
61
+ locale TEXT NOT NULL,
62
+ params_hash TEXT,
63
+ params TEXT,
64
+ tree TEXT,
65
+ prior_tree TEXT,
66
+ outcome TEXT,
67
+ prior_tombstone TEXT,
68
+ added_at TEXT NOT NULL
69
+ );
70
+ CREATE INDEX idx_ri_release ON release_items (release_id);
71
+ CREATE INDEX idx_ri_replay ON release_items (project_id, seq DESC);
72
+ CREATE INDEX idx_ri_scope ON release_items
73
+ (project_id, locale, route_id, params_hash, seq DESC);
74
+
75
+ -- One open (draft) release per (project, user). Drafts overlay published reads
76
+ -- when a request carries ?preview=<open_releases.preview_key>.
77
+ CREATE TABLE open_releases (
78
+ project_id TEXT NOT NULL,
79
+ user_id TEXT NOT NULL,
80
+ name TEXT,
81
+ created_at TEXT NOT NULL,
82
+ preview_key TEXT NOT NULL,
83
+ PRIMARY KEY (project_id, user_id)
84
+ );
85
+ CREATE UNIQUE INDEX idx_or_preview ON open_releases (project_id, preview_key);
86
+
87
+ -- Items in open releases. UNIQUE on scope so addReleaseItems can UPSERT-merge.
88
+ -- params_hash is '' (canonical empty) for layouts to fit the unique constraint.
89
+ CREATE TABLE open_release_items (
90
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
91
+ project_id TEXT NOT NULL,
92
+ user_id TEXT NOT NULL,
93
+ kind TEXT NOT NULL CHECK (kind IN ('page', 'layout', 'page-delete')),
94
+ route_id TEXT NOT NULL,
95
+ locale TEXT NOT NULL,
96
+ params_hash TEXT NOT NULL DEFAULT '',
97
+ params TEXT,
98
+ tree TEXT,
99
+ outcome TEXT,
100
+ added_at TEXT NOT NULL,
101
+ FOREIGN KEY (project_id, user_id)
102
+ REFERENCES open_releases(project_id, user_id) ON DELETE CASCADE
103
+ );
104
+ CREATE INDEX idx_ori_release ON open_release_items (project_id, user_id);
105
+ CREATE UNIQUE INDEX idx_ori_scope ON open_release_items
106
+ (project_id, user_id, kind, route_id, locale, params_hash);
107
+
108
+ -- Media library metadata. Files themselves live on disk under static/uploads/.
109
+ CREATE TABLE media_items (
110
+ id TEXT PRIMARY KEY,
111
+ project_id TEXT NOT NULL,
112
+ filename TEXT NOT NULL,
113
+ original_name TEXT NOT NULL,
114
+ mime TEXT NOT NULL,
115
+ size INTEGER NOT NULL,
116
+ url TEXT NOT NULL,
117
+ uploaded_at TEXT NOT NULL,
118
+ uploaded_by TEXT NOT NULL
119
+ );
120
+ CREATE INDEX idx_media_listing ON media_items (project_id, uploaded_at DESC, id);
121
+
122
+ -- Per-project version stamp. Bumped on every publish. Used as an ETag basis
123
+ -- and to invalidate the in-process LRU cache.
124
+ CREATE TABLE project_state (
125
+ project_id TEXT PRIMARY KEY,
126
+ cms_version INTEGER NOT NULL DEFAULT 0,
127
+ last_release_seq INTEGER NOT NULL DEFAULT 0
128
+ );
129
+ `;
@@ -0,0 +1 @@
1
+ export declare const sql = "\n-- Add the `site` kind: a single project-scoped tree of non-localized,\n-- non-route-bound settings (business name, logo, contact, analytics, \u2026).\n--\n-- Three changes:\n-- 1. New `published_site` table, one row per project.\n-- 2. Extend the kind CHECK on `open_release_items` and `release_items` so a\n-- site draft item can land in the same release flow as page/layout items.\n-- SQLite can't ALTER a CHECK constraint in place, so we rebuild the two\n-- tables. Site items use sentinel `__site__` values for the route_id /\n-- locale / params_hash columns to satisfy the existing NOT NULL columns\n-- and the (kind, route_id, locale, params_hash) unique index \u2014 only one\n-- site draft per user.\n\nPRAGMA foreign_keys = OFF;\n\nCREATE TABLE published_site (\n project_id TEXT NOT NULL PRIMARY KEY,\n tree TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n updated_by_release TEXT NOT NULL\n) WITHOUT ROWID;\n\n-- Rebuild open_release_items with 'site' added to the kind CHECK.\nCREATE TABLE open_release_items_new (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('page', 'layout', 'page-delete', 'site')),\n route_id TEXT NOT NULL,\n locale TEXT NOT NULL,\n params_hash TEXT NOT NULL DEFAULT '',\n params TEXT,\n tree TEXT,\n outcome TEXT,\n added_at TEXT NOT NULL,\n FOREIGN KEY (project_id, user_id)\n REFERENCES open_releases(project_id, user_id) ON DELETE CASCADE\n);\nINSERT INTO open_release_items_new\n (id, project_id, user_id, kind, route_id, locale, params_hash, params, tree, outcome, added_at)\n SELECT id, project_id, user_id, kind, route_id, locale, params_hash, params, tree, outcome, added_at\n FROM open_release_items;\nDROP TABLE open_release_items;\nALTER TABLE open_release_items_new RENAME TO open_release_items;\nCREATE INDEX idx_ori_release ON open_release_items (project_id, user_id);\nCREATE UNIQUE INDEX idx_ori_scope ON open_release_items\n (project_id, user_id, kind, route_id, locale, params_hash);\n\n-- Rebuild release_items with 'site' added to the kind CHECK.\nCREATE TABLE release_items_new (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n release_id TEXT NOT NULL REFERENCES releases(id) ON DELETE CASCADE,\n project_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('page', 'layout', 'page-delete', 'site')),\n route_id TEXT NOT NULL,\n locale TEXT NOT NULL,\n params_hash TEXT,\n params TEXT,\n tree TEXT,\n prior_tree TEXT,\n outcome TEXT,\n prior_tombstone TEXT,\n added_at TEXT NOT NULL\n);\nINSERT INTO release_items_new\n (id, release_id, project_id, seq, kind, route_id, locale, params_hash, params, tree, prior_tree, outcome, prior_tombstone, added_at)\n SELECT id, release_id, project_id, seq, kind, route_id, locale, params_hash, params, tree, prior_tree, outcome, prior_tombstone, added_at\n FROM release_items;\nDROP TABLE release_items;\nALTER TABLE release_items_new RENAME TO release_items;\nCREATE INDEX idx_ri_release ON release_items (release_id);\nCREATE INDEX idx_ri_replay ON release_items (project_id, seq DESC);\nCREATE INDEX idx_ri_scope ON release_items\n (project_id, locale, route_id, params_hash, seq DESC);\n\nPRAGMA foreign_keys = ON;\n";
@@ -0,0 +1,86 @@
1
+ // Verbatim copy of the original `0002_site.sql`, inlined as a template literal.
2
+ //
3
+ // The runner used `import.meta.glob('./migrations/*.sql', { query: '?raw' })`,
4
+ // which is a Vite-only API: it resolves to nothing once this code is a compiled
5
+ // dependency inside a consumer's node_modules. Inlining is what makes the
6
+ // migrations portable. Escaping: backticks and `${` must be escaped here, and
7
+ // nothing else changes.
8
+ export const sql = `
9
+ -- Add the \`site\` kind: a single project-scoped tree of non-localized,
10
+ -- non-route-bound settings (business name, logo, contact, analytics, …).
11
+ --
12
+ -- Three changes:
13
+ -- 1. New \`published_site\` table, one row per project.
14
+ -- 2. Extend the kind CHECK on \`open_release_items\` and \`release_items\` so a
15
+ -- site draft item can land in the same release flow as page/layout items.
16
+ -- SQLite can't ALTER a CHECK constraint in place, so we rebuild the two
17
+ -- tables. Site items use sentinel \`__site__\` values for the route_id /
18
+ -- locale / params_hash columns to satisfy the existing NOT NULL columns
19
+ -- and the (kind, route_id, locale, params_hash) unique index — only one
20
+ -- site draft per user.
21
+
22
+ PRAGMA foreign_keys = OFF;
23
+
24
+ CREATE TABLE published_site (
25
+ project_id TEXT NOT NULL PRIMARY KEY,
26
+ tree TEXT NOT NULL,
27
+ updated_at TEXT NOT NULL,
28
+ updated_by_release TEXT NOT NULL
29
+ ) WITHOUT ROWID;
30
+
31
+ -- Rebuild open_release_items with 'site' added to the kind CHECK.
32
+ CREATE TABLE open_release_items_new (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ project_id TEXT NOT NULL,
35
+ user_id TEXT NOT NULL,
36
+ kind TEXT NOT NULL CHECK (kind IN ('page', 'layout', 'page-delete', 'site')),
37
+ route_id TEXT NOT NULL,
38
+ locale TEXT NOT NULL,
39
+ params_hash TEXT NOT NULL DEFAULT '',
40
+ params TEXT,
41
+ tree TEXT,
42
+ outcome TEXT,
43
+ added_at TEXT NOT NULL,
44
+ FOREIGN KEY (project_id, user_id)
45
+ REFERENCES open_releases(project_id, user_id) ON DELETE CASCADE
46
+ );
47
+ INSERT INTO open_release_items_new
48
+ (id, project_id, user_id, kind, route_id, locale, params_hash, params, tree, outcome, added_at)
49
+ SELECT id, project_id, user_id, kind, route_id, locale, params_hash, params, tree, outcome, added_at
50
+ FROM open_release_items;
51
+ DROP TABLE open_release_items;
52
+ ALTER TABLE open_release_items_new RENAME TO open_release_items;
53
+ CREATE INDEX idx_ori_release ON open_release_items (project_id, user_id);
54
+ CREATE UNIQUE INDEX idx_ori_scope ON open_release_items
55
+ (project_id, user_id, kind, route_id, locale, params_hash);
56
+
57
+ -- Rebuild release_items with 'site' added to the kind CHECK.
58
+ CREATE TABLE release_items_new (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ release_id TEXT NOT NULL REFERENCES releases(id) ON DELETE CASCADE,
61
+ project_id TEXT NOT NULL,
62
+ seq INTEGER NOT NULL,
63
+ kind TEXT NOT NULL CHECK (kind IN ('page', 'layout', 'page-delete', 'site')),
64
+ route_id TEXT NOT NULL,
65
+ locale TEXT NOT NULL,
66
+ params_hash TEXT,
67
+ params TEXT,
68
+ tree TEXT,
69
+ prior_tree TEXT,
70
+ outcome TEXT,
71
+ prior_tombstone TEXT,
72
+ added_at TEXT NOT NULL
73
+ );
74
+ INSERT INTO release_items_new
75
+ (id, release_id, project_id, seq, kind, route_id, locale, params_hash, params, tree, prior_tree, outcome, prior_tombstone, added_at)
76
+ SELECT id, release_id, project_id, seq, kind, route_id, locale, params_hash, params, tree, prior_tree, outcome, prior_tombstone, added_at
77
+ FROM release_items;
78
+ DROP TABLE release_items;
79
+ ALTER TABLE release_items_new RENAME TO release_items;
80
+ CREATE INDEX idx_ri_release ON release_items (release_id);
81
+ CREATE INDEX idx_ri_replay ON release_items (project_id, seq DESC);
82
+ CREATE INDEX idx_ri_scope ON release_items
83
+ (project_id, locale, route_id, params_hash, seq DESC);
84
+
85
+ PRAGMA foreign_keys = ON;
86
+ `;
@@ -0,0 +1 @@
1
+ export declare const sql = "\nCREATE TABLE cms_editors (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL,\n -- Lookups are case-insensitive, but the address is shown back as entered.\n email_lower TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n password_hash TEXT NOT NULL,\n created_at TEXT NOT NULL,\n disabled_at TEXT\n);\n\n-- Authorization. One row per (project, editor) the editor may edit; the absence\n-- of a row is a 403. No FK on project_id for the reason above.\nCREATE TABLE cms_project_editors (\n project_id TEXT NOT NULL,\n editor_id TEXT NOT NULL REFERENCES cms_editors(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'editor',\n granted_at TEXT NOT NULL,\n PRIMARY KEY (project_id, editor_id)\n) WITHOUT ROWID;\nCREATE INDEX idx_cpe_editor ON cms_project_editors (editor_id);\n\n-- Sessions are stored by SHA-256 of the token, never the token itself, so a\n-- database read (a backup, a leaked file) yields nothing that can be replayed\n-- as a cookie. The cookie carries the raw token.\nCREATE TABLE cms_sessions (\n token_hash TEXT PRIMARY KEY,\n editor_id TEXT NOT NULL REFERENCES cms_editors(id) ON DELETE CASCADE,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n last_seen_at TEXT\n) WITHOUT ROWID;\nCREATE INDEX idx_cms_sessions_editor ON cms_sessions (editor_id);\n";
@@ -0,0 +1,44 @@
1
+ // Editors, project grants, and sessions for the built-in `localEditors()` auth
2
+ // adapter.
3
+ //
4
+ // Purely additive: three new tables, nothing existing is altered or rebuilt, so
5
+ // this applies to a live database in place. `project_id` stays an unconstrained
6
+ // TEXT key, exactly as it is in every other table here — there is no project
7
+ // registry to keep in step. A project is reachable for writes precisely when
8
+ // some editor has been granted it, which is what closes the old
9
+ // "any [project_id] string creates a tenant on first write" hole.
10
+ export const sql = `
11
+ CREATE TABLE cms_editors (
12
+ id TEXT PRIMARY KEY,
13
+ email TEXT NOT NULL,
14
+ -- Lookups are case-insensitive, but the address is shown back as entered.
15
+ email_lower TEXT NOT NULL UNIQUE,
16
+ name TEXT NOT NULL,
17
+ password_hash TEXT NOT NULL,
18
+ created_at TEXT NOT NULL,
19
+ disabled_at TEXT
20
+ );
21
+
22
+ -- Authorization. One row per (project, editor) the editor may edit; the absence
23
+ -- of a row is a 403. No FK on project_id for the reason above.
24
+ CREATE TABLE cms_project_editors (
25
+ project_id TEXT NOT NULL,
26
+ editor_id TEXT NOT NULL REFERENCES cms_editors(id) ON DELETE CASCADE,
27
+ role TEXT NOT NULL DEFAULT 'editor',
28
+ granted_at TEXT NOT NULL,
29
+ PRIMARY KEY (project_id, editor_id)
30
+ ) WITHOUT ROWID;
31
+ CREATE INDEX idx_cpe_editor ON cms_project_editors (editor_id);
32
+
33
+ -- Sessions are stored by SHA-256 of the token, never the token itself, so a
34
+ -- database read (a backup, a leaked file) yields nothing that can be replayed
35
+ -- as a cookie. The cookie carries the raw token.
36
+ CREATE TABLE cms_sessions (
37
+ token_hash TEXT PRIMARY KEY,
38
+ editor_id TEXT NOT NULL REFERENCES cms_editors(id) ON DELETE CASCADE,
39
+ created_at TEXT NOT NULL,
40
+ expires_at TEXT NOT NULL,
41
+ last_seen_at TEXT
42
+ ) WITHOUT ROWID;
43
+ CREATE INDEX idx_cms_sessions_editor ON cms_sessions (editor_id);
44
+ `;
@@ -0,0 +1,15 @@
1
+ export type Migration = {
2
+ name: string;
3
+ sql: string;
4
+ };
5
+ /**
6
+ * Every migration, in apply order.
7
+ *
8
+ * ⚠ The `name` values are the primary key of the `_migrations` table on every
9
+ * database already in production. `0001_init.sql` and `0002_site.sql` are the
10
+ * exact strings recorded there — including the `.sql` suffix these are no
11
+ * longer stored in. Rename one and the runner stops recognising it as applied,
12
+ * re-runs `CREATE TABLE published_pages` against live data, and the app fails
13
+ * to boot. `migrations.test.ts` pins these strings for that reason.
14
+ */
15
+ export declare const MIGRATIONS: Migration[];
@@ -0,0 +1,18 @@
1
+ import { sql as init } from './0001-init.js';
2
+ import { sql as site } from './0002-site.js';
3
+ import { sql as editors } from './0003-editors.js';
4
+ /**
5
+ * Every migration, in apply order.
6
+ *
7
+ * ⚠ The `name` values are the primary key of the `_migrations` table on every
8
+ * database already in production. `0001_init.sql` and `0002_site.sql` are the
9
+ * exact strings recorded there — including the `.sql` suffix these are no
10
+ * longer stored in. Rename one and the runner stops recognising it as applied,
11
+ * re-runs `CREATE TABLE published_pages` against live data, and the app fails
12
+ * to boot. `migrations.test.ts` pins these strings for that reason.
13
+ */
14
+ export const MIGRATIONS = [
15
+ { name: '0001_init.sql', sql: init },
16
+ { name: '0002_site.sql', sql: site },
17
+ { name: '0003_editors.sql', sql: editors }
18
+ ];
@@ -0,0 +1,67 @@
1
+ import { type PageEntry, type PageDeleteOutcome } from '../../core/page-entry.js';
2
+ import { type Tree } from '../../core/path.js';
3
+ import type { AddReleaseItemInput, CreatePageResult, DiscardItemTarget, ListMediaOptions, ListMediaResult, MediaItem, OpenRelease, PageMapRoute, PublishedRelease, ReleaseItem, RenamePageResult, StagePageDeleteResult } from '../../core/wire.js';
4
+ import type { SqliteDb } from './sqlite.js';
5
+ /** The locale a request falls back to when it names none. */
6
+ export declare const DEFAULT_LOCALE = "en";
7
+ /** An opaque, unguessable key for a release's preview URL. */
8
+ export declare const generatePreviewKey: () => string;
9
+ /**
10
+ * Every SQL statement and every piece of CMS domain logic, bound to one
11
+ * database connection.
12
+ *
13
+ * This was a module of top-level functions reading a process-wide `getDb()`
14
+ * singleton. Closing over `db` instead is what lets a test file hold its own
15
+ * `':memory:'` database (so the suites run in parallel rather than serialised
16
+ * behind a shared file) and what stops a host from being limited to one CMS per
17
+ * process. The statement helpers below are memoised per instance as a side
18
+ * effect — they previously re-`prepare`d on every single call.
19
+ *
20
+ * The body is otherwise a verbatim move.
21
+ */
22
+ export declare const createStore: (db: SqliteDb) => {
23
+ __resetProjectForTests: (projectId: string) => void;
24
+ addReleaseItems: (projectId: string, userId: string, inputs: AddReleaseItemInput[]) => OpenRelease;
25
+ allPageRouteIds: (projectId: string) => string[];
26
+ buildStateAtRelease: (projectId: string, releaseId: string) => {
27
+ layoutSnap: Record<string, Record<string, Tree>>;
28
+ pageSnap: Record<string, Record<string, PageEntry[]>>;
29
+ siteSnap: Tree;
30
+ } | null;
31
+ createMediaItem: (projectId: string, input: Omit<MediaItem, "id" | "uploadedAt">) => MediaItem;
32
+ createPage: (projectId: string, userId: string, routeId: string, locale: string, params: Record<string, string>, metadata?: Record<string, unknown>) => CreatePageResult;
33
+ deleteMediaItem: (projectId: string, id: string) => MediaItem | null;
34
+ discardOpenRelease: (projectId: string, userId: string) => boolean;
35
+ discardReleaseItem: (projectId: string, userId: string, target: DiscardItemTarget) => boolean;
36
+ findMediaItem: (projectId: string, id: string) => MediaItem | null;
37
+ findOpenReleaseByPreviewKey: (projectId: string, key: string) => OpenRelease | null;
38
+ findPublishedReleaseByPreviewKey: (projectId: string, key: string) => PublishedRelease | null;
39
+ getCmsVersion: (projectId: string) => number;
40
+ getOpenRelease: (projectId: string, userId: string) => OpenRelease | null;
41
+ getPublishedLayoutTree: (projectId: string, locale: string, routeId: string) => Tree | null;
42
+ getPublishedPageEntries: (projectId: string, locale: string, routeId: string) => PageEntry[];
43
+ getPublishedPageEntry: (projectId: string, locale: string, routeId: string, params: Record<string, string>) => PageEntry | null;
44
+ getPublishedSiteTree: (projectId: string) => Tree;
45
+ getReleaseHistory: (projectId: string) => PublishedRelease[];
46
+ listAllPages: (projectId: string, userId: string, locale: string) => PageMapRoute[];
47
+ listMediaItems: (projectId: string, { offset, limit }?: ListMediaOptions) => ListMediaResult;
48
+ listPageParamValues: (projectId: string, routeId: string, param: string) => string[];
49
+ lookupReleaseByPreviewKey: (projectId: string, key: string) => {
50
+ id: string;
51
+ items: ReleaseItem[];
52
+ } | null;
53
+ publishRelease: (projectId: string, userId: string, name?: string) => PublishedRelease | null;
54
+ regeneratePreviewKey: (projectId: string, userId: string) => string | null;
55
+ regeneratePublishedPreviewKey: (projectId: string, releaseId: string) => string | null;
56
+ renamePage: (projectId: string, userId: string, args: {
57
+ routeId: string;
58
+ locale: string;
59
+ fromParams: Record<string, string>;
60
+ toParams: Record<string, string>;
61
+ toUrl: string;
62
+ }) => RenamePageResult;
63
+ revertRelease: (projectId: string, releaseId: string, userId: string) => PublishedRelease | null;
64
+ seedPublishedDocs: (projectId: string, layouts: Record<string, Record<string, Tree>>, pages: Record<string, Record<string, PageEntry[]>>) => void;
65
+ stagePageDelete: (projectId: string, userId: string, routeId: string, locale: string, params: Record<string, string>, outcome?: PageDeleteOutcome) => StagePageDeleteResult;
66
+ };
67
+ export type CmsStore = ReturnType<typeof createStore>;