@jant/core 0.3.36 → 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 (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,298 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { createTestDatabase } from "../../__tests__/helpers/db.js";
3
- import { createPageService } from "../page.js";
4
- import { createPostService } from "../post.js";
5
- import { createNavItemService } from "../navigation.js";
6
- import { createPathRegistryService } from "../path-registry.js";
7
- import { ValidationError, ConflictError } from "../../lib/errors.js";
8
- import type { Database } from "../../db/index.js";
9
-
10
- describe("PageService", () => {
11
- let db: Database;
12
- let pageService: ReturnType<typeof createPageService>;
13
- let postService: ReturnType<typeof createPostService>;
14
- let navItemService: ReturnType<typeof createNavItemService>;
15
- let pathRegistry: ReturnType<typeof createPathRegistryService>;
16
-
17
- beforeEach(() => {
18
- const testDb = createTestDatabase();
19
- db = testDb.db as unknown as Database;
20
- pathRegistry = createPathRegistryService(db);
21
- pageService = createPageService(db, pathRegistry);
22
- postService = createPostService(db, pathRegistry);
23
- navItemService = createNavItemService(db);
24
- });
25
-
26
- describe("listNotInNav", () => {
27
- it("returns all pages when none are in navigation", async () => {
28
- await pageService.create({ slug: "about", title: "About" });
29
- await pageService.create({ slug: "contact", title: "Contact" });
30
-
31
- const pages = await pageService.listNotInNav();
32
- expect(pages).toHaveLength(2);
33
- });
34
-
35
- it("excludes pages that have a nav item", async () => {
36
- const aboutPage = await pageService.create({
37
- slug: "about",
38
- title: "About",
39
- });
40
- await pageService.create({ slug: "contact", title: "Contact" });
41
-
42
- // Add "About" to navigation
43
- await navItemService.create({
44
- type: "page",
45
- label: "About",
46
- url: "/about",
47
- pageId: aboutPage.id,
48
- });
49
-
50
- const pages = await pageService.listNotInNav();
51
- expect(pages).toHaveLength(1);
52
- expect(pages[0]?.slug).toBe("contact");
53
- });
54
-
55
- it("returns empty array when all pages are in navigation", async () => {
56
- const aboutPage = await pageService.create({
57
- slug: "about",
58
- title: "About",
59
- });
60
-
61
- await navItemService.create({
62
- type: "page",
63
- label: "About",
64
- url: "/about",
65
- pageId: aboutPage.id,
66
- });
67
-
68
- const pages = await pageService.listNotInNav();
69
- expect(pages).toHaveLength(0);
70
- });
71
-
72
- it("returns empty array when no pages exist", async () => {
73
- const pages = await pageService.listNotInNav();
74
- expect(pages).toHaveLength(0);
75
- });
76
-
77
- it("is not affected by link-type nav items (no pageId)", async () => {
78
- await pageService.create({ slug: "about", title: "About" });
79
-
80
- // Link-type nav items have no pageId
81
- await navItemService.create({
82
- type: "link",
83
- label: "External",
84
- url: "https://example.com",
85
- });
86
-
87
- const pages = await pageService.listNotInNav();
88
- expect(pages).toHaveLength(1);
89
- });
90
-
91
- it("returns multiple pages correctly", async () => {
92
- await pageService.create({ slug: "first", title: "First" });
93
- await pageService.create({ slug: "second", title: "Second" });
94
- await pageService.create({ slug: "third", title: "Third" });
95
-
96
- // Add one to nav
97
- const pages = await pageService.list();
98
- await navItemService.create({
99
- type: "page",
100
- label: "Second",
101
- url: "/second",
102
- pageId: (
103
- pages.find((p) => p.slug === "second") as (typeof pages)[number]
104
- ).id,
105
- });
106
-
107
- const notInNav = await pageService.listNotInNav();
108
- expect(notInNav).toHaveLength(2);
109
- const slugs = notInNav.map((p) => p.slug);
110
- expect(slugs).toContain("first");
111
- expect(slugs).toContain("third");
112
- expect(slugs).not.toContain("second");
113
- });
114
- });
115
-
116
- describe("update nav item sync", () => {
117
- it("syncs nav item label when page title changes", async () => {
118
- const page = await pageService.create({
119
- slug: "about",
120
- title: "About",
121
- });
122
- await navItemService.create({
123
- type: "page",
124
- label: "About",
125
- url: "/about",
126
- pageId: page.id,
127
- });
128
-
129
- await pageService.update(page.id, { title: "About Us" });
130
-
131
- const navs = await navItemService.list();
132
- expect(navs).toHaveLength(1);
133
- expect(navs[0]?.label).toBe("About Us");
134
- });
135
-
136
- it("syncs nav item url when page slug changes", async () => {
137
- const page = await pageService.create({
138
- slug: "about",
139
- title: "About",
140
- });
141
- await navItemService.create({
142
- type: "page",
143
- label: "About",
144
- url: "/about",
145
- pageId: page.id,
146
- });
147
-
148
- await pageService.update(page.id, { slug: "about-us" });
149
-
150
- const navs = await navItemService.list();
151
- expect(navs).toHaveLength(1);
152
- expect(navs[0]?.url).toBe("/about-us");
153
- });
154
-
155
- it("syncs both label and url when title and slug change together", async () => {
156
- const page = await pageService.create({
157
- slug: "about",
158
- title: "About",
159
- });
160
- await navItemService.create({
161
- type: "page",
162
- label: "About",
163
- url: "/about",
164
- pageId: page.id,
165
- });
166
-
167
- await pageService.update(page.id, {
168
- title: "About Our Company",
169
- slug: "about-our-company",
170
- });
171
-
172
- const navs = await navItemService.list();
173
- expect(navs).toHaveLength(1);
174
- expect(navs[0]?.label).toBe("About Our Company");
175
- expect(navs[0]?.url).toBe("/about-our-company");
176
- });
177
-
178
- it("does not change nav item label when title is unchanged", async () => {
179
- const page = await pageService.create({
180
- slug: "about",
181
- title: "About",
182
- });
183
- await navItemService.create({
184
- type: "page",
185
- label: "Custom Label",
186
- url: "/about",
187
- pageId: page.id,
188
- });
189
-
190
- // Update body only, not title
191
- await pageService.update(page.id, { body: "New content" });
192
-
193
- const navs = await navItemService.list();
194
- expect(navs[0]?.label).toBe("Custom Label");
195
- });
196
-
197
- it("does not affect nav items for other pages", async () => {
198
- const page1 = await pageService.create({
199
- slug: "about",
200
- title: "About",
201
- });
202
- const page2 = await pageService.create({
203
- slug: "contact",
204
- title: "Contact",
205
- });
206
- await navItemService.create({
207
- type: "page",
208
- label: "About",
209
- url: "/about",
210
- pageId: page1.id,
211
- });
212
- await navItemService.create({
213
- type: "page",
214
- label: "Contact",
215
- url: "/contact",
216
- pageId: page2.id,
217
- });
218
-
219
- await pageService.update(page1.id, { title: "About Us" });
220
-
221
- const navs = await navItemService.list();
222
- const aboutNav = navs.find((n) => n.pageId === page1.id);
223
- const contactNav = navs.find((n) => n.pageId === page2.id);
224
- expect(aboutNav?.label).toBe("About Us");
225
- expect(contactNav?.label).toBe("Contact");
226
- });
227
- });
228
-
229
- describe("path registry integration", () => {
230
- it("rejects reserved slug on create", async () => {
231
- await expect(
232
- pageService.create({ slug: "dash", title: "Dashboard" }),
233
- ).rejects.toThrow(ValidationError);
234
- });
235
-
236
- it("rejects slug that conflicts with a post path", async () => {
237
- await postService.create({
238
- format: "note",
239
- body: "test",
240
- path: "about",
241
- });
242
-
243
- await expect(
244
- pageService.create({ slug: "about", title: "About" }),
245
- ).rejects.toThrow(ConflictError);
246
- });
247
-
248
- it("rejects reserved slug on update", async () => {
249
- const page = await pageService.create({
250
- slug: "about",
251
- title: "About",
252
- });
253
-
254
- await expect(
255
- pageService.update(page.id, { slug: "api" }),
256
- ).rejects.toThrow(ValidationError);
257
- });
258
-
259
- it("rejects slug update that conflicts with another entity", async () => {
260
- const page = await pageService.create({
261
- slug: "about",
262
- title: "About",
263
- });
264
- await postService.create({
265
- format: "note",
266
- body: "test",
267
- path: "contact",
268
- });
269
-
270
- await expect(
271
- pageService.update(page.id, { slug: "contact" }),
272
- ).rejects.toThrow(ConflictError);
273
- });
274
-
275
- it("releases path on delete", async () => {
276
- const page = await pageService.create({
277
- slug: "about",
278
- title: "About",
279
- });
280
-
281
- await pageService.delete(page.id);
282
-
283
- expect(await pathRegistry.isAvailable("about")).toBe(true);
284
- });
285
-
286
- it("releases old slug and claims new slug on update", async () => {
287
- const page = await pageService.create({
288
- slug: "about",
289
- title: "About",
290
- });
291
-
292
- await pageService.update(page.id, { slug: "about-us" });
293
-
294
- expect(await pathRegistry.isAvailable("about")).toBe(true);
295
- expect(await pathRegistry.isAvailable("about-us")).toBe(false);
296
- });
297
- });
298
- });
@@ -1,165 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { createTestDatabase } from "../../__tests__/helpers/db.js";
3
- import { createPathRegistryService } from "../path-registry.js";
4
- import { ValidationError, ConflictError } from "../../lib/errors.js";
5
- import type { Database } from "../../db/index.js";
6
-
7
- describe("PathRegistryService", () => {
8
- let db: Database;
9
- let pathRegistry: ReturnType<typeof createPathRegistryService>;
10
-
11
- beforeEach(() => {
12
- const testDb = createTestDatabase();
13
- db = testDb.db as unknown as Database;
14
- pathRegistry = createPathRegistryService(db);
15
- });
16
-
17
- describe("claim", () => {
18
- it("claims a path successfully", async () => {
19
- const entry = await pathRegistry.claim("about", "page", 1);
20
-
21
- expect(entry.path).toBe("about");
22
- expect(entry.ownerType).toBe("page");
23
- expect(entry.ownerId).toBe(1);
24
- expect(entry.createdAt).toBeGreaterThan(0);
25
- });
26
-
27
- it("normalizes the path before claiming", async () => {
28
- const entry = await pathRegistry.claim(" /About/ ", "page", 1);
29
- expect(entry.path).toBe("about");
30
- });
31
-
32
- it("rejects reserved paths", async () => {
33
- await expect(pathRegistry.claim("dash", "page", 1)).rejects.toThrow(
34
- ValidationError,
35
- );
36
- await expect(pathRegistry.claim("api", "page", 1)).rejects.toThrow(
37
- ValidationError,
38
- );
39
- await expect(pathRegistry.claim("search", "page", 1)).rejects.toThrow(
40
- ValidationError,
41
- );
42
- });
43
-
44
- it("rejects reserved paths regardless of casing", async () => {
45
- await expect(pathRegistry.claim("DASH", "page", 1)).rejects.toThrow(
46
- ValidationError,
47
- );
48
- });
49
-
50
- it("throws ConflictError when path is already claimed by another entity", async () => {
51
- await pathRegistry.claim("about", "page", 1);
52
-
53
- await expect(pathRegistry.claim("about", "post", 2)).rejects.toThrow(
54
- ConflictError,
55
- );
56
- });
57
-
58
- it("is idempotent for the same owner", async () => {
59
- const first = await pathRegistry.claim("about", "page", 1);
60
- const second = await pathRegistry.claim("about", "page", 1);
61
-
62
- expect(second.path).toBe(first.path);
63
- expect(second.ownerType).toBe(first.ownerType);
64
- expect(second.ownerId).toBe(first.ownerId);
65
- });
66
-
67
- it("allows multi-level paths", async () => {
68
- const entry = await pathRegistry.claim("2024/01/my-post", "post", 1);
69
- expect(entry.path).toBe("2024/01/my-post");
70
- });
71
- });
72
-
73
- describe("release", () => {
74
- it("releases a claimed path", async () => {
75
- await pathRegistry.claim("about", "page", 1);
76
- await pathRegistry.release("about");
77
-
78
- const entry = await pathRegistry.getByPath("about");
79
- expect(entry).toBeNull();
80
- });
81
-
82
- it("normalizes the path before releasing", async () => {
83
- await pathRegistry.claim("about", "page", 1);
84
- await pathRegistry.release(" /About/ ");
85
-
86
- const entry = await pathRegistry.getByPath("about");
87
- expect(entry).toBeNull();
88
- });
89
-
90
- it("is a no-op for unclaimed paths", async () => {
91
- // Should not throw
92
- await pathRegistry.release("nonexistent");
93
- });
94
- });
95
-
96
- describe("releaseByOwner", () => {
97
- it("releases all paths for a specific owner", async () => {
98
- await pathRegistry.claim("about", "page", 1);
99
- await pathRegistry.claim("contact", "page", 1);
100
- await pathRegistry.claim("blog", "page", 2);
101
-
102
- await pathRegistry.releaseByOwner("page", 1);
103
-
104
- expect(await pathRegistry.getByPath("about")).toBeNull();
105
- expect(await pathRegistry.getByPath("contact")).toBeNull();
106
- // Different owner's path should remain
107
- expect(await pathRegistry.getByPath("blog")).not.toBeNull();
108
- });
109
-
110
- it("does not affect other owner types", async () => {
111
- await pathRegistry.claim("about", "page", 1);
112
- await pathRegistry.claim("my-post", "post", 1);
113
-
114
- await pathRegistry.releaseByOwner("page", 1);
115
-
116
- expect(await pathRegistry.getByPath("about")).toBeNull();
117
- expect(await pathRegistry.getByPath("my-post")).not.toBeNull();
118
- });
119
- });
120
-
121
- describe("getByPath", () => {
122
- it("returns entry for claimed path", async () => {
123
- await pathRegistry.claim("about", "page", 1);
124
-
125
- const entry = await pathRegistry.getByPath("about");
126
- expect(entry).not.toBeNull();
127
- expect(entry?.ownerType).toBe("page");
128
- expect(entry?.ownerId).toBe(1);
129
- });
130
-
131
- it("normalizes the lookup path", async () => {
132
- await pathRegistry.claim("about", "page", 1);
133
-
134
- const entry = await pathRegistry.getByPath(" /About/ ");
135
- expect(entry).not.toBeNull();
136
- });
137
-
138
- it("returns null for unclaimed path", async () => {
139
- const entry = await pathRegistry.getByPath("nonexistent");
140
- expect(entry).toBeNull();
141
- });
142
- });
143
-
144
- describe("isAvailable", () => {
145
- it("returns true for unclaimed, non-reserved paths", async () => {
146
- expect(await pathRegistry.isAvailable("about")).toBe(true);
147
- });
148
-
149
- it("returns false for reserved paths", async () => {
150
- expect(await pathRegistry.isAvailable("dash")).toBe(false);
151
- expect(await pathRegistry.isAvailable("api")).toBe(false);
152
- });
153
-
154
- it("returns false for claimed paths", async () => {
155
- await pathRegistry.claim("about", "page", 1);
156
- expect(await pathRegistry.isAvailable("about")).toBe(false);
157
- });
158
-
159
- it("returns true after a path is released", async () => {
160
- await pathRegistry.claim("about", "page", 1);
161
- await pathRegistry.release("about");
162
- expect(await pathRegistry.isAvailable("about")).toBe(true);
163
- });
164
- });
165
- });
@@ -1,159 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { createTestDatabase } from "../../__tests__/helpers/db.js";
3
- import { createRedirectService } from "../redirect.js";
4
- import { createPageService } from "../page.js";
5
- import { createPostService } from "../post.js";
6
- import { createPathRegistryService } from "../path-registry.js";
7
- import { ConflictError } from "../../lib/errors.js";
8
- import type { Database } from "../../db/index.js";
9
-
10
- describe("RedirectService", () => {
11
- let db: Database;
12
- let redirectService: ReturnType<typeof createRedirectService>;
13
- let pageService: ReturnType<typeof createPageService>;
14
- let postService: ReturnType<typeof createPostService>;
15
- let pathRegistry: ReturnType<typeof createPathRegistryService>;
16
-
17
- beforeEach(() => {
18
- const testDb = createTestDatabase();
19
- db = testDb.db as unknown as Database;
20
- pathRegistry = createPathRegistryService(db);
21
- redirectService = createRedirectService(db, pathRegistry);
22
- pageService = createPageService(db, pathRegistry);
23
- postService = createPostService(db, pathRegistry);
24
- });
25
-
26
- describe("create", () => {
27
- it("creates a 301 redirect by default", async () => {
28
- const redirect = await redirectService.create("/old-path", "/new-path");
29
-
30
- expect(redirect.fromPath).toBe("old-path"); // normalizePath removes leading slash
31
- expect(redirect.toPath).toBe("/new-path");
32
- expect(redirect.type).toBe(301);
33
- expect(redirect.id).toBe(1);
34
- });
35
-
36
- it("creates a 302 redirect", async () => {
37
- const redirect = await redirectService.create(
38
- "/temp",
39
- "/destination",
40
- 302,
41
- );
42
-
43
- expect(redirect.type).toBe(302);
44
- });
45
-
46
- it("normalizes from path", async () => {
47
- const redirect = await redirectService.create(
48
- " /OLD-PATH/ ",
49
- "/new-path",
50
- );
51
-
52
- expect(redirect.fromPath).toBe("old-path");
53
- });
54
-
55
- it("replaces existing redirect for same from path", async () => {
56
- await redirectService.create("/old", "/first");
57
- const second = await redirectService.create("/old", "/second");
58
-
59
- expect(second.toPath).toBe("/second");
60
-
61
- const list = await redirectService.list();
62
- expect(list).toHaveLength(1);
63
- });
64
- });
65
-
66
- describe("getByPath", () => {
67
- it("finds redirect by from path", async () => {
68
- await redirectService.create("/old-page", "/new-page");
69
-
70
- const found = await redirectService.getByPath("/old-page");
71
- expect(found).not.toBeNull();
72
- expect(found?.toPath).toBe("/new-page");
73
- });
74
-
75
- it("normalizes the lookup path", async () => {
76
- await redirectService.create("/old-page", "/new-page");
77
-
78
- const found = await redirectService.getByPath(" /OLD-PAGE/ ");
79
- expect(found).not.toBeNull();
80
- });
81
-
82
- it("returns null for non-existent path", async () => {
83
- const found = await redirectService.getByPath("/nonexistent");
84
- expect(found).toBeNull();
85
- });
86
- });
87
-
88
- describe("delete", () => {
89
- it("deletes a redirect by ID", async () => {
90
- const redirect = await redirectService.create("/old", "/new");
91
- const result = await redirectService.delete(redirect.id);
92
-
93
- expect(result).toBe(true);
94
-
95
- const found = await redirectService.getByPath("/old");
96
- expect(found).toBeNull();
97
- });
98
-
99
- it("returns false for non-existent ID", async () => {
100
- const result = await redirectService.delete(9999);
101
- expect(result).toBe(false);
102
- });
103
- });
104
-
105
- describe("list", () => {
106
- it("returns empty array when no redirects exist", async () => {
107
- const redirects = await redirectService.list();
108
- expect(redirects).toEqual([]);
109
- });
110
-
111
- it("returns all redirects", async () => {
112
- await redirectService.create("/old-a", "/new-a");
113
- await redirectService.create("/old-b", "/new-b");
114
- await redirectService.create("/old-c", "/new-c");
115
-
116
- const redirects = await redirectService.list();
117
- expect(redirects).toHaveLength(3);
118
- });
119
- });
120
-
121
- describe("path registry integration", () => {
122
- it("rejects redirect that conflicts with a page", async () => {
123
- await pageService.create({ slug: "about", title: "About" });
124
-
125
- await expect(
126
- redirectService.create("/about", "/new-about"),
127
- ).rejects.toThrow(ConflictError);
128
- });
129
-
130
- it("rejects redirect that conflicts with a post path", async () => {
131
- await postService.create({
132
- format: "note",
133
- body: "test",
134
- path: "my-post",
135
- });
136
-
137
- await expect(
138
- redirectService.create("/my-post", "/elsewhere"),
139
- ).rejects.toThrow(ConflictError);
140
- });
141
-
142
- it("allows upsert for existing redirect (same type)", async () => {
143
- await redirectService.create("/old", "/first");
144
- const second = await redirectService.create("/old", "/second");
145
-
146
- expect(second.toPath).toBe("/second");
147
-
148
- const list = await redirectService.list();
149
- expect(list).toHaveLength(1);
150
- });
151
-
152
- it("releases path on delete", async () => {
153
- const redirect = await redirectService.create("/old", "/new");
154
- await redirectService.delete(redirect.id);
155
-
156
- expect(await pathRegistry.isAvailable("old")).toBe(true);
157
- });
158
- });
159
- });