@velastack/cms 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,6 +49,6 @@ export type { CmsStore } from './store/queries.js';
49
49
  export type { SqliteDb, SqliteStatement } from './store/sqlite.js';
50
50
  export type { Migration } from './store/migrations/index.js';
51
51
  export { MIGRATIONS } from './store/migrations/index.js';
52
- export type { DocsResponse, MediaItem, OpenRelease, PageMapEntry, PageMapRoute, PagesResponse, PagesRoute, PublishedRelease, ReleaseItem, SiteResponse } from '../core/wire.js';
52
+ export type { DocsResponse, MediaItem, OpenRelease, PageMapEntry, PageMapRoute, PagesResponse, PagesRoute, PublishedRelease, ReleaseItem, ReleasePublisher, SiteResponse } from '../core/wire.js';
53
53
  export type { PageDeleteOutcome, PageEntry } from '../core/page-entry.js';
54
54
  export type { Tree } from '../core/path.js';
@@ -101,7 +101,7 @@ export const postReleasePublish = async (ctx) => {
101
101
  name = body.name;
102
102
  }
103
103
  }
104
- const published = ctx.store.publishRelease(ctx.projectId, user.id, name);
104
+ const published = ctx.store.publishRelease(ctx.projectId, user, name);
105
105
  if (!published)
106
106
  return new Response('no open release to publish', { status: 404 });
107
107
  return Response.json({ release: published });
@@ -130,7 +130,7 @@ export const postReleaseRevert = (ctx) => {
130
130
  const id = ctx.params.id;
131
131
  if (!id)
132
132
  return badRequest('id required');
133
- const reverted = ctx.store.revertRelease(ctx.projectId, id, user.id);
133
+ const reverted = ctx.store.revertRelease(ctx.projectId, id, user);
134
134
  if (!reverted)
135
135
  return new Response('release not found', { status: 404 });
136
136
  return Response.json({ release: reverted });
@@ -0,0 +1 @@
1
+ export declare const sql = "\nALTER TABLE releases ADD COLUMN published_by_email TEXT;\nALTER TABLE releases ADD COLUMN published_by_name TEXT;\nUPDATE releases SET\n published_by_email = (SELECT e.email FROM cms_editors e WHERE e.id = releases.published_by),\n published_by_name = (SELECT e.name FROM cms_editors e WHERE e.id = releases.published_by);\n";
@@ -0,0 +1,15 @@
1
+ // Who published a release, as they were at the time.
2
+ //
3
+ // `published_by` is an opaque editor id, which is all the auth adapter
4
+ // promises to keep stable — and all the admin bar could show. The publisher's
5
+ // email and name are now captured on the row at publish time, so history reads
6
+ // the same under any adapter and survives the account being renamed or
7
+ // removed. Existing rows are filled from `cms_editors` where the id still
8
+ // resolves; the rest stay null and readers fall back to the id.
9
+ export const sql = `
10
+ ALTER TABLE releases ADD COLUMN published_by_email TEXT;
11
+ ALTER TABLE releases ADD COLUMN published_by_name TEXT;
12
+ UPDATE releases SET
13
+ published_by_email = (SELECT e.email FROM cms_editors e WHERE e.id = releases.published_by),
14
+ published_by_name = (SELECT e.name FROM cms_editors e WHERE e.id = releases.published_by);
15
+ `;
@@ -1,6 +1,7 @@
1
1
  import { sql as init } from './0001-init.js';
2
2
  import { sql as site } from './0002-site.js';
3
3
  import { sql as editors } from './0003-editors.js';
4
+ import { sql as releasePublisher } from './0004-release-publisher.js';
4
5
  /**
5
6
  * Every migration, in apply order.
6
7
  *
@@ -14,5 +15,6 @@ import { sql as editors } from './0003-editors.js';
14
15
  export const MIGRATIONS = [
15
16
  { name: '0001_init.sql', sql: init },
16
17
  { name: '0002_site.sql', sql: site },
17
- { name: '0003_editors.sql', sql: editors }
18
+ { name: '0003_editors.sql', sql: editors },
19
+ { name: '0004_release_publisher.sql', sql: releasePublisher }
18
20
  ];
@@ -2,6 +2,7 @@ import { type PageEntry, type PageDeleteOutcome } from '../../core/page-entry.js
2
2
  import { type Tree } from '../../core/path.js';
3
3
  import type { AddReleaseItemInput, CreatePageResult, DiscardItemTarget, ListMediaOptions, ListMediaResult, MediaItem, OpenRelease, PageMapRoute, PublishedRelease, ReleaseItem, RenamePageResult, StagePageDeleteResult } from '../../core/wire.js';
4
4
  import type { SqliteDb } from './sqlite.js';
5
+ import type { CmsEditor } from '../types.js';
5
6
  /** The locale a request falls back to when it names none. */
6
7
  export declare const DEFAULT_LOCALE = "en";
7
8
  /** An opaque, unguessable key for a release's preview URL. */
@@ -50,7 +51,7 @@ export declare const createStore: (db: SqliteDb) => {
50
51
  id: string;
51
52
  items: ReleaseItem[];
52
53
  } | null;
53
- publishRelease: (projectId: string, userId: string, name?: string) => PublishedRelease | null;
54
+ publishRelease: (projectId: string, user: CmsEditor, name?: string) => PublishedRelease | null;
54
55
  regeneratePreviewKey: (projectId: string, userId: string) => string | null;
55
56
  regeneratePublishedPreviewKey: (projectId: string, releaseId: string) => string | null;
56
57
  renamePage: (projectId: string, userId: string, args: {
@@ -60,7 +61,7 @@ export declare const createStore: (db: SqliteDb) => {
60
61
  toParams: Record<string, string>;
61
62
  toUrl: string;
62
63
  }) => RenamePageResult;
63
- revertRelease: (projectId: string, releaseId: string, userId: string) => PublishedRelease | null;
64
+ revertRelease: (projectId: string, releaseId: string, user: CmsEditor) => PublishedRelease | null;
64
65
  seedPublishedDocs: (projectId: string, layouts: Record<string, Record<string, Tree>>, pages: Record<string, Record<string, PageEntry[]>>) => void;
65
66
  stagePageDelete: (projectId: string, userId: string, routeId: string, locale: string, params: Record<string, string>, outcome?: PageDeleteOutcome) => StagePageDeleteResult;
66
67
  };
@@ -517,7 +517,7 @@ export const createStore = (db) => {
517
517
  updated_by_release = excluded.updated_by_release`);
518
518
  const updatePublishedPageTombstoneStmt = () => db.prepare('UPDATE published_pages SET tombstone = ?, updated_at = ?, updated_by_release = ? WHERE project_id = ? AND locale = ? AND route_id = ? AND params_hash = ?');
519
519
  const deletePublishedPageStmt = () => db.prepare('DELETE FROM published_pages WHERE project_id = ? AND locale = ? AND route_id = ? AND params_hash = ?');
520
- const insertReleaseStmt = () => db.prepare('INSERT INTO releases (id, project_id, seq, name, published_by, published_at, preview_key) VALUES (?, ?, ?, ?, ?, ?, ?)');
520
+ const insertReleaseStmt = () => db.prepare('INSERT INTO releases (id, project_id, seq, name, published_by, published_by_email, published_by_name, published_at, preview_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
521
521
  const insertReleaseItemStmt = () => db.prepare('INSERT INTO release_items (release_id, project_id, seq, kind, route_id, locale, params_hash, params, tree, prior_tree, outcome, prior_tombstone, added_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
522
522
  const deleteOpenItemsForUserStmt = () => db.prepare('DELETE FROM open_release_items WHERE project_id = ? AND user_id = ?');
523
523
  /**
@@ -527,7 +527,8 @@ export const createStore = (db) => {
527
527
  * open release. Returns the new `PublishedRelease`, or null if there's nothing
528
528
  * to publish.
529
529
  */
530
- const publishRelease = (projectId, userId, name) => {
530
+ const publishRelease = (projectId, user, name) => {
531
+ const userId = user.id;
531
532
  const tx = db.transaction(() => {
532
533
  ensureProjectState(projectId);
533
534
  const releaseRow = getOpenReleaseStmt().get(projectId, userId);
@@ -546,7 +547,7 @@ export const createStore = (db) => {
546
547
  // resolves on every per-item insert below. We delete the header at the
547
548
  // end if the body turned out to be empty.
548
549
  const finalName = name ?? releaseRow.name ?? null;
549
- insertReleaseStmt().run(releaseId, projectId, seq, finalName, userId, publishedAt, previewKey);
550
+ insertReleaseStmt().run(releaseId, projectId, seq, finalName, user.id, user.email, user.name, publishedAt, previewKey);
550
551
  for (const item of items) {
551
552
  if (item.kind === 'site') {
552
553
  const target = getPublishedSiteTree(projectId);
@@ -635,7 +636,7 @@ export const createStore = (db) => {
635
636
  deleteOpenReleaseStmt().run(projectId, userId);
636
637
  const result = {
637
638
  id: releaseId,
638
- publishedBy: userId,
639
+ publishedBy: { id: user.id, email: user.email, name: user.name },
639
640
  publishedAt,
640
641
  preview_key: previewKey,
641
642
  items: publishedItems
@@ -646,9 +647,9 @@ export const createStore = (db) => {
646
647
  });
647
648
  return tx();
648
649
  };
649
- const listReleaseHistoryStmt = () => db.prepare('SELECT id, name, published_by, published_at, preview_key, reverted_at FROM releases WHERE project_id = ? ORDER BY seq ASC');
650
- const findReleaseByPreviewKeyStmt = () => db.prepare('SELECT id, name, published_by, published_at, preview_key, reverted_at, seq FROM releases WHERE project_id = ? AND preview_key = ?');
651
- const getReleaseByIdStmt = () => db.prepare('SELECT id, name, published_by, published_at, preview_key, reverted_at, seq FROM releases WHERE project_id = ? AND id = ?');
650
+ const listReleaseHistoryStmt = () => db.prepare('SELECT id, name, published_by, published_by_email, published_by_name, published_at, preview_key, reverted_at FROM releases WHERE project_id = ? ORDER BY seq ASC');
651
+ const findReleaseByPreviewKeyStmt = () => db.prepare('SELECT id, name, published_by, published_by_email, published_by_name, published_at, preview_key, reverted_at, seq FROM releases WHERE project_id = ? AND preview_key = ?');
652
+ const getReleaseByIdStmt = () => db.prepare('SELECT id, name, published_by, published_by_email, published_by_name, published_at, preview_key, reverted_at, seq FROM releases WHERE project_id = ? AND id = ?');
652
653
  const listReleaseItemsByReleaseIdStmt = () => db.prepare('SELECT kind, route_id, locale, params, tree, prior_tree, outcome, prior_tombstone FROM release_items WHERE release_id = ? ORDER BY id');
653
654
  const listReleaseItemsAfterSeqStmt = () => db.prepare('SELECT seq, kind, route_id, locale, params, tree, prior_tree, outcome, prior_tombstone FROM release_items WHERE project_id = ? AND seq > ? ORDER BY seq DESC, id DESC');
654
655
  const updateReleasePreviewKeyStmt = () => db.prepare('UPDATE releases SET preview_key = ? WHERE id = ?');
@@ -702,7 +703,11 @@ export const createStore = (db) => {
702
703
  const items = listReleaseItemsByReleaseIdStmt().all(row.id).map(rowToPublishedItem);
703
704
  const r = {
704
705
  id: row.id,
705
- publishedBy: row.published_by,
706
+ publishedBy: {
707
+ id: row.published_by,
708
+ email: row.published_by_email,
709
+ name: row.published_by_name
710
+ },
706
711
  publishedAt: row.published_at,
707
712
  preview_key: row.preview_key,
708
713
  items
@@ -822,7 +827,7 @@ export const createStore = (db) => {
822
827
  // ---------------------------------------------------------------------------
823
828
  // Revert
824
829
  // ---------------------------------------------------------------------------
825
- const revertRelease = (projectId, releaseId, userId) => {
830
+ const revertRelease = (projectId, releaseId, user) => {
826
831
  const tx = db.transaction(() => {
827
832
  ensureProjectState(projectId);
828
833
  const sourceRow = getReleaseByIdStmt().get(projectId, releaseId);
@@ -839,7 +844,7 @@ export const createStore = (db) => {
839
844
  const inverseName = sourceRow.name
840
845
  ? `Revert "${sourceRow.name}"`
841
846
  : `Revert ${sourceRow.id.slice(0, 8)}`;
842
- insertReleaseStmt().run(inverseId, projectId, seq, inverseName, userId, publishedAt, previewKey);
847
+ insertReleaseStmt().run(inverseId, projectId, seq, inverseName, user.id, user.email, user.name, publishedAt, previewKey);
843
848
  for (const item of sourceItems) {
844
849
  if (item.kind === 'site') {
845
850
  const restoreTree = item.priorTree ?? {};
@@ -962,7 +967,7 @@ export const createStore = (db) => {
962
967
  return {
963
968
  id: inverseId,
964
969
  name: inverseName,
965
- publishedBy: userId,
970
+ publishedBy: { id: user.id, email: user.email, name: user.name },
966
971
  publishedAt,
967
972
  preview_key: previewKey,
968
973
  items: inverseItems
@@ -26,7 +26,7 @@
26
26
  type PublishedRelease = {
27
27
  id: string;
28
28
  name?: string;
29
- publishedBy: string;
29
+ publishedBy: { id: string; email: string | null; name: string | null };
30
30
  publishedAt: string;
31
31
  preview_key: string;
32
32
  revertedAt?: string;
@@ -206,10 +206,9 @@
206
206
  {/if}
207
207
  </div>
208
208
  <div class="vela:text-[11px] vela:text-bar-text-tertiary vela:mt-0.5">
209
- {formatDate(release.publishedAt)} · published by {release.publishedBy}{isLive
210
- ? ' · '
211
- : ''}{#if isLive}<span class="vela:text-[var(--cms-status-clean-dot)]"
212
- >live now</span
209
+ {formatDate(release.publishedAt)} · published by {release.publishedBy.email ??
210
+ release.publishedBy.id}{isLive ? ' · ' : ''}{#if isLive}<span
211
+ class="vela:text-[var(--cms-status-clean-dot)]">live now</span
213
212
  >{/if}
214
213
  </div>
215
214
  {#if paths.length > 0}
@@ -54,10 +54,20 @@ export type PublishedReleaseItem = ReleaseItemCore & {
54
54
  priorTree: Tree | null;
55
55
  priorTombstone?: PageDeleteOutcome;
56
56
  };
57
+ /**
58
+ * The editor who published a release, captured at publish time. `email` and
59
+ * `name` are null only on rows published before they were recorded, where the
60
+ * account no longer resolves.
61
+ */
62
+ export type ReleasePublisher = {
63
+ id: string;
64
+ email: string | null;
65
+ name: string | null;
66
+ };
57
67
  export type PublishedRelease = {
58
68
  id: string;
59
69
  name?: string;
60
- publishedBy: string;
70
+ publishedBy: ReleasePublisher;
61
71
  publishedAt: string;
62
72
  preview_key: string;
63
73
  items: PublishedReleaseItem[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velastack/cms",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A scope-aware CMS for SvelteKit, for static or dynamic sites.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "type": "module",
24
24
  "engines": {
25
- "node": "^20.19.0 || >=22.12.0"
25
+ "node": ">=22.12.0"
26
26
  },
27
27
  "scripts": {
28
28
  "dev": "vite dev",
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@sveltejs/kit": "^2.0.0",
79
- "better-sqlite3": "^11.0.0 || ^12.0.0",
79
+ "better-sqlite3": "^12.0.0 || ^13.0.0",
80
80
  "marked": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
81
81
  "svelte": "^5.0.0",
82
82
  "vite": "^8.0.0"
@@ -98,7 +98,7 @@
98
98
  "@tailwindcss/cli": "^4.3.3",
99
99
  "@tailwindcss/vite": "^4.3.3",
100
100
  "@types/node": "^26.4.0",
101
- "better-sqlite3": "^12.11.1",
101
+ "better-sqlite3": "^13.0.3",
102
102
  "marked": "^18.0.11",
103
103
  "prettier": "^3.9.6",
104
104
  "prettier-plugin-svelte": "^4.1.1",