@jant/core 0.3.23 → 0.3.24
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/app.js +4 -5
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
- package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
- package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
- package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
- package/dist/themes/threads/timeline/LinkCard.js +68 -0
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +4 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +28 -12
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +199 -51
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +45 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/{minimal → threads}/index.ts +30 -13
- package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
- package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
- package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
- package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
- package/src/themes/threads/style.css +336 -0
- package/src/themes/threads/timeline/LinkCard.tsx +67 -0
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/src/routes/api/timeline.tsx +0 -159
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test Database Helper
|
|
3
3
|
*
|
|
4
|
-
* Creates an in-memory SQLite database with all migrations applied.
|
|
4
|
+
* Creates an in-memory SQLite database with all migrations applied (up to v2).
|
|
5
5
|
* Used for service integration tests.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -13,11 +13,22 @@ import { resolve } from "path";
|
|
|
13
13
|
|
|
14
14
|
const MIGRATIONS_DIR = resolve(import.meta.dirname, "../../db/migrations");
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Applies a migration file, splitting on Drizzle statement breakpoints.
|
|
18
|
+
*/
|
|
19
|
+
function applyMigration(sqlite: Database.Database, filename: string) {
|
|
20
|
+
const migration = readFileSync(resolve(MIGRATIONS_DIR, filename), "utf-8");
|
|
21
|
+
for (const sql of migration.split("--> statement-breakpoint")) {
|
|
22
|
+
const trimmed = sql.trim();
|
|
23
|
+
if (trimmed) sqlite.exec(trimmed);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
/**
|
|
17
28
|
* Creates a fresh in-memory SQLite database with all migrations applied.
|
|
18
29
|
* Each call returns an isolated database instance for test isolation.
|
|
19
30
|
*
|
|
20
|
-
* @param options.fts - Whether to
|
|
31
|
+
* @param options.fts - Whether to enable FTS5 for search tests (default: false).
|
|
21
32
|
* The trigram tokenizer used in production may not be available in all
|
|
22
33
|
* better-sqlite3 builds, so FTS is opt-in for tests that need it.
|
|
23
34
|
*/
|
|
@@ -28,87 +39,53 @@ export function createTestDatabase(options?: { fts?: boolean }) {
|
|
|
28
39
|
sqlite.pragma("journal_mode = WAL");
|
|
29
40
|
sqlite.pragma("foreign_keys = ON");
|
|
30
41
|
|
|
31
|
-
// Apply base
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
// Apply v1 base migrations (0000-0004)
|
|
43
|
+
applyMigration(sqlite, "0000_square_wallflower.sql");
|
|
44
|
+
// Skip 0001 (FTS) — v2 migration will create updated FTS if needed
|
|
45
|
+
applyMigration(sqlite, "0002_add_media_attachments.sql");
|
|
46
|
+
applyMigration(sqlite, "0003_add_navigation_links.sql");
|
|
47
|
+
applyMigration(sqlite, "0004_add_storage_provider.sql");
|
|
48
|
+
|
|
49
|
+
// Apply v2 schema migration (0005)
|
|
50
|
+
// Split FTS-related statements so we can handle them separately
|
|
51
|
+
const v2Migration = readFileSync(
|
|
52
|
+
resolve(MIGRATIONS_DIR, "0005_v2_schema_migration.sql"),
|
|
34
53
|
"utf-8",
|
|
35
54
|
);
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
for (const stmt of v2Migration.split("--> statement-breakpoint")) {
|
|
57
|
+
const trimmed = stmt.trim();
|
|
58
|
+
if (!trimmed) continue;
|
|
59
|
+
|
|
60
|
+
// Skip FTS-related statements if FTS not requested
|
|
61
|
+
const isFts = trimmed.includes("posts_fts");
|
|
62
|
+
if (!options?.fts && isFts) continue;
|
|
42
63
|
|
|
43
|
-
// Optionally apply FTS5 migration (with fallback tokenizer)
|
|
44
|
-
if (options?.fts) {
|
|
45
64
|
try {
|
|
46
|
-
|
|
47
|
-
resolve(MIGRATIONS_DIR, "0001_add_search_fts.sql"),
|
|
48
|
-
"utf-8",
|
|
49
|
-
);
|
|
50
|
-
sqlite.exec(migration1);
|
|
65
|
+
sqlite.exec(trimmed);
|
|
51
66
|
} catch {
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
SELECT NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.content, '')
|
|
72
|
-
WHERE NEW.deleted_at IS NULL;
|
|
73
|
-
END;
|
|
74
|
-
|
|
75
|
-
CREATE TRIGGER IF NOT EXISTS posts_fts_delete AFTER DELETE ON posts BEGIN
|
|
76
|
-
DELETE FROM posts_fts WHERE rowid = OLD.id;
|
|
77
|
-
END;
|
|
78
|
-
`);
|
|
67
|
+
// Handle trigram tokenizer failure for FTS virtual table
|
|
68
|
+
if (options?.fts && trimmed.includes("CREATE VIRTUAL TABLE")) {
|
|
69
|
+
sqlite.exec(`
|
|
70
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
|
71
|
+
title,
|
|
72
|
+
body,
|
|
73
|
+
quote_text,
|
|
74
|
+
content='posts',
|
|
75
|
+
content_rowid='id'
|
|
76
|
+
);
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
// Ignore DROP TRIGGER/TABLE IF EXISTS failures silently
|
|
80
|
+
else if (
|
|
81
|
+
!trimmed.startsWith("DROP TRIGGER") &&
|
|
82
|
+
!trimmed.startsWith("DROP TABLE")
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(`Migration statement failed: ${trimmed.slice(0, 100)}`);
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
// Apply media attachments migration (position + blurhash)
|
|
83
|
-
const migration2 = readFileSync(
|
|
84
|
-
resolve(MIGRATIONS_DIR, "0002_add_media_attachments.sql"),
|
|
85
|
-
"utf-8",
|
|
86
|
-
);
|
|
87
|
-
for (const sql of migration2.split("--> statement-breakpoint")) {
|
|
88
|
-
const trimmed = sql.trim();
|
|
89
|
-
if (trimmed) sqlite.exec(trimmed);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Apply navigation links migration
|
|
93
|
-
const migration3 = readFileSync(
|
|
94
|
-
resolve(MIGRATIONS_DIR, "0003_add_navigation_links.sql"),
|
|
95
|
-
"utf-8",
|
|
96
|
-
);
|
|
97
|
-
for (const sql of migration3.split("--> statement-breakpoint")) {
|
|
98
|
-
const trimmed = sql.trim();
|
|
99
|
-
if (trimmed) sqlite.exec(trimmed);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Apply storage provider migration
|
|
103
|
-
const migration4 = readFileSync(
|
|
104
|
-
resolve(MIGRATIONS_DIR, "0004_add_storage_provider.sql"),
|
|
105
|
-
"utf-8",
|
|
106
|
-
);
|
|
107
|
-
for (const sql of migration4.split("--> statement-breakpoint")) {
|
|
108
|
-
const trimmed = sql.trim();
|
|
109
|
-
if (trimmed) sqlite.exec(trimmed);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
89
|
const db = drizzle(sqlite, { schema });
|
|
113
90
|
|
|
114
91
|
return { db, sqlite };
|
package/src/app.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { i18nMiddleware } from "./i18n/index.js";
|
|
|
11
11
|
import { useLingui } from "@lingui/react/macro";
|
|
12
12
|
import type { Bindings, JantConfig } from "./types.js";
|
|
13
13
|
import { SETTINGS_KEYS } from "./lib/constants.js";
|
|
14
|
-
import { theme as
|
|
14
|
+
import { theme as threadsTheme } from "./themes/threads/index.js";
|
|
15
15
|
import { hashPassword } from "better-auth/crypto";
|
|
16
16
|
|
|
17
17
|
// Routes - Pages
|
|
@@ -36,8 +36,6 @@ import { navigationRoutes as dashNavigationRoutes } from "./routes/dash/navigati
|
|
|
36
36
|
import { postsApiRoutes } from "./routes/api/posts.js";
|
|
37
37
|
import { uploadApiRoutes } from "./routes/api/upload.js";
|
|
38
38
|
import { searchApiRoutes } from "./routes/api/search.js";
|
|
39
|
-
import { timelineApiRoutes } from "./routes/api/timeline.js";
|
|
40
|
-
|
|
41
39
|
// Routes - Feed
|
|
42
40
|
import { rssRoutes } from "./routes/feed/rss.js";
|
|
43
41
|
import { sitemapRoutes } from "./routes/feed/sitemap.js";
|
|
@@ -83,8 +81,8 @@ export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
|
|
|
83
81
|
* ```
|
|
84
82
|
*/
|
|
85
83
|
export function createApp(config: JantConfig = {}): App {
|
|
86
|
-
// Merge with default
|
|
87
|
-
const defaultTheme =
|
|
84
|
+
// Merge with default threads theme
|
|
85
|
+
const defaultTheme = threadsTheme();
|
|
88
86
|
const resolvedConfig: JantConfig = {
|
|
89
87
|
...config,
|
|
90
88
|
theme: {
|
|
@@ -93,6 +91,7 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
93
91
|
...defaultTheme.components,
|
|
94
92
|
...config.theme?.components,
|
|
95
93
|
},
|
|
94
|
+
timelineMore: config.theme?.timelineMore ?? defaultTheme.timelineMore,
|
|
96
95
|
cssVariables: {
|
|
97
96
|
...defaultTheme.cssVariables,
|
|
98
97
|
...config.theme?.cssVariables,
|
|
@@ -197,7 +196,6 @@ export function createApp(config: JantConfig = {}): App {
|
|
|
197
196
|
|
|
198
197
|
// API Routes
|
|
199
198
|
app.route("/api/posts", postsApiRoutes);
|
|
200
|
-
app.route("/api/timeline", timelineApiRoutes);
|
|
201
199
|
|
|
202
200
|
// Setup page component
|
|
203
201
|
const SetupContent: FC = () => {
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
-- v2 Schema Migration
|
|
2
|
+
-- Restructures posts, creates pages, updates collections, replaces navigation_links with nav_items
|
|
3
|
+
|
|
4
|
+
-- Disable FK checks for migration (dropping/recreating tables with cross-references)
|
|
5
|
+
PRAGMA foreign_keys = OFF;
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
|
|
8
|
+
-- =============================================================================
|
|
9
|
+
-- 1. Create pages table (before modifying posts, migrate type='page' data)
|
|
10
|
+
-- =============================================================================
|
|
11
|
+
|
|
12
|
+
CREATE TABLE `pages` (
|
|
13
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
14
|
+
`slug` text NOT NULL,
|
|
15
|
+
`title` text,
|
|
16
|
+
`body` text,
|
|
17
|
+
`body_html` text,
|
|
18
|
+
`status` text DEFAULT 'published' NOT NULL,
|
|
19
|
+
`created_at` integer NOT NULL,
|
|
20
|
+
`updated_at` integer NOT NULL
|
|
21
|
+
);
|
|
22
|
+
--> statement-breakpoint
|
|
23
|
+
CREATE UNIQUE INDEX `pages_slug_unique` ON `pages` (`slug`);
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
|
|
26
|
+
-- Migrate type='page' posts into pages table
|
|
27
|
+
INSERT INTO `pages` (`slug`, `title`, `body`, `body_html`, `status`, `created_at`, `updated_at`)
|
|
28
|
+
SELECT
|
|
29
|
+
CASE
|
|
30
|
+
WHEN `path` IS NOT NULL AND `path` != '' THEN REPLACE(`path`, '/', '')
|
|
31
|
+
ELSE 'page-' || `id`
|
|
32
|
+
END,
|
|
33
|
+
`title`,
|
|
34
|
+
`content`,
|
|
35
|
+
`content_html`,
|
|
36
|
+
CASE WHEN `visibility` = 'draft' THEN 'draft' ELSE 'published' END,
|
|
37
|
+
`created_at`,
|
|
38
|
+
`updated_at`
|
|
39
|
+
FROM `posts`
|
|
40
|
+
WHERE `type` = 'page';
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
|
|
43
|
+
-- =============================================================================
|
|
44
|
+
-- 2. Restructure posts table (create new → migrate → drop old → rename)
|
|
45
|
+
-- =============================================================================
|
|
46
|
+
|
|
47
|
+
CREATE TABLE `posts_new` (
|
|
48
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
49
|
+
`format` text DEFAULT 'note' NOT NULL,
|
|
50
|
+
`status` text DEFAULT 'published' NOT NULL,
|
|
51
|
+
`featured` integer DEFAULT 0 NOT NULL,
|
|
52
|
+
`pinned` integer DEFAULT 0 NOT NULL,
|
|
53
|
+
`slug` text,
|
|
54
|
+
`title` text,
|
|
55
|
+
`url` text,
|
|
56
|
+
`body` text,
|
|
57
|
+
`body_html` text,
|
|
58
|
+
`quote_text` text,
|
|
59
|
+
`rating` integer,
|
|
60
|
+
`collection_id` integer,
|
|
61
|
+
`reply_to_id` integer,
|
|
62
|
+
`thread_id` integer,
|
|
63
|
+
`deleted_at` integer,
|
|
64
|
+
`published_at` integer NOT NULL,
|
|
65
|
+
`created_at` integer NOT NULL,
|
|
66
|
+
`updated_at` integer NOT NULL,
|
|
67
|
+
FOREIGN KEY (`collection_id`) REFERENCES `collections`(`id`) ON UPDATE no action ON DELETE set null
|
|
68
|
+
);
|
|
69
|
+
--> statement-breakpoint
|
|
70
|
+
|
|
71
|
+
-- Migrate data from old posts to new posts (excluding type='page')
|
|
72
|
+
INSERT INTO `posts_new` (
|
|
73
|
+
`id`, `format`, `status`, `featured`, `pinned`,
|
|
74
|
+
`slug`, `title`, `url`, `body`, `body_html`, `quote_text`, `rating`,
|
|
75
|
+
`collection_id`, `reply_to_id`, `thread_id`,
|
|
76
|
+
`deleted_at`, `published_at`, `created_at`, `updated_at`
|
|
77
|
+
)
|
|
78
|
+
SELECT
|
|
79
|
+
`id`,
|
|
80
|
+
-- format mapping: article→note, image→note, note→note, link→link, quote→quote
|
|
81
|
+
CASE
|
|
82
|
+
WHEN `type` IN ('article', 'image', 'note') THEN 'note'
|
|
83
|
+
WHEN `type` = 'link' THEN 'link'
|
|
84
|
+
WHEN `type` = 'quote' THEN 'quote'
|
|
85
|
+
ELSE 'note'
|
|
86
|
+
END,
|
|
87
|
+
-- status mapping from visibility
|
|
88
|
+
CASE WHEN `visibility` = 'draft' THEN 'draft' ELSE 'published' END,
|
|
89
|
+
-- featured mapping from visibility
|
|
90
|
+
CASE WHEN `visibility` = 'featured' THEN 1 ELSE 0 END,
|
|
91
|
+
-- pinned: default 0
|
|
92
|
+
0,
|
|
93
|
+
-- slug: migrate from path (strip leading /)
|
|
94
|
+
CASE
|
|
95
|
+
WHEN `path` IS NOT NULL AND `path` != '' THEN REPLACE(`path`, '/', '')
|
|
96
|
+
ELSE NULL
|
|
97
|
+
END,
|
|
98
|
+
`title`,
|
|
99
|
+
`source_url`,
|
|
100
|
+
`content`,
|
|
101
|
+
`content_html`,
|
|
102
|
+
-- quote_text: for quote type, content was the quote; set to null (manual fix may be needed)
|
|
103
|
+
NULL,
|
|
104
|
+
-- rating: null
|
|
105
|
+
NULL,
|
|
106
|
+
-- collection_id: migrate from post_collections (take first collection for each post)
|
|
107
|
+
(SELECT `collection_id` FROM `post_collections` WHERE `post_collections`.`post_id` = `posts`.`id` LIMIT 1),
|
|
108
|
+
`reply_to_id`,
|
|
109
|
+
`thread_id`,
|
|
110
|
+
`deleted_at`,
|
|
111
|
+
`published_at`,
|
|
112
|
+
`created_at`,
|
|
113
|
+
`updated_at`
|
|
114
|
+
FROM `posts`
|
|
115
|
+
WHERE `type` != 'page';
|
|
116
|
+
--> statement-breakpoint
|
|
117
|
+
|
|
118
|
+
-- Update media references to point at new table (foreign keys reference posts)
|
|
119
|
+
-- media.post_id still works since IDs are preserved
|
|
120
|
+
--> statement-breakpoint
|
|
121
|
+
|
|
122
|
+
-- Drop old posts table and rename new one
|
|
123
|
+
DROP TABLE `posts`;
|
|
124
|
+
--> statement-breakpoint
|
|
125
|
+
ALTER TABLE `posts_new` RENAME TO `posts`;
|
|
126
|
+
--> statement-breakpoint
|
|
127
|
+
CREATE UNIQUE INDEX `posts_slug_unique` ON `posts` (`slug`);
|
|
128
|
+
--> statement-breakpoint
|
|
129
|
+
|
|
130
|
+
-- =============================================================================
|
|
131
|
+
-- 3. Update collections table (add new columns, rename path→slug)
|
|
132
|
+
-- =============================================================================
|
|
133
|
+
|
|
134
|
+
CREATE TABLE `collections_new` (
|
|
135
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
136
|
+
`slug` text NOT NULL,
|
|
137
|
+
`title` text NOT NULL,
|
|
138
|
+
`description` text,
|
|
139
|
+
`icon` text,
|
|
140
|
+
`sort_order` text DEFAULT 'newest' NOT NULL,
|
|
141
|
+
`position` integer DEFAULT 0 NOT NULL,
|
|
142
|
+
`show_divider` integer DEFAULT 0 NOT NULL,
|
|
143
|
+
`created_at` integer NOT NULL,
|
|
144
|
+
`updated_at` integer NOT NULL
|
|
145
|
+
);
|
|
146
|
+
--> statement-breakpoint
|
|
147
|
+
CREATE UNIQUE INDEX `collections_new_slug_unique` ON `collections_new` (`slug`);
|
|
148
|
+
--> statement-breakpoint
|
|
149
|
+
|
|
150
|
+
INSERT INTO `collections_new` (`id`, `slug`, `title`, `description`, `icon`, `sort_order`, `position`, `show_divider`, `created_at`, `updated_at`)
|
|
151
|
+
SELECT
|
|
152
|
+
`id`,
|
|
153
|
+
COALESCE(`path`, 'collection-' || `id`),
|
|
154
|
+
`title`,
|
|
155
|
+
`description`,
|
|
156
|
+
NULL,
|
|
157
|
+
'newest',
|
|
158
|
+
0,
|
|
159
|
+
0,
|
|
160
|
+
`created_at`,
|
|
161
|
+
`updated_at`
|
|
162
|
+
FROM `collections`;
|
|
163
|
+
--> statement-breakpoint
|
|
164
|
+
|
|
165
|
+
DROP TABLE `collections`;
|
|
166
|
+
--> statement-breakpoint
|
|
167
|
+
ALTER TABLE `collections_new` RENAME TO `collections`;
|
|
168
|
+
--> statement-breakpoint
|
|
169
|
+
CREATE UNIQUE INDEX `collections_slug_unique` ON `collections` (`slug`);
|
|
170
|
+
--> statement-breakpoint
|
|
171
|
+
|
|
172
|
+
-- =============================================================================
|
|
173
|
+
-- 4. Replace navigation_links with nav_items
|
|
174
|
+
-- =============================================================================
|
|
175
|
+
|
|
176
|
+
CREATE TABLE `nav_items` (
|
|
177
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
178
|
+
`type` text DEFAULT 'link' NOT NULL,
|
|
179
|
+
`label` text NOT NULL,
|
|
180
|
+
`url` text NOT NULL,
|
|
181
|
+
`page_id` integer,
|
|
182
|
+
`position` integer DEFAULT 0 NOT NULL,
|
|
183
|
+
`created_at` integer NOT NULL,
|
|
184
|
+
`updated_at` integer NOT NULL,
|
|
185
|
+
FOREIGN KEY (`page_id`) REFERENCES `pages`(`id`) ON UPDATE no action ON DELETE cascade
|
|
186
|
+
);
|
|
187
|
+
--> statement-breakpoint
|
|
188
|
+
|
|
189
|
+
-- Migrate existing navigation_links as type='link'
|
|
190
|
+
INSERT INTO `nav_items` (`type`, `label`, `url`, `page_id`, `position`, `created_at`, `updated_at`)
|
|
191
|
+
SELECT 'link', `label`, `url`, NULL, `position`, `created_at`, `updated_at`
|
|
192
|
+
FROM `navigation_links`;
|
|
193
|
+
--> statement-breakpoint
|
|
194
|
+
|
|
195
|
+
DROP TABLE `navigation_links`;
|
|
196
|
+
--> statement-breakpoint
|
|
197
|
+
|
|
198
|
+
-- =============================================================================
|
|
199
|
+
-- 5. Drop post_collections table (replaced by posts.collection_id)
|
|
200
|
+
-- =============================================================================
|
|
201
|
+
|
|
202
|
+
DROP TABLE `post_collections`;
|
|
203
|
+
--> statement-breakpoint
|
|
204
|
+
|
|
205
|
+
-- =============================================================================
|
|
206
|
+
-- 6. Rebuild FTS5 (column rename: content→body, add quote_text)
|
|
207
|
+
-- =============================================================================
|
|
208
|
+
|
|
209
|
+
-- Drop old FTS triggers
|
|
210
|
+
DROP TRIGGER IF EXISTS posts_fts_insert;
|
|
211
|
+
--> statement-breakpoint
|
|
212
|
+
DROP TRIGGER IF EXISTS posts_fts_update;
|
|
213
|
+
--> statement-breakpoint
|
|
214
|
+
DROP TRIGGER IF EXISTS posts_fts_delete;
|
|
215
|
+
--> statement-breakpoint
|
|
216
|
+
|
|
217
|
+
-- Drop old FTS table
|
|
218
|
+
DROP TABLE IF EXISTS posts_fts;
|
|
219
|
+
--> statement-breakpoint
|
|
220
|
+
|
|
221
|
+
-- Create new FTS table with updated columns
|
|
222
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
|
223
|
+
title,
|
|
224
|
+
body,
|
|
225
|
+
quote_text,
|
|
226
|
+
content=posts,
|
|
227
|
+
content_rowid=id,
|
|
228
|
+
tokenize='trigram'
|
|
229
|
+
);
|
|
230
|
+
--> statement-breakpoint
|
|
231
|
+
|
|
232
|
+
-- Populate FTS with migrated data
|
|
233
|
+
INSERT INTO posts_fts(rowid, title, body, quote_text)
|
|
234
|
+
SELECT id, COALESCE(title, ''), COALESCE(body, ''), COALESCE(quote_text, '')
|
|
235
|
+
FROM posts WHERE deleted_at IS NULL;
|
|
236
|
+
--> statement-breakpoint
|
|
237
|
+
|
|
238
|
+
-- Trigger: sync FTS on INSERT
|
|
239
|
+
CREATE TRIGGER posts_fts_insert AFTER INSERT ON posts
|
|
240
|
+
WHEN NEW.deleted_at IS NULL
|
|
241
|
+
BEGIN
|
|
242
|
+
INSERT INTO posts_fts(rowid, title, body, quote_text)
|
|
243
|
+
VALUES (NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.body, ''), COALESCE(NEW.quote_text, ''));
|
|
244
|
+
END;
|
|
245
|
+
--> statement-breakpoint
|
|
246
|
+
|
|
247
|
+
-- Trigger: sync FTS on UPDATE
|
|
248
|
+
CREATE TRIGGER posts_fts_update AFTER UPDATE ON posts BEGIN
|
|
249
|
+
DELETE FROM posts_fts WHERE rowid = OLD.id;
|
|
250
|
+
INSERT INTO posts_fts(rowid, title, body, quote_text)
|
|
251
|
+
SELECT NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.body, ''), COALESCE(NEW.quote_text, '')
|
|
252
|
+
WHERE NEW.deleted_at IS NULL;
|
|
253
|
+
END;
|
|
254
|
+
--> statement-breakpoint
|
|
255
|
+
|
|
256
|
+
-- Trigger: sync FTS on DELETE
|
|
257
|
+
CREATE TRIGGER posts_fts_delete AFTER DELETE ON posts BEGIN
|
|
258
|
+
DELETE FROM posts_fts WHERE rowid = OLD.id;
|
|
259
|
+
END;
|
|
260
|
+
--> statement-breakpoint
|
|
261
|
+
|
|
262
|
+
-- =============================================================================
|
|
263
|
+
-- 7. Re-enable FK checks and verify integrity
|
|
264
|
+
-- =============================================================================
|
|
265
|
+
|
|
266
|
+
PRAGMA foreign_keys = ON;
|
|
267
|
+
--> statement-breakpoint
|
|
268
|
+
PRAGMA foreign_key_check;
|
package/src/db/schema.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Drizzle Schema
|
|
3
3
|
*
|
|
4
|
-
* Database schema for Jant
|
|
4
|
+
* Database schema for Jant v2
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
sqliteTable,
|
|
9
|
-
text,
|
|
10
|
-
integer,
|
|
11
|
-
primaryKey,
|
|
12
|
-
} from "drizzle-orm/sqlite-core";
|
|
7
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
13
8
|
|
|
14
9
|
// =============================================================================
|
|
15
10
|
// Posts
|
|
@@ -17,21 +12,26 @@ import {
|
|
|
17
12
|
|
|
18
13
|
export const posts = sqliteTable("posts", {
|
|
19
14
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
20
|
-
|
|
21
|
-
enum: ["note", "
|
|
15
|
+
format: text("format", {
|
|
16
|
+
enum: ["note", "link", "quote"],
|
|
22
17
|
}).notNull(),
|
|
23
|
-
|
|
24
|
-
enum: ["
|
|
18
|
+
status: text("status", {
|
|
19
|
+
enum: ["draft", "published"],
|
|
25
20
|
})
|
|
26
21
|
.notNull()
|
|
27
|
-
.default("
|
|
22
|
+
.default("published"),
|
|
23
|
+
featured: integer("featured").notNull().default(0),
|
|
24
|
+
pinned: integer("pinned").notNull().default(0),
|
|
25
|
+
slug: text("slug").unique(),
|
|
28
26
|
title: text("title"),
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
url: text("url"),
|
|
28
|
+
body: text("body"),
|
|
29
|
+
bodyHtml: text("body_html"),
|
|
30
|
+
quoteText: text("quote_text"),
|
|
31
|
+
rating: integer("rating"),
|
|
32
|
+
collectionId: integer("collection_id").references(() => collections.id, {
|
|
33
|
+
onDelete: "set null",
|
|
34
|
+
}),
|
|
35
35
|
replyToId: integer("reply_to_id"),
|
|
36
36
|
threadId: integer("thread_id"),
|
|
37
37
|
deletedAt: integer("deleted_at"),
|
|
@@ -40,6 +40,25 @@ export const posts = sqliteTable("posts", {
|
|
|
40
40
|
updatedAt: integer("updated_at").notNull(),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Pages
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
export const pages = sqliteTable("pages", {
|
|
48
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
49
|
+
slug: text("slug").notNull().unique(),
|
|
50
|
+
title: text("title"),
|
|
51
|
+
body: text("body"),
|
|
52
|
+
bodyHtml: text("body_html"),
|
|
53
|
+
status: text("status", {
|
|
54
|
+
enum: ["draft", "published"],
|
|
55
|
+
})
|
|
56
|
+
.notNull()
|
|
57
|
+
.default("published"),
|
|
58
|
+
createdAt: integer("created_at").notNull(),
|
|
59
|
+
updatedAt: integer("updated_at").notNull(),
|
|
60
|
+
});
|
|
61
|
+
|
|
43
62
|
// =============================================================================
|
|
44
63
|
// Media
|
|
45
64
|
// =============================================================================
|
|
@@ -67,30 +86,41 @@ export const media = sqliteTable("media", {
|
|
|
67
86
|
|
|
68
87
|
export const collections = sqliteTable("collections", {
|
|
69
88
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
70
|
-
|
|
89
|
+
slug: text("slug").notNull().unique(),
|
|
71
90
|
title: text("title").notNull(),
|
|
72
91
|
description: text("description"),
|
|
92
|
+
icon: text("icon"),
|
|
93
|
+
sortOrder: text("sort_order", {
|
|
94
|
+
enum: ["newest", "oldest", "rating_desc", "rating_asc"],
|
|
95
|
+
})
|
|
96
|
+
.notNull()
|
|
97
|
+
.default("newest"),
|
|
98
|
+
position: integer("position").notNull().default(0),
|
|
99
|
+
showDivider: integer("show_divider").notNull().default(0),
|
|
73
100
|
createdAt: integer("created_at").notNull(),
|
|
74
101
|
updatedAt: integer("updated_at").notNull(),
|
|
75
102
|
});
|
|
76
103
|
|
|
77
104
|
// =============================================================================
|
|
78
|
-
//
|
|
105
|
+
// Navigation Items
|
|
79
106
|
// =============================================================================
|
|
80
107
|
|
|
81
|
-
export const
|
|
82
|
-
"
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
108
|
+
export const navItems = sqliteTable("nav_items", {
|
|
109
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
110
|
+
type: text("type", {
|
|
111
|
+
enum: ["page", "link"],
|
|
112
|
+
})
|
|
113
|
+
.notNull()
|
|
114
|
+
.default("link"),
|
|
115
|
+
label: text("label").notNull(),
|
|
116
|
+
url: text("url").notNull(),
|
|
117
|
+
pageId: integer("page_id").references(() => pages.id, {
|
|
118
|
+
onDelete: "cascade",
|
|
119
|
+
}),
|
|
120
|
+
position: integer("position").notNull().default(0),
|
|
121
|
+
createdAt: integer("created_at").notNull(),
|
|
122
|
+
updatedAt: integer("updated_at").notNull(),
|
|
123
|
+
});
|
|
94
124
|
|
|
95
125
|
// =============================================================================
|
|
96
126
|
// Redirects
|
|
@@ -104,19 +134,6 @@ export const redirects = sqliteTable("redirects", {
|
|
|
104
134
|
createdAt: integer("created_at").notNull(),
|
|
105
135
|
});
|
|
106
136
|
|
|
107
|
-
// =============================================================================
|
|
108
|
-
// Navigation Links
|
|
109
|
-
// =============================================================================
|
|
110
|
-
|
|
111
|
-
export const navigationLinks = sqliteTable("navigation_links", {
|
|
112
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
113
|
-
label: text("label").notNull(),
|
|
114
|
-
url: text("url").notNull(),
|
|
115
|
-
position: integer("position").notNull().default(0),
|
|
116
|
-
createdAt: integer("created_at").notNull(),
|
|
117
|
-
updatedAt: integer("updated_at").notNull(),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
137
|
// =============================================================================
|
|
121
138
|
// Settings (Key-Value)
|
|
122
139
|
// =============================================================================
|