@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -1,8 +1,9 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest";
2
2
  import { createTestDatabase } from "../../__tests__/helpers/db.js";
3
+ import { collections, sidebarItems } from "../../db/schema.js";
3
4
  import { createCollectionService } from "../collection.js";
5
+ import { createPathService } from "../path.js";
4
6
  import { createPostService } from "../post.js";
5
- import { createPathRegistryService } from "../path-registry.js";
6
7
  import type { Database } from "../../db/index.js";
7
8
 
8
9
  describe("CollectionService", () => {
@@ -14,7 +15,7 @@ describe("CollectionService", () => {
14
15
  const testDb = createTestDatabase();
15
16
  db = testDb.db as unknown as Database;
16
17
  collectionService = createCollectionService(db);
17
- postService = createPostService(db, createPathRegistryService(db));
18
+ postService = createPostService(db, { slugIdLength: 5 });
18
19
  });
19
20
 
20
21
  describe("create", () => {
@@ -24,7 +25,8 @@ describe("CollectionService", () => {
24
25
  title: "My Collection",
25
26
  });
26
27
 
27
- expect(collection.id).toBe(1);
28
+ expect(typeof collection.id).toBe("string");
29
+ expect(collection.id.length).toBeGreaterThan(0);
28
30
  expect(collection.slug).toBe("my-collection");
29
31
  expect(collection.title).toBe("My Collection");
30
32
  expect(collection.description).toBeNull();
@@ -39,7 +41,6 @@ describe("CollectionService", () => {
39
41
  description: "Posts about technology",
40
42
  icon: "laptop",
41
43
  sortOrder: "oldest",
42
- position: 5,
43
44
  });
44
45
 
45
46
  expect(collection.slug).toBe("tech");
@@ -47,7 +48,6 @@ describe("CollectionService", () => {
47
48
  expect(collection.description).toBe("Posts about technology");
48
49
  expect(collection.icon).toBe("laptop");
49
50
  expect(collection.sortOrder).toBe("oldest");
50
- expect(collection.position).toBe(5);
51
51
  });
52
52
 
53
53
  it("sets timestamps", async () => {
@@ -60,23 +60,41 @@ describe("CollectionService", () => {
60
60
  expect(collection.updatedAt).toBeGreaterThan(0);
61
61
  });
62
62
 
63
- it("auto-assigns position when not provided", async () => {
64
- const first = await collectionService.create({
65
- slug: "first",
66
- title: "First",
67
- });
68
- const second = await collectionService.create({
69
- slug: "second",
70
- title: "Second",
63
+ it("auto-creates a sidebar item", async () => {
64
+ const collection = await collectionService.create({
65
+ slug: "test",
66
+ title: "Test",
71
67
  });
72
- const third = await collectionService.create({
73
- slug: "third",
74
- title: "Third",
68
+
69
+ const sidebarItems = await collectionService.listSidebarItems();
70
+ expect(sidebarItems).toHaveLength(1);
71
+ expect(sidebarItems[0]?.type).toBe("collection");
72
+ expect(sidebarItems[0]?.collectionId).toBe(collection.id);
73
+ expect(typeof sidebarItems[0]?.position).toBe("string");
74
+ });
75
+
76
+ it("rolls back the collection insert when slug persistence fails inside the batch", async () => {
77
+ await collectionService.create({
78
+ slug: "race-condition",
79
+ title: "Existing",
75
80
  });
76
81
 
77
- expect(first.position).toBe(0);
78
- expect(second.position).toBe(1);
79
- expect(third.position).toBe(2);
82
+ const paths = createPathService(db);
83
+ const raceyPaths = {
84
+ ...paths,
85
+ isPathAvailable: async () => true,
86
+ };
87
+ const raceyCollectionService = createCollectionService(db, raceyPaths);
88
+
89
+ await expect(
90
+ raceyCollectionService.create({
91
+ slug: "race-condition",
92
+ title: "Race Condition",
93
+ }),
94
+ ).rejects.toThrow('Slug "race-condition" is already in use');
95
+
96
+ const rows = await db.select({ id: collections.id }).from(collections);
97
+ expect(rows).toHaveLength(1);
80
98
  });
81
99
  });
82
100
 
@@ -94,7 +112,9 @@ describe("CollectionService", () => {
94
112
  });
95
113
 
96
114
  it("returns null for non-existent ID", async () => {
97
- const found = await collectionService.getById(9999);
115
+ const found = await collectionService.getById(
116
+ "00000000-0000-0000-0000-000000009999",
117
+ );
98
118
  expect(found).toBeNull();
99
119
  });
100
120
  });
@@ -129,29 +149,6 @@ describe("CollectionService", () => {
129
149
  const list = await collectionService.list();
130
150
  expect(list).toHaveLength(3);
131
151
  });
132
-
133
- it("orders by position ASC, then createdAt DESC", async () => {
134
- const a = await collectionService.create({
135
- slug: "a",
136
- title: "A",
137
- position: 2,
138
- });
139
- const b = await collectionService.create({
140
- slug: "b",
141
- title: "B",
142
- position: 0,
143
- });
144
- const c = await collectionService.create({
145
- slug: "c",
146
- title: "C",
147
- position: 1,
148
- });
149
-
150
- const list = await collectionService.list();
151
- expect(list[0]?.id).toBe(b.id);
152
- expect(list[1]?.id).toBe(c.id);
153
- expect(list[2]?.id).toBe(a.id);
154
- });
155
152
  });
156
153
 
157
154
  describe("update", () => {
@@ -212,7 +209,7 @@ describe("CollectionService", () => {
212
209
  expect(updated?.icon).toBeNull();
213
210
  });
214
211
 
215
- it("updates icon, sortOrder, and position", async () => {
212
+ it("updates icon and sortOrder", async () => {
216
213
  const collection = await collectionService.create({
217
214
  slug: "test",
218
215
  title: "Test",
@@ -221,12 +218,10 @@ describe("CollectionService", () => {
221
218
  const updated = await collectionService.update(collection.id, {
222
219
  icon: "rocket",
223
220
  sortOrder: "rating_desc",
224
- position: 10,
225
221
  });
226
222
 
227
223
  expect(updated?.icon).toBe("rocket");
228
224
  expect(updated?.sortOrder).toBe("rating_desc");
229
- expect(updated?.position).toBe(10);
230
225
  });
231
226
 
232
227
  it("updates updatedAt timestamp", async () => {
@@ -243,7 +238,10 @@ describe("CollectionService", () => {
243
238
  });
244
239
 
245
240
  it("returns null for non-existent collection", async () => {
246
- const result = await collectionService.update(9999, { title: "X" });
241
+ const result = await collectionService.update(
242
+ "00000000-0000-0000-0000-000000009999",
243
+ { title: "X" },
244
+ );
247
245
  expect(result).toBeNull();
248
246
  });
249
247
  });
@@ -269,7 +267,7 @@ describe("CollectionService", () => {
269
267
  });
270
268
  const post = await postService.create({
271
269
  format: "note",
272
- body: "test post",
270
+ bodyMarkdown: "test post",
273
271
  });
274
272
 
275
273
  await collectionService.addPost(collection.id, post.id);
@@ -289,58 +287,213 @@ describe("CollectionService", () => {
289
287
  expect(after).toHaveLength(0);
290
288
  });
291
289
 
290
+ it("removes sidebar item when collection is deleted", async () => {
291
+ const collection = await collectionService.create({
292
+ slug: "test",
293
+ title: "Test",
294
+ });
295
+
296
+ // Verify sidebar item exists
297
+ const before = await collectionService.listSidebarItems();
298
+ expect(before).toHaveLength(1);
299
+
300
+ await collectionService.delete(collection.id);
301
+
302
+ // Sidebar item should be gone
303
+ const after = await collectionService.listSidebarItems();
304
+ expect(after).toHaveLength(0);
305
+ });
306
+
292
307
  it("returns false for non-existent collection", async () => {
293
- const result = await collectionService.delete(9999);
308
+ const result = await collectionService.delete(
309
+ "00000000-0000-0000-0000-000000009999",
310
+ );
294
311
  expect(result).toBe(false);
295
312
  });
296
313
  });
297
314
 
298
- describe("reorder", () => {
299
- it("updates positions based on array order", async () => {
300
- const a = await collectionService.create({ slug: "a", title: "A" });
301
- const b = await collectionService.create({ slug: "b", title: "B" });
302
- const c = await collectionService.create({ slug: "c", title: "C" });
315
+ describe("listSidebarItems", () => {
316
+ it("returns empty array when no items exist", async () => {
317
+ const items = await collectionService.listSidebarItems();
318
+ expect(items).toEqual([]);
319
+ });
320
+
321
+ it("returns items ordered by position", async () => {
322
+ await collectionService.create({ slug: "first", title: "First" });
323
+ await collectionService.create({ slug: "second", title: "Second" });
324
+
325
+ const items = await collectionService.listSidebarItems();
326
+ expect(items).toHaveLength(2);
327
+ expect(items[0]?.type).toBe("collection");
328
+ expect(items[1]?.type).toBe("collection");
329
+ // First created should come first (string comparison for fractional indexing)
330
+ const pos0 = items[0]?.position ?? "";
331
+ const pos1 = items[1]?.position ?? "";
332
+ expect(pos0 < pos1).toBe(true);
333
+ });
334
+
335
+ it("includes dividers", async () => {
336
+ await collectionService.create({ slug: "a", title: "A" });
337
+ await collectionService.createSidebarItem("divider");
338
+ await collectionService.create({ slug: "b", title: "B" });
303
339
 
304
- // Reverse the order: C, B, A
305
- await collectionService.reorder([c.id, b.id, a.id]);
340
+ const items = await collectionService.listSidebarItems();
341
+ expect(items).toHaveLength(3);
342
+ expect(items[0]?.type).toBe("collection");
343
+ expect(items[1]?.type).toBe("divider");
344
+ expect(items[2]?.type).toBe("collection");
345
+ });
346
+ });
306
347
 
307
- const reorderedC = await collectionService.getById(c.id);
308
- const reorderedB = await collectionService.getById(b.id);
309
- const reorderedA = await collectionService.getById(a.id);
348
+ describe("createSidebarItem", () => {
349
+ it("creates a divider", async () => {
350
+ const item = await collectionService.createSidebarItem("divider");
310
351
 
311
- expect(reorderedC?.position).toBe(0);
312
- expect(reorderedB?.position).toBe(1);
313
- expect(reorderedA?.position).toBe(2);
352
+ expect(item.type).toBe("divider");
353
+ expect(item.collectionId).toBeNull();
354
+ expect(typeof item.position).toBe("string");
355
+ expect(item.createdAt).toBeGreaterThan(0);
314
356
  });
315
357
 
316
- it("updates updatedAt when reordering", async () => {
317
- const a = await collectionService.create({ slug: "a", title: "A" });
318
- const b = await collectionService.create({ slug: "b", title: "B" });
358
+ it("creates items with incrementing positions", async () => {
359
+ const first = await collectionService.createSidebarItem("divider");
360
+ const second = await collectionService.createSidebarItem("divider");
319
361
 
320
- await collectionService.reorder([b.id, a.id]);
362
+ expect(first.position < second.position).toBe(true);
363
+ });
364
+
365
+ it("rejects adding the same collection twice", async () => {
366
+ const collection = await collectionService.create({
367
+ slug: "notes",
368
+ title: "Notes",
369
+ });
321
370
 
322
- const reorderedA = await collectionService.getById(a.id);
323
- expect(reorderedA?.updatedAt).toBeGreaterThanOrEqual(a.updatedAt);
371
+ await expect(
372
+ collectionService.createSidebarItem("collection", collection.id),
373
+ ).rejects.toThrow("Collection is already in the sidebar.");
324
374
  });
325
375
 
326
- it("handles empty array", async () => {
327
- await collectionService.reorder([]);
328
- // Should not throw
329
- const list = await collectionService.list();
330
- expect(list).toEqual([]);
376
+ it("rejects duplicate sidebar positions at the database layer", async () => {
377
+ const item = await collectionService.createSidebarItem("divider");
378
+
379
+ await expect(
380
+ db.insert(sidebarItems).values({
381
+ id: "00000000-0000-7000-8000-000000000001",
382
+ type: "divider",
383
+ collectionId: null,
384
+ position: item.position,
385
+ createdAt: item.createdAt,
386
+ updatedAt: item.updatedAt,
387
+ }),
388
+ ).rejects.toThrow();
331
389
  });
390
+ });
332
391
 
333
- it("reflects new order in list()", async () => {
334
- const a = await collectionService.create({ slug: "a", title: "A" });
335
- const b = await collectionService.create({ slug: "b", title: "B" });
336
- const c = await collectionService.create({ slug: "c", title: "C" });
392
+ describe("deleteSidebarItem", () => {
393
+ it("deletes a sidebar item", async () => {
394
+ const item = await collectionService.createSidebarItem("divider");
395
+ const result = await collectionService.deleteSidebarItem(item.id);
396
+ expect(result).toBe(true);
337
397
 
338
- await collectionService.reorder([c.id, a.id, b.id]);
398
+ const items = await collectionService.listSidebarItems();
399
+ expect(items).toHaveLength(0);
400
+ });
339
401
 
340
- const list = await collectionService.list();
341
- expect(list[0]?.id).toBe(c.id);
342
- expect(list[1]?.id).toBe(a.id);
343
- expect(list[2]?.id).toBe(b.id);
402
+ it("returns false for non-existent item", async () => {
403
+ const result = await collectionService.deleteSidebarItem(
404
+ "00000000-0000-0000-0000-000000009999",
405
+ );
406
+ expect(result).toBe(false);
407
+ });
408
+ });
409
+
410
+ describe("moveSidebarItem", () => {
411
+ it("moves an item between two others", async () => {
412
+ const col1 = await collectionService.create({ slug: "a", title: "A" });
413
+ const col2 = await collectionService.create({ slug: "b", title: "B" });
414
+ const col3 = await collectionService.create({ slug: "c", title: "C" });
415
+
416
+ // Get sidebar items (A, B, C order)
417
+ const items = await collectionService.listSidebarItems();
418
+ expect(items).toHaveLength(3);
419
+ const itemA = items.find((i) => i.collectionId === col1.id);
420
+ const itemB = items.find((i) => i.collectionId === col2.id);
421
+ const itemC = items.find((i) => i.collectionId === col3.id);
422
+ expect(itemA).toBeDefined();
423
+ expect(itemB).toBeDefined();
424
+ expect(itemC).toBeDefined();
425
+
426
+ // Move C between A and B
427
+ const moved = await collectionService.moveSidebarItem(
428
+ itemC?.id ?? "",
429
+ itemA?.id ?? "",
430
+ itemB?.id ?? "",
431
+ );
432
+
433
+ expect(moved).not.toBeNull();
434
+
435
+ // Verify new order: A, C, B
436
+ const reordered = await collectionService.listSidebarItems();
437
+ expect(reordered[0]?.collectionId).toBe(col1.id);
438
+ expect(reordered[1]?.collectionId).toBe(col3.id);
439
+ expect(reordered[2]?.collectionId).toBe(col2.id);
440
+ });
441
+
442
+ it("moves an item to the beginning", async () => {
443
+ const col1 = await collectionService.create({ slug: "a", title: "A" });
444
+ const col2 = await collectionService.create({ slug: "b", title: "B" });
445
+ const col3 = await collectionService.create({ slug: "c", title: "C" });
446
+
447
+ const items = await collectionService.listSidebarItems();
448
+ const itemA = items.find((i) => i.collectionId === col1.id);
449
+ const itemC = items.find((i) => i.collectionId === col3.id);
450
+ expect(itemA).toBeDefined();
451
+ expect(itemC).toBeDefined();
452
+
453
+ // Move C to the beginning (before A, no after)
454
+ await collectionService.moveSidebarItem(
455
+ itemC?.id ?? "",
456
+ null,
457
+ itemA?.id ?? "",
458
+ );
459
+
460
+ const reordered = await collectionService.listSidebarItems();
461
+ expect(reordered[0]?.collectionId).toBe(col3.id);
462
+ expect(reordered[1]?.collectionId).toBe(col1.id);
463
+ expect(reordered[2]?.collectionId).toBe(col2.id);
464
+ });
465
+
466
+ it("moves an item to the end", async () => {
467
+ const col1 = await collectionService.create({ slug: "a", title: "A" });
468
+ const col2 = await collectionService.create({ slug: "b", title: "B" });
469
+ const col3 = await collectionService.create({ slug: "c", title: "C" });
470
+
471
+ const items = await collectionService.listSidebarItems();
472
+ const itemA = items.find((i) => i.collectionId === col1.id);
473
+ const itemC = items.find((i) => i.collectionId === col3.id);
474
+ expect(itemA).toBeDefined();
475
+ expect(itemC).toBeDefined();
476
+
477
+ // Move A to the end (after C, no before)
478
+ await collectionService.moveSidebarItem(
479
+ itemA?.id ?? "",
480
+ itemC?.id ?? "",
481
+ null,
482
+ );
483
+
484
+ const reordered = await collectionService.listSidebarItems();
485
+ expect(reordered[0]?.collectionId).toBe(col2.id);
486
+ expect(reordered[1]?.collectionId).toBe(col3.id);
487
+ expect(reordered[2]?.collectionId).toBe(col1.id);
488
+ });
489
+
490
+ it("returns null for non-existent item", async () => {
491
+ const result = await collectionService.moveSidebarItem(
492
+ "00000000-0000-0000-0000-000000009999",
493
+ null,
494
+ null,
495
+ );
496
+ expect(result).toBeNull();
344
497
  });
345
498
  });
346
499
 
@@ -362,9 +515,18 @@ describe("CollectionService", () => {
362
515
  title: "Col 2",
363
516
  });
364
517
 
365
- const p1 = await postService.create({ format: "note", body: "post 1" });
366
- const p2 = await postService.create({ format: "note", body: "post 2" });
367
- const p3 = await postService.create({ format: "note", body: "post 3" });
518
+ const p1 = await postService.create({
519
+ format: "note",
520
+ bodyMarkdown: "post 1",
521
+ });
522
+ const p2 = await postService.create({
523
+ format: "note",
524
+ bodyMarkdown: "post 2",
525
+ });
526
+ const p3 = await postService.create({
527
+ format: "note",
528
+ bodyMarkdown: "post 3",
529
+ });
368
530
 
369
531
  await collectionService.addPost(col1.id, p1.id);
370
532
  await collectionService.addPost(col1.id, p2.id);
@@ -383,9 +545,12 @@ describe("CollectionService", () => {
383
545
 
384
546
  const p1 = await postService.create({
385
547
  format: "note",
386
- body: "with collection",
548
+ bodyMarkdown: "with collection",
549
+ });
550
+ await postService.create({
551
+ format: "note",
552
+ bodyMarkdown: "no collection",
387
553
  });
388
- await postService.create({ format: "note", body: "no collection" });
389
554
 
390
555
  await collectionService.addPost(col.id, p1.id);
391
556
 
@@ -402,11 +567,11 @@ describe("CollectionService", () => {
402
567
 
403
568
  const post = await postService.create({
404
569
  format: "note",
405
- body: "will be deleted",
570
+ bodyMarkdown: "will be deleted",
406
571
  });
407
572
  const post2 = await postService.create({
408
573
  format: "note",
409
- body: "still alive",
574
+ bodyMarkdown: "still alive",
410
575
  });
411
576
 
412
577
  await collectionService.addPost(col.id, post.id);
@@ -428,7 +593,7 @@ describe("CollectionService", () => {
428
593
  });
429
594
  const post = await postService.create({
430
595
  format: "note",
431
- body: "test",
596
+ bodyMarkdown: "test",
432
597
  });
433
598
 
434
599
  await collectionService.addPost(col.id, post.id);
@@ -447,7 +612,7 @@ describe("CollectionService", () => {
447
612
  });
448
613
  const post = await postService.create({
449
614
  format: "note",
450
- body: "test",
615
+ bodyMarkdown: "test",
451
616
  });
452
617
 
453
618
  await collectionService.addPost(col.id, post.id);
@@ -464,7 +629,7 @@ describe("CollectionService", () => {
464
629
  });
465
630
  const post = await postService.create({
466
631
  format: "note",
467
- body: "test",
632
+ bodyMarkdown: "test",
468
633
  });
469
634
 
470
635
  await collectionService.addPost(col.id, post.id);
@@ -479,37 +644,35 @@ describe("CollectionService", () => {
479
644
 
480
645
  describe("getCollectionsByPostId", () => {
481
646
  it("returns all collections a post belongs to", async () => {
482
- const col1 = await collectionService.create({
647
+ await collectionService.create({
483
648
  slug: "col1",
484
649
  title: "Col 1",
485
- position: 0,
486
650
  });
487
- const col2 = await collectionService.create({
651
+ await collectionService.create({
488
652
  slug: "col2",
489
653
  title: "Col 2",
490
- position: 1,
491
654
  });
492
655
 
656
+ const cols = await collectionService.list();
657
+ expect(cols).toHaveLength(2);
493
658
  const post = await postService.create({
494
659
  format: "note",
495
- body: "test",
660
+ bodyMarkdown: "test",
496
661
  });
497
662
 
498
- await collectionService.addPost(col1.id, post.id);
499
- await collectionService.addPost(col2.id, post.id);
663
+ await collectionService.addPost(cols[0]?.id ?? "", post.id);
664
+ await collectionService.addPost(cols[1]?.id ?? "", post.id);
500
665
 
501
666
  const collections = await collectionService.getCollectionsByPostId(
502
667
  post.id,
503
668
  );
504
669
  expect(collections).toHaveLength(2);
505
- expect(collections[0]?.slug).toBe("col1");
506
- expect(collections[1]?.slug).toBe("col2");
507
670
  });
508
671
 
509
672
  it("returns empty array for post with no collections", async () => {
510
673
  const post = await postService.create({
511
674
  format: "note",
512
- body: "test",
675
+ bodyMarkdown: "test",
513
676
  });
514
677
 
515
678
  const collections = await collectionService.getCollectionsByPostId(
@@ -525,8 +688,14 @@ describe("CollectionService", () => {
525
688
  slug: "test",
526
689
  title: "Test",
527
690
  });
528
- const p1 = await postService.create({ format: "note", body: "one" });
529
- const p2 = await postService.create({ format: "note", body: "two" });
691
+ const p1 = await postService.create({
692
+ format: "note",
693
+ bodyMarkdown: "one",
694
+ });
695
+ const p2 = await postService.create({
696
+ format: "note",
697
+ bodyMarkdown: "two",
698
+ });
530
699
 
531
700
  await collectionService.addPost(col.id, p1.id);
532
701
  await collectionService.addPost(col.id, p2.id);
@@ -538,127 +707,6 @@ describe("CollectionService", () => {
538
707
  });
539
708
  });
540
709
 
541
- describe("createDivider", () => {
542
- it("creates a divider with auto-assigned position", async () => {
543
- const divider = await collectionService.createDivider();
544
-
545
- expect(divider.id).toBe(1);
546
- expect(divider.position).toBe(0);
547
- expect(divider.createdAt).toBeGreaterThan(0);
548
- expect(divider.updatedAt).toBeGreaterThan(0);
549
- });
550
-
551
- it("assigns position after existing collections", async () => {
552
- await collectionService.create({ slug: "a", title: "A" }); // position 0
553
- await collectionService.create({ slug: "b", title: "B" }); // position 1
554
-
555
- const divider = await collectionService.createDivider();
556
- expect(divider.position).toBe(2);
557
- });
558
-
559
- it("assigns position after existing dividers", async () => {
560
- const d1 = await collectionService.createDivider(); // position 0
561
- const d2 = await collectionService.createDivider(); // position 1
562
-
563
- expect(d1.position).toBe(0);
564
- expect(d2.position).toBe(1);
565
- });
566
-
567
- it("considers both collections and dividers for position", async () => {
568
- await collectionService.create({ slug: "a", title: "A" }); // position 0
569
- await collectionService.createDivider(); // position 1
570
- await collectionService.create({ slug: "b", title: "B" }); // position 2
571
-
572
- const divider = await collectionService.createDivider();
573
- expect(divider.position).toBe(3);
574
- });
575
- });
576
-
577
- describe("deleteDivider", () => {
578
- it("deletes a divider by ID", async () => {
579
- const divider = await collectionService.createDivider();
580
-
581
- const result = await collectionService.deleteDivider(divider.id);
582
- expect(result).toBe(true);
583
-
584
- const list = await collectionService.listDividers();
585
- expect(list).toHaveLength(0);
586
- });
587
-
588
- it("returns false for non-existent divider", async () => {
589
- const result = await collectionService.deleteDivider(9999);
590
- expect(result).toBe(false);
591
- });
592
- });
593
-
594
- describe("listDividers", () => {
595
- it("returns empty array when no dividers exist", async () => {
596
- const list = await collectionService.listDividers();
597
- expect(list).toEqual([]);
598
- });
599
-
600
- it("returns dividers ordered by position", async () => {
601
- const d1 = await collectionService.createDivider();
602
- const d2 = await collectionService.createDivider();
603
-
604
- const list = await collectionService.listDividers();
605
- expect(list).toHaveLength(2);
606
- expect(list[0]?.id).toBe(d1.id);
607
- expect(list[1]?.id).toBe(d2.id);
608
- });
609
- });
610
-
611
- describe("reorderAll", () => {
612
- it("handles mixed prefixed IDs correctly", async () => {
613
- const a = await collectionService.create({ slug: "a", title: "A" });
614
- const b = await collectionService.create({ slug: "b", title: "B" });
615
- const d1 = await collectionService.createDivider();
616
-
617
- // Reorder: divider first, then B, then A
618
- await collectionService.reorderAll([
619
- `d-${d1.id}`,
620
- `c-${b.id}`,
621
- `c-${a.id}`,
622
- ]);
623
-
624
- const dividers = await collectionService.listDividers();
625
- expect(dividers[0]?.position).toBe(0);
626
-
627
- const colB = await collectionService.getById(b.id);
628
- const colA = await collectionService.getById(a.id);
629
- expect(colB?.position).toBe(1);
630
- expect(colA?.position).toBe(2);
631
- });
632
-
633
- it("handles empty array", async () => {
634
- await collectionService.reorderAll([]);
635
- // Should not throw
636
- const list = await collectionService.list();
637
- expect(list).toEqual([]);
638
- });
639
-
640
- it("reflects new order in combined list", async () => {
641
- const a = await collectionService.create({ slug: "a", title: "A" });
642
- const d1 = await collectionService.createDivider();
643
- const b = await collectionService.create({ slug: "b", title: "B" });
644
-
645
- // Put divider between B and A
646
- await collectionService.reorderAll([
647
- `c-${b.id}`,
648
- `d-${d1.id}`,
649
- `c-${a.id}`,
650
- ]);
651
-
652
- const cols = await collectionService.list();
653
- const divs = await collectionService.listDividers();
654
-
655
- // B at position 0, divider at 1, A at 2
656
- expect(cols.find((c) => c.id === b.id)?.position).toBe(0);
657
- expect(divs[0]?.position).toBe(1);
658
- expect(cols.find((c) => c.id === a.id)?.position).toBe(2);
659
- });
660
- });
661
-
662
710
  describe("syncPostCollections", () => {
663
711
  it("replaces all collection memberships for a post", async () => {
664
712
  const col1 = await collectionService.create({
@@ -676,7 +724,7 @@ describe("CollectionService", () => {
676
724
 
677
725
  const post = await postService.create({
678
726
  format: "note",
679
- body: "test",
727
+ bodyMarkdown: "test",
680
728
  });
681
729
 
682
730
  // Initially in col1 and col2
@@ -703,7 +751,7 @@ describe("CollectionService", () => {
703
751
  });
704
752
  const post = await postService.create({
705
753
  format: "note",
706
- body: "test",
754
+ bodyMarkdown: "test",
707
755
  });
708
756
 
709
757
  await collectionService.addPost(col.id, post.id);