@jant/core 0.3.36 → 0.3.38

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 (271) 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/url-FWFqPJPb.js +1 -0
  6. package/dist/client/client.css +1 -1
  7. package/dist/client/client.js +4012 -3276
  8. package/dist/index.js +10285 -5809
  9. package/package.json +11 -3
  10. package/src/__tests__/helpers/app.ts +9 -9
  11. package/src/__tests__/helpers/db.ts +91 -93
  12. package/src/app.tsx +157 -27
  13. package/src/auth.ts +20 -2
  14. package/src/client/archive-nav.js +187 -0
  15. package/src/client/audio-player.ts +478 -0
  16. package/src/client/audio-processor.ts +84 -0
  17. package/src/client/avatar-upload.ts +3 -2
  18. package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
  19. package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
  20. package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
  21. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  22. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  23. package/src/client/components/collection-sidebar-types.ts +7 -9
  24. package/src/client/components/compose-types.ts +101 -4
  25. package/src/client/components/jant-collection-form.ts +43 -7
  26. package/src/client/components/jant-collection-sidebar.ts +88 -84
  27. package/src/client/components/jant-compose-dialog.ts +1655 -219
  28. package/src/client/components/jant-compose-editor.ts +732 -168
  29. package/src/client/components/jant-compose-fullscreen.ts +23 -78
  30. package/src/client/components/jant-media-lightbox.ts +2 -0
  31. package/src/client/components/jant-nav-manager.ts +24 -284
  32. package/src/client/components/jant-post-form.ts +89 -9
  33. package/src/client/components/jant-post-menu.ts +1019 -0
  34. package/src/client/components/jant-settings-avatar.ts +3 -3
  35. package/src/client/components/jant-settings-general.ts +38 -4
  36. package/src/client/components/jant-text-preview.ts +232 -0
  37. package/src/client/components/nav-manager-types.ts +4 -19
  38. package/src/client/components/post-form-template.ts +107 -12
  39. package/src/client/components/post-form-types.ts +11 -4
  40. package/src/client/compose-bridge.ts +410 -109
  41. package/src/client/image-processor.ts +26 -8
  42. package/src/client/media-metadata.ts +247 -0
  43. package/src/client/multipart-upload.ts +160 -0
  44. package/src/client/post-form-bridge.ts +52 -1
  45. package/src/client/settings-bridge.ts +0 -12
  46. package/src/client/thread-context.ts +140 -0
  47. package/src/client/tiptap/create-editor.ts +46 -0
  48. package/src/client/tiptap/extensions.ts +5 -0
  49. package/src/client/tiptap/image-node.ts +2 -8
  50. package/src/client/tiptap/paste-image.ts +2 -13
  51. package/src/client/tiptap/slash-commands.ts +173 -63
  52. package/src/client/toast.ts +101 -3
  53. package/src/client/types/sortablejs.d.ts +15 -0
  54. package/src/client/upload-with-metadata.ts +54 -0
  55. package/src/client/video-processor.ts +207 -0
  56. package/src/client.ts +5 -2
  57. package/src/db/__tests__/migrations.test.ts +118 -0
  58. package/src/db/index.ts +52 -0
  59. package/src/db/migrations/0000_baseline.sql +269 -0
  60. package/src/db/migrations/0001_fts_setup.sql +31 -0
  61. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  62. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  63. package/src/db/migrations/meta/_journal.json +4 -39
  64. package/src/db/schema.ts +409 -145
  65. package/src/i18n/__tests__/detect.test.ts +115 -0
  66. package/src/i18n/context.tsx +2 -2
  67. package/src/i18n/detect.ts +85 -1
  68. package/src/i18n/i18n.ts +1 -1
  69. package/src/i18n/index.ts +2 -1
  70. package/src/i18n/locales/en.po +487 -1217
  71. package/src/i18n/locales/en.ts +1 -1
  72. package/src/i18n/locales/zh-Hans.po +613 -996
  73. package/src/i18n/locales/zh-Hans.ts +1 -1
  74. package/src/i18n/locales/zh-Hant.po +624 -1007
  75. package/src/i18n/locales/zh-Hant.ts +1 -1
  76. package/src/i18n/middleware.ts +6 -0
  77. package/src/index.ts +5 -7
  78. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  79. package/src/lib/__tests__/constants.test.ts +0 -1
  80. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  81. package/src/lib/__tests__/nanoid.test.ts +26 -0
  82. package/src/lib/__tests__/schemas.test.ts +181 -63
  83. package/src/lib/__tests__/slug.test.ts +126 -0
  84. package/src/lib/__tests__/sse.test.ts +6 -6
  85. package/src/lib/__tests__/summary.test.ts +264 -0
  86. package/src/lib/__tests__/theme.test.ts +1 -1
  87. package/src/lib/__tests__/timeline.test.ts +33 -30
  88. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  89. package/src/lib/__tests__/view.test.ts +141 -66
  90. package/src/lib/blurhash-placeholder.ts +102 -0
  91. package/src/lib/constants.ts +3 -1
  92. package/src/lib/emoji-catalog.ts +885 -68
  93. package/src/lib/errors.ts +11 -8
  94. package/src/lib/feed.ts +78 -32
  95. package/src/lib/html.ts +2 -1
  96. package/src/lib/icon-catalog.ts +5033 -1
  97. package/src/lib/icons.ts +3 -2
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/markdown-to-tiptap.ts +286 -0
  100. package/src/lib/media-helpers.ts +12 -3
  101. package/src/lib/nanoid.ts +29 -0
  102. package/src/lib/navigation.ts +1 -1
  103. package/src/lib/render.tsx +20 -2
  104. package/src/lib/resolve-config.ts +6 -2
  105. package/src/lib/schemas.ts +224 -55
  106. package/src/lib/search-snippet.ts +34 -0
  107. package/src/lib/slug.ts +96 -0
  108. package/src/lib/sse.ts +6 -6
  109. package/src/lib/storage.ts +115 -7
  110. package/src/lib/summary.ts +66 -0
  111. package/src/lib/theme.ts +11 -8
  112. package/src/lib/timeline.ts +74 -34
  113. package/src/lib/tiptap-render.ts +5 -10
  114. package/src/lib/tiptap-to-markdown.ts +305 -0
  115. package/src/lib/upload.ts +190 -29
  116. package/src/lib/url.ts +31 -0
  117. package/src/lib/view.ts +204 -37
  118. package/src/middleware/__tests__/auth.test.ts +191 -11
  119. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  120. package/src/middleware/auth.ts +63 -9
  121. package/src/middleware/onboarding.ts +1 -1
  122. package/src/middleware/secure-headers.ts +40 -0
  123. package/src/preset.css +45 -2
  124. package/src/routes/__tests__/compose.test.ts +17 -24
  125. package/src/routes/api/__tests__/collections.test.ts +109 -61
  126. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  127. package/src/routes/api/__tests__/posts.test.ts +132 -68
  128. package/src/routes/api/__tests__/search.test.ts +15 -2
  129. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  130. package/src/routes/api/collections.ts +51 -42
  131. package/src/routes/api/custom-urls.ts +80 -0
  132. package/src/routes/api/export.ts +31 -0
  133. package/src/routes/api/nav-items.ts +23 -19
  134. package/src/routes/api/posts.ts +43 -39
  135. package/src/routes/api/search.ts +3 -4
  136. package/src/routes/api/upload-multipart.ts +245 -0
  137. package/src/routes/api/upload.ts +85 -19
  138. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  139. package/src/routes/auth/setup.tsx +26 -33
  140. package/src/routes/auth/signin.tsx +3 -7
  141. package/src/routes/compose.tsx +10 -55
  142. package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
  143. package/src/routes/dash/custom-urls.tsx +414 -0
  144. package/src/routes/dash/settings.tsx +304 -232
  145. package/src/routes/feed/__tests__/rss.test.ts +27 -28
  146. package/src/routes/feed/rss.ts +6 -4
  147. package/src/routes/feed/sitemap.ts +2 -12
  148. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  149. package/src/routes/pages/__tests__/featured.test.ts +41 -22
  150. package/src/routes/pages/archive.tsx +175 -39
  151. package/src/routes/pages/collection.tsx +22 -10
  152. package/src/routes/pages/collections.tsx +3 -3
  153. package/src/routes/pages/featured.tsx +28 -4
  154. package/src/routes/pages/home.tsx +16 -15
  155. package/src/routes/pages/latest.tsx +1 -11
  156. package/src/routes/pages/new.tsx +39 -0
  157. package/src/routes/pages/page.tsx +95 -49
  158. package/src/routes/pages/search.tsx +1 -1
  159. package/src/services/__tests__/api-token.test.ts +135 -0
  160. package/src/services/__tests__/collection.test.ts +275 -227
  161. package/src/services/__tests__/custom-url.test.ts +213 -0
  162. package/src/services/__tests__/media.test.ts +162 -22
  163. package/src/services/__tests__/navigation.test.ts +109 -68
  164. package/src/services/__tests__/post-timeline.test.ts +205 -32
  165. package/src/services/__tests__/post.test.ts +713 -234
  166. package/src/services/__tests__/search.test.ts +67 -10
  167. package/src/services/api-token.ts +166 -0
  168. package/src/services/auth.ts +17 -2
  169. package/src/services/collection.ts +397 -131
  170. package/src/services/custom-url.ts +188 -0
  171. package/src/services/export.ts +802 -0
  172. package/src/services/index.ts +26 -19
  173. package/src/services/media.ts +100 -22
  174. package/src/services/navigation.ts +158 -47
  175. package/src/services/path.ts +339 -0
  176. package/src/services/post.ts +687 -154
  177. package/src/services/search.ts +160 -75
  178. package/src/styles/components.css +58 -7
  179. package/src/styles/tokens.css +84 -6
  180. package/src/styles/ui.css +2964 -457
  181. package/src/types/bindings.ts +4 -1
  182. package/src/types/config.ts +12 -4
  183. package/src/types/constants.ts +15 -3
  184. package/src/types/entities.ts +74 -35
  185. package/src/types/operations.ts +11 -24
  186. package/src/types/props.ts +51 -16
  187. package/src/types/views.ts +45 -22
  188. package/src/ui/color-themes.ts +133 -23
  189. package/src/ui/compose/ComposeDialog.tsx +239 -17
  190. package/src/ui/compose/ComposePrompt.tsx +1 -1
  191. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  192. package/src/ui/dash/ListItemRow.tsx +1 -1
  193. package/src/ui/dash/StatusBadge.tsx +3 -1
  194. package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
  195. package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
  196. package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
  197. package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
  198. package/src/ui/dash/index.ts +0 -3
  199. package/src/ui/dash/settings/AccountContent.tsx +3 -57
  200. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  201. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  202. package/src/ui/dash/settings/AvatarContent.tsx +8 -0
  203. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  204. package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
  205. package/src/ui/feed/LinkCard.tsx +89 -40
  206. package/src/ui/feed/NoteCard.tsx +39 -25
  207. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  208. package/src/ui/feed/QuoteCard.tsx +38 -23
  209. package/src/ui/feed/ThreadPreview.tsx +90 -26
  210. package/src/ui/feed/TimelineFeed.tsx +3 -2
  211. package/src/ui/feed/TimelineItem.tsx +15 -6
  212. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  213. package/src/ui/feed/thread-preview-state.ts +61 -0
  214. package/src/ui/font-themes.ts +2 -2
  215. package/src/ui/layouts/BaseLayout.tsx +2 -2
  216. package/src/ui/layouts/SiteLayout.tsx +105 -92
  217. package/src/ui/pages/ArchivePage.tsx +923 -98
  218. package/src/ui/pages/ComposePage.tsx +54 -0
  219. package/src/ui/pages/PostPage.tsx +30 -45
  220. package/src/ui/pages/SearchPage.tsx +181 -37
  221. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  222. package/src/ui/shared/CollectionsSidebar.tsx +47 -37
  223. package/src/ui/shared/MediaGallery.tsx +445 -149
  224. package/src/ui/shared/PostFooter.tsx +204 -0
  225. package/src/ui/shared/StarRating.tsx +27 -0
  226. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  227. package/src/ui/shared/index.ts +0 -1
  228. package/dist/client/assets/url-8Dj-5CLW.js +0 -1
  229. package/src/client/media-upload.ts +0 -161
  230. package/src/client/page-slug-bridge.ts +0 -42
  231. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  232. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  233. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  234. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  235. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  236. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  237. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  238. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  239. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  240. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  241. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  242. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  243. package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
  244. package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
  245. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  246. package/src/lib/__tests__/sqid.test.ts +0 -65
  247. package/src/lib/sqid.ts +0 -79
  248. package/src/routes/api/__tests__/pages.test.ts +0 -218
  249. package/src/routes/api/pages.ts +0 -73
  250. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  251. package/src/routes/dash/index.tsx +0 -109
  252. package/src/routes/dash/media.tsx +0 -135
  253. package/src/routes/dash/pages.tsx +0 -245
  254. package/src/routes/dash/posts.tsx +0 -338
  255. package/src/routes/dash/redirects.tsx +0 -263
  256. package/src/routes/pages/post.tsx +0 -59
  257. package/src/services/__tests__/page.test.ts +0 -298
  258. package/src/services/__tests__/path-registry.test.ts +0 -165
  259. package/src/services/__tests__/redirect.test.ts +0 -159
  260. package/src/services/page.ts +0 -216
  261. package/src/services/path-registry.ts +0 -160
  262. package/src/services/redirect.ts +0 -97
  263. package/src/ui/dash/PageForm.tsx +0 -187
  264. package/src/ui/dash/PostList.tsx +0 -95
  265. package/src/ui/dash/media/MediaListContent.tsx +0 -206
  266. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  267. package/src/ui/dash/pages/PagesContent.tsx +0 -75
  268. package/src/ui/dash/posts/PostForm.tsx +0 -260
  269. package/src/ui/layouts/DashLayout.tsx +0 -247
  270. package/src/ui/pages/SinglePage.tsx +0 -23
  271. package/src/ui/shared/ThreadView.tsx +0 -136
@@ -1,12 +1,19 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { createTestApp } from "../../../__tests__/helpers/app.js";
3
3
  import { postsApiRoutes } from "../posts.js";
4
- import * as sqid from "../../../lib/sqid.js";
5
4
 
6
5
  describe("Posts API Routes", () => {
7
6
  describe("GET /api/posts", () => {
7
+ it("returns 401 when not authenticated", async () => {
8
+ const { app } = createTestApp({ authenticated: false });
9
+ app.route("/api/posts", postsApiRoutes);
10
+
11
+ const res = await app.request("/api/posts");
12
+ expect(res.status).toBe(401);
13
+ });
14
+
8
15
  it("returns empty list when no posts exist", async () => {
9
- const { app } = createTestApp();
16
+ const { app } = createTestApp({ authenticated: true });
10
17
  app.route("/api/posts", postsApiRoutes);
11
18
 
12
19
  const res = await app.request("/api/posts");
@@ -17,30 +24,30 @@ describe("Posts API Routes", () => {
17
24
  expect(body.nextCursor).toBeNull();
18
25
  });
19
26
 
20
- it("returns posts with sqids", async () => {
21
- const { app, services } = createTestApp();
27
+ it("returns posts with IDs", async () => {
28
+ const { app, services } = createTestApp({ authenticated: true });
22
29
  app.route("/api/posts", postsApiRoutes);
23
30
 
24
31
  await services.posts.create({
25
32
  format: "note",
26
- body: "Hello world",
33
+ bodyMarkdown: "Hello world",
27
34
  });
28
35
 
29
36
  const res = await app.request("/api/posts");
30
37
  const body = await res.json();
31
38
 
32
39
  expect(body.posts).toHaveLength(1);
33
- expect(body.posts[0].body).toBe("Hello world");
34
- expect(body.posts[0].sqid).toBeTruthy();
40
+ expect(body.posts[0].bodyText).toBe("Hello world");
41
+ expect(body.posts[0].id).toBeTruthy();
35
42
  });
36
43
 
37
44
  it("includes mediaAttachments in list response", async () => {
38
- const { app, services } = createTestApp();
45
+ const { app, services } = createTestApp({ authenticated: true });
39
46
  app.route("/api/posts", postsApiRoutes);
40
47
 
41
48
  const post = await services.posts.create({
42
49
  format: "note",
43
- body: "with media",
50
+ bodyMarkdown: "with media",
44
51
  });
45
52
 
46
53
  const media = await services.media.create({
@@ -63,20 +70,20 @@ describe("Posts API Routes", () => {
63
70
  expect(body.posts[0].mediaAttachments[0].mimeType).toBe("image/jpeg");
64
71
  expect(body.posts[0].mediaAttachments[0].url).toBeTruthy();
65
72
  expect(body.posts[0].mediaAttachments[0].previewUrl).toBeTruthy();
66
- expect(body.posts[0].mediaAttachments[0].position).toBe(0);
73
+ expect(body.posts[0].mediaAttachments[0].position).toBe("a0");
67
74
  });
68
75
 
69
76
  it("filters by status", async () => {
70
- const { app, services } = createTestApp();
77
+ const { app, services } = createTestApp({ authenticated: true });
71
78
  app.route("/api/posts", postsApiRoutes);
72
79
 
73
80
  await services.posts.create({
74
81
  format: "note",
75
- body: "published post",
82
+ bodyMarkdown: "published post",
76
83
  });
77
84
  await services.posts.create({
78
85
  format: "note",
79
- body: "draft post",
86
+ bodyMarkdown: "draft post",
80
87
  status: "draft",
81
88
  });
82
89
 
@@ -88,13 +95,13 @@ describe("Posts API Routes", () => {
88
95
  });
89
96
 
90
97
  it("supports limit parameter", async () => {
91
- const { app, services } = createTestApp();
98
+ const { app, services } = createTestApp({ authenticated: true });
92
99
  app.route("/api/posts", postsApiRoutes);
93
100
 
94
101
  for (let i = 0; i < 5; i++) {
95
102
  await services.posts.create({
96
103
  format: "note",
97
- body: `post ${i}`,
104
+ bodyMarkdown: `post ${i}`,
98
105
  });
99
106
  }
100
107
 
@@ -107,31 +114,41 @@ describe("Posts API Routes", () => {
107
114
  });
108
115
 
109
116
  describe("GET /api/posts/:id", () => {
110
- it("returns a post by sqid", async () => {
111
- const { app, services } = createTestApp();
117
+ it("returns 401 when not authenticated", async () => {
118
+ const { app, services } = createTestApp({ authenticated: false });
112
119
  app.route("/api/posts", postsApiRoutes);
113
120
 
114
121
  const post = await services.posts.create({
115
122
  format: "note",
116
- body: "test post",
123
+ bodyMarkdown: "test post",
117
124
  });
118
- const id = sqid.encode(post.id);
125
+ const res = await app.request(`/api/posts/${post.id}`);
126
+ expect(res.status).toBe(401);
127
+ });
128
+
129
+ it("returns a post by ID", async () => {
130
+ const { app, services } = createTestApp({ authenticated: true });
131
+ app.route("/api/posts", postsApiRoutes);
119
132
 
120
- const res = await app.request(`/api/posts/${id}`);
133
+ const post = await services.posts.create({
134
+ format: "note",
135
+ bodyMarkdown: "test post",
136
+ });
137
+ const res = await app.request(`/api/posts/${post.id}`);
121
138
  expect(res.status).toBe(200);
122
139
 
123
140
  const body = await res.json();
124
- expect(body.body).toBe("test post");
125
- expect(body.sqid).toBe(id);
141
+ expect(body.bodyText).toBe("test post");
142
+ expect(body.id).toBe(post.id);
126
143
  });
127
144
 
128
145
  it("includes mediaAttachments in single post response", async () => {
129
- const { app, services } = createTestApp();
146
+ const { app, services } = createTestApp({ authenticated: true });
130
147
  app.route("/api/posts", postsApiRoutes);
131
148
 
132
149
  const post = await services.posts.create({
133
150
  format: "note",
134
- body: "with media",
151
+ bodyMarkdown: "with media",
135
152
  });
136
153
 
137
154
  const media = await services.media.create({
@@ -144,15 +161,15 @@ describe("Posts API Routes", () => {
144
161
 
145
162
  await services.media.attachToPost(post.id, [media.id]);
146
163
 
147
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`);
164
+ const res = await app.request(`/api/posts/${post.id}`);
148
165
  const body = await res.json();
149
166
 
150
167
  expect(body.mediaAttachments).toHaveLength(1);
151
168
  expect(body.mediaAttachments[0].id).toBe(media.id);
152
169
  });
153
170
 
154
- it("returns 400 for invalid sqid", async () => {
155
- const { app } = createTestApp();
171
+ it("returns 400 for invalid ID", async () => {
172
+ const { app } = createTestApp({ authenticated: true });
156
173
  app.route("/api/posts", postsApiRoutes);
157
174
 
158
175
  const res = await app.request("/api/posts/!!invalid!!");
@@ -160,10 +177,12 @@ describe("Posts API Routes", () => {
160
177
  });
161
178
 
162
179
  it("returns 404 for non-existent post", async () => {
163
- const { app } = createTestApp();
180
+ const { app } = createTestApp({ authenticated: true });
164
181
  app.route("/api/posts", postsApiRoutes);
165
182
 
166
- const res = await app.request(`/api/posts/${sqid.encode(9999)}`);
183
+ const res = await app.request(
184
+ "/api/posts/00000000-0000-0000-0000-000000009999",
185
+ );
167
186
  expect(res.status).toBe(404);
168
187
  });
169
188
  });
@@ -178,7 +197,7 @@ describe("Posts API Routes", () => {
178
197
  headers: { "Content-Type": "application/json" },
179
198
  body: JSON.stringify({
180
199
  format: "note",
181
- body: "test",
200
+ bodyMarkdown: "test",
182
201
  }),
183
202
  });
184
203
 
@@ -194,18 +213,57 @@ describe("Posts API Routes", () => {
194
213
  headers: { "Content-Type": "application/json" },
195
214
  body: JSON.stringify({
196
215
  format: "note",
197
- body: "Hello from API",
216
+ bodyMarkdown: "Hello from API",
198
217
  }),
199
218
  });
200
219
 
201
220
  expect(res.status).toBe(201);
202
221
 
203
222
  const body = await res.json();
204
- expect(body.body).toBe("Hello from API");
205
- expect(body.sqid).toBeTruthy();
223
+ expect(body.bodyText).toBe("Hello from API");
224
+ expect(body.id).toBeTruthy();
206
225
  expect(body.mediaAttachments).toEqual([]);
207
226
  });
208
227
 
228
+ it("creates a post with bodyMarkdown", async () => {
229
+ const { app } = createTestApp({ authenticated: true });
230
+ app.route("/api/posts", postsApiRoutes);
231
+
232
+ const res = await app.request("/api/posts", {
233
+ method: "POST",
234
+ headers: { "Content-Type": "application/json" },
235
+ body: JSON.stringify({
236
+ format: "note",
237
+ bodyMarkdown: "Hello **bold** world",
238
+ }),
239
+ });
240
+
241
+ expect(res.status).toBe(201);
242
+
243
+ const body = await res.json();
244
+ expect(body.bodyText).toContain("Hello");
245
+ expect(body.bodyHtml).toContain("<strong>bold</strong>");
246
+ });
247
+
248
+ it("returns 400 when both body and bodyMarkdown are provided", async () => {
249
+ const { app } = createTestApp({ authenticated: true });
250
+ app.route("/api/posts", postsApiRoutes);
251
+
252
+ const res = await app.request("/api/posts", {
253
+ method: "POST",
254
+ headers: { "Content-Type": "application/json" },
255
+ body: JSON.stringify({
256
+ format: "note",
257
+ body: '{"type":"doc","content":[]}',
258
+ bodyMarkdown: "Hello",
259
+ }),
260
+ });
261
+
262
+ expect(res.status).toBe(400);
263
+ const body = await res.json();
264
+ expect(body.error).toContain("Provide either body or bodyMarkdown");
265
+ });
266
+
209
267
  it("creates a post with mediaIds and attaches them", async () => {
210
268
  const { app, services } = createTestApp({ authenticated: true });
211
269
  app.route("/api/posts", postsApiRoutes);
@@ -230,7 +288,7 @@ describe("Posts API Routes", () => {
230
288
  headers: { "Content-Type": "application/json" },
231
289
  body: JSON.stringify({
232
290
  format: "note",
233
- body: "with images",
291
+ bodyMarkdown: "with images",
234
292
  mediaIds: [m1.id, m2.id],
235
293
  }),
236
294
  });
@@ -239,9 +297,9 @@ describe("Posts API Routes", () => {
239
297
  const body = await res.json();
240
298
  expect(body.mediaAttachments).toHaveLength(2);
241
299
  expect(body.mediaAttachments[0].id).toBe(m1.id);
242
- expect(body.mediaAttachments[0].position).toBe(0);
300
+ expect(body.mediaAttachments[0].position).toBe("a0");
243
301
  expect(body.mediaAttachments[1].id).toBe(m2.id);
244
- expect(body.mediaAttachments[1].position).toBe(1);
302
+ expect(body.mediaAttachments[1].position).toBe("a1");
245
303
  });
246
304
 
247
305
  it("returns 400 for invalid media IDs", async () => {
@@ -253,7 +311,7 @@ describe("Posts API Routes", () => {
253
311
  headers: { "Content-Type": "application/json" },
254
312
  body: JSON.stringify({
255
313
  format: "note",
256
- body: "test",
314
+ bodyMarkdown: "test",
257
315
  mediaIds: ["nonexistent-id"],
258
316
  }),
259
317
  });
@@ -300,13 +358,13 @@ describe("Posts API Routes", () => {
300
358
 
301
359
  const post = await services.posts.create({
302
360
  format: "note",
303
- body: "original",
361
+ bodyMarkdown: "original",
304
362
  });
305
363
 
306
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
364
+ const res = await app.request(`/api/posts/${post.id}`, {
307
365
  method: "PUT",
308
366
  headers: { "Content-Type": "application/json" },
309
- body: JSON.stringify({ body: "updated" }),
367
+ body: JSON.stringify({ bodyMarkdown: "updated" }),
310
368
  });
311
369
 
312
370
  expect(res.status).toBe(401);
@@ -318,18 +376,18 @@ describe("Posts API Routes", () => {
318
376
 
319
377
  const post = await services.posts.create({
320
378
  format: "note",
321
- body: "original",
379
+ bodyMarkdown: "original",
322
380
  });
323
381
 
324
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
382
+ const res = await app.request(`/api/posts/${post.id}`, {
325
383
  method: "PUT",
326
384
  headers: { "Content-Type": "application/json" },
327
- body: JSON.stringify({ body: "updated" }),
385
+ body: JSON.stringify({ bodyMarkdown: "updated" }),
328
386
  });
329
387
 
330
388
  expect(res.status).toBe(200);
331
389
  const body = await res.json();
332
- expect(body.body).toBe("updated");
390
+ expect(body.bodyText).toBe("updated");
333
391
  expect(body.mediaAttachments).toEqual([]);
334
392
  });
335
393
 
@@ -339,7 +397,7 @@ describe("Posts API Routes", () => {
339
397
 
340
398
  const post = await services.posts.create({
341
399
  format: "note",
342
- body: "test",
400
+ bodyMarkdown: "test",
343
401
  });
344
402
 
345
403
  const m1 = await services.media.create({
@@ -360,7 +418,7 @@ describe("Posts API Routes", () => {
360
418
  storageKey: "media/2025/01/b.jpg",
361
419
  });
362
420
 
363
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
421
+ const res = await app.request(`/api/posts/${post.id}`, {
364
422
  method: "PUT",
365
423
  headers: { "Content-Type": "application/json" },
366
424
  body: JSON.stringify({ mediaIds: [m2.id] }),
@@ -378,7 +436,7 @@ describe("Posts API Routes", () => {
378
436
 
379
437
  const post = await services.posts.create({
380
438
  format: "note",
381
- body: "test",
439
+ bodyMarkdown: "test",
382
440
  });
383
441
 
384
442
  const m1 = await services.media.create({
@@ -391,10 +449,10 @@ describe("Posts API Routes", () => {
391
449
 
392
450
  await services.media.attachToPost(post.id, [m1.id]);
393
451
 
394
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
452
+ const res = await app.request(`/api/posts/${post.id}`, {
395
453
  method: "PUT",
396
454
  headers: { "Content-Type": "application/json" },
397
- body: JSON.stringify({ body: "updated content" }),
455
+ body: JSON.stringify({ bodyMarkdown: "updated content" }),
398
456
  });
399
457
 
400
458
  expect(res.status).toBe(200);
@@ -407,11 +465,14 @@ describe("Posts API Routes", () => {
407
465
  const { app } = createTestApp({ authenticated: true });
408
466
  app.route("/api/posts", postsApiRoutes);
409
467
 
410
- const res = await app.request(`/api/posts/${sqid.encode(9999)}`, {
411
- method: "PUT",
412
- headers: { "Content-Type": "application/json" },
413
- body: JSON.stringify({ body: "test" }),
414
- });
468
+ const res = await app.request(
469
+ "/api/posts/00000000-0000-0000-0000-000000009999",
470
+ {
471
+ method: "PUT",
472
+ headers: { "Content-Type": "application/json" },
473
+ body: JSON.stringify({ bodyMarkdown: "test" }),
474
+ },
475
+ );
415
476
 
416
477
  expect(res.status).toBe(404);
417
478
  });
@@ -422,10 +483,10 @@ describe("Posts API Routes", () => {
422
483
 
423
484
  const post = await services.posts.create({
424
485
  format: "note",
425
- body: "test",
486
+ bodyMarkdown: "test",
426
487
  });
427
488
 
428
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
489
+ const res = await app.request(`/api/posts/${post.id}`, {
429
490
  method: "PUT",
430
491
  headers: { "Content-Type": "application/json" },
431
492
  body: JSON.stringify({ format: "invalid-type" }),
@@ -442,10 +503,10 @@ describe("Posts API Routes", () => {
442
503
 
443
504
  const post = await services.posts.create({
444
505
  format: "note",
445
- body: "test",
506
+ bodyMarkdown: "test",
446
507
  });
447
508
 
448
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
509
+ const res = await app.request(`/api/posts/${post.id}`, {
449
510
  method: "DELETE",
450
511
  });
451
512
 
@@ -458,10 +519,10 @@ describe("Posts API Routes", () => {
458
519
 
459
520
  const post = await services.posts.create({
460
521
  format: "note",
461
- body: "to be deleted",
522
+ bodyMarkdown: "to be deleted",
462
523
  });
463
524
 
464
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
525
+ const res = await app.request(`/api/posts/${post.id}`, {
465
526
  method: "DELETE",
466
527
  });
467
528
 
@@ -478,9 +539,12 @@ describe("Posts API Routes", () => {
478
539
  const { app } = createTestApp({ authenticated: true });
479
540
  app.route("/api/posts", postsApiRoutes);
480
541
 
481
- const res = await app.request(`/api/posts/${sqid.encode(9999)}`, {
482
- method: "DELETE",
483
- });
542
+ const res = await app.request(
543
+ "/api/posts/00000000-0000-0000-0000-000000009999",
544
+ {
545
+ method: "DELETE",
546
+ },
547
+ );
484
548
 
485
549
  expect(res.status).toBe(404);
486
550
  });
@@ -491,7 +555,7 @@ describe("Posts API Routes", () => {
491
555
 
492
556
  const post = await services.posts.create({
493
557
  format: "note",
494
- body: "with media",
558
+ bodyMarkdown: "with media",
495
559
  });
496
560
 
497
561
  const m1 = await services.media.create({
@@ -511,7 +575,7 @@ describe("Posts API Routes", () => {
511
575
 
512
576
  await services.media.attachToPost(post.id, [m1.id, m2.id]);
513
577
 
514
- const res = await app.request(`/api/posts/${sqid.encode(post.id)}`, {
578
+ const res = await app.request(`/api/posts/${post.id}`, {
515
579
  method: "DELETE",
516
580
  });
517
581
 
@@ -528,11 +592,11 @@ describe("Posts API Routes", () => {
528
592
 
529
593
  const root = await services.posts.create({
530
594
  format: "note",
531
- body: "thread root",
595
+ bodyMarkdown: "thread root",
532
596
  });
533
597
  const reply = await services.posts.create({
534
598
  format: "note",
535
- body: "reply",
599
+ bodyMarkdown: "reply",
536
600
  replyToId: root.id,
537
601
  });
538
602
 
@@ -554,7 +618,7 @@ describe("Posts API Routes", () => {
554
618
  await services.media.attachToPost(root.id, [rootMedia.id]);
555
619
  await services.media.attachToPost(reply.id, [replyMedia.id]);
556
620
 
557
- const res = await app.request(`/api/posts/${sqid.encode(root.id)}`, {
621
+ const res = await app.request(`/api/posts/${root.id}`, {
558
622
  method: "DELETE",
559
623
  });
560
624
 
@@ -2,6 +2,19 @@ import { describe, it, expect } from "vitest";
2
2
  import { createTestApp } from "../../../__tests__/helpers/app.js";
3
3
  import { searchApiRoutes } from "../search.js";
4
4
 
5
+ /** Wraps plain text in a minimal valid TipTap JSON document. */
6
+ function tiptapDoc(text: string): string {
7
+ return JSON.stringify({
8
+ type: "doc",
9
+ content: [
10
+ {
11
+ type: "paragraph",
12
+ content: [{ type: "text", text }],
13
+ },
14
+ ],
15
+ });
16
+ }
17
+
5
18
  describe("Search API Routes", () => {
6
19
  it("returns 400 when query is missing", async () => {
7
20
  const { app } = createTestApp({ fts: true });
@@ -40,7 +53,7 @@ describe("Search API Routes", () => {
40
53
 
41
54
  await services.posts.create({
42
55
  format: "note",
43
- body: "Testing search functionality in jant",
56
+ body: tiptapDoc("Testing search functionality in jant"),
44
57
  });
45
58
 
46
59
  const res = await app.request("/api/search?q=jant");
@@ -50,7 +63,7 @@ describe("Search API Routes", () => {
50
63
  expect(body.query).toBe("jant");
51
64
  expect(body.results.length).toBeGreaterThanOrEqual(1);
52
65
  expect(body.count).toBeGreaterThanOrEqual(1);
53
- expect(body.results[0].url).toMatch(/^\/p\//);
66
+ expect(body.results[0].url).toMatch(/^\/[a-z0-9]/);
54
67
  });
55
68
 
56
69
  it("returns empty results for non-matching query", async () => {