@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.
- package/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
package/src/services/index.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import type { Database } from "../db/index.js";
|
|
8
8
|
import { createSettingsService, type SettingsService } from "./settings.js";
|
|
9
9
|
import { createPostService, type PostService } from "./post.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { createCustomUrlService, type CustomUrlService } from "./custom-url.js";
|
|
11
|
+
import { createPathService, type PathService } from "./path.js";
|
|
12
12
|
import { createMediaService, type MediaService } from "./media.js";
|
|
13
13
|
import {
|
|
14
14
|
createCollectionService,
|
|
@@ -17,48 +17,55 @@ import {
|
|
|
17
17
|
import { createSearchService, type SearchService } from "./search.js";
|
|
18
18
|
import { createNavItemService, type NavItemService } from "./navigation.js";
|
|
19
19
|
import { createAuthService, type AuthService } from "./auth.js";
|
|
20
|
-
import {
|
|
21
|
-
createPathRegistryService,
|
|
22
|
-
type PathRegistryService,
|
|
23
|
-
} from "./path-registry.js";
|
|
20
|
+
import { createApiTokenService, type ApiTokenService } from "./api-token.js";
|
|
24
21
|
|
|
25
22
|
export interface Services {
|
|
26
23
|
settings: SettingsService;
|
|
24
|
+
paths: PathService;
|
|
27
25
|
posts: PostService;
|
|
28
|
-
|
|
29
|
-
redirects: RedirectService;
|
|
26
|
+
customUrls: CustomUrlService;
|
|
30
27
|
media: MediaService;
|
|
31
28
|
collections: CollectionService;
|
|
32
29
|
search: SearchService;
|
|
33
30
|
navItems: NavItemService;
|
|
34
31
|
auth: AuthService;
|
|
35
|
-
|
|
32
|
+
apiTokens: ApiTokenService;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
export function createServices(
|
|
35
|
+
export function createServices(
|
|
36
|
+
db: Database,
|
|
37
|
+
d1: D1Database,
|
|
38
|
+
config?: { slugIdLength?: number },
|
|
39
|
+
): Services {
|
|
39
40
|
const settings = createSettingsService(db);
|
|
40
|
-
const
|
|
41
|
+
const paths = createPathService(db);
|
|
41
42
|
return {
|
|
42
43
|
settings,
|
|
43
|
-
|
|
44
|
-
posts: createPostService(
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
paths,
|
|
45
|
+
posts: createPostService(
|
|
46
|
+
db,
|
|
47
|
+
{
|
|
48
|
+
slugIdLength: config?.slugIdLength ?? 5,
|
|
49
|
+
},
|
|
50
|
+
paths,
|
|
51
|
+
),
|
|
52
|
+
customUrls: createCustomUrlService(db, paths),
|
|
47
53
|
media: createMediaService(db),
|
|
48
|
-
collections: createCollectionService(db),
|
|
54
|
+
collections: createCollectionService(db, paths),
|
|
49
55
|
search: createSearchService(d1),
|
|
50
56
|
navItems: createNavItemService(db),
|
|
51
57
|
auth: createAuthService(db, settings),
|
|
58
|
+
apiTokens: createApiTokenService(db),
|
|
52
59
|
};
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
export type { SettingsService } from "./settings.js";
|
|
63
|
+
export type { PathService } from "./path.js";
|
|
56
64
|
export type { PostService, PostFilters, PostDeleteDeps } from "./post.js";
|
|
57
|
-
export type {
|
|
58
|
-
export type { RedirectService } from "./redirect.js";
|
|
65
|
+
export type { CustomUrlService } from "./custom-url.js";
|
|
59
66
|
export type { MediaService, MediaFilters } from "./media.js";
|
|
60
67
|
export type { CollectionService } from "./collection.js";
|
|
61
68
|
export type { SearchService, SearchResult, SearchOptions } from "./search.js";
|
|
62
69
|
export type { NavItemService } from "./navigation.js";
|
|
63
70
|
export type { AuthService } from "./auth.js";
|
|
64
|
-
export type {
|
|
71
|
+
export type { ApiTokenService } from "./api-token.js";
|
package/src/services/media.ts
CHANGED
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { eq, desc, inArray, asc, sql, and } from "drizzle-orm";
|
|
8
|
+
import { generateKeyBetween } from "fractional-indexing";
|
|
8
9
|
import { uuidv7 } from "uuidv7";
|
|
9
10
|
import type { Database } from "../db/index.js";
|
|
10
11
|
import { media } from "../db/schema.js";
|
|
11
12
|
import { now } from "../lib/time.js";
|
|
12
13
|
import type { StorageDriver } from "../lib/storage.js";
|
|
13
|
-
import
|
|
14
|
+
import { toMediaKind } from "../lib/upload.js";
|
|
15
|
+
import type { Media, MediaKind } from "../types.js";
|
|
14
16
|
import { MAX_MEDIA_ATTACHMENTS } from "../types.js";
|
|
15
17
|
import { ValidationError } from "../lib/errors.js";
|
|
16
18
|
|
|
19
|
+
const DEFAULT_MEDIA_POSITION = "a0";
|
|
20
|
+
|
|
17
21
|
export interface MediaFilters {
|
|
18
22
|
limit?: number;
|
|
19
23
|
/** Filter by MIME type prefix, e.g. "image/" */
|
|
@@ -23,8 +27,8 @@ export interface MediaFilters {
|
|
|
23
27
|
export interface MediaService {
|
|
24
28
|
getById(id: string): Promise<Media | null>;
|
|
25
29
|
getByIds(ids: string[]): Promise<Media[]>;
|
|
26
|
-
getByPostId(postId:
|
|
27
|
-
getByPostIds(postIds:
|
|
30
|
+
getByPostId(postId: string): Promise<Media[]>;
|
|
31
|
+
getByPostIds(postIds: string[]): Promise<Map<string, Media[]>>;
|
|
28
32
|
list(filters?: MediaFilters): Promise<Media[]>;
|
|
29
33
|
create(data: CreateMediaData): Promise<Media>;
|
|
30
34
|
/**
|
|
@@ -50,15 +54,15 @@ export interface MediaService {
|
|
|
50
54
|
* @param storage - Optional storage driver; when provided the files are deleted from storage
|
|
51
55
|
*/
|
|
52
56
|
deleteByIds(ids: string[], storage?: StorageDriver | null): Promise<void>;
|
|
53
|
-
getByStorageKey(storageKey: string): Promise<Media | null>;
|
|
54
|
-
attachToPost(postId:
|
|
55
|
-
detachFromPost(postId:
|
|
57
|
+
getByStorageKey(storageKey: string, provider: string): Promise<Media | null>;
|
|
58
|
+
attachToPost(postId: string, mediaIds: string[]): Promise<void>;
|
|
59
|
+
detachFromPost(postId: string): Promise<void>;
|
|
56
60
|
updateAlt(id: string, alt: string): Promise<void>;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
export interface CreateMediaData {
|
|
60
64
|
id?: string;
|
|
61
|
-
postId?:
|
|
65
|
+
postId?: string;
|
|
62
66
|
filename: string;
|
|
63
67
|
originalName: string;
|
|
64
68
|
mimeType: string;
|
|
@@ -68,11 +72,38 @@ export interface CreateMediaData {
|
|
|
68
72
|
width?: number;
|
|
69
73
|
height?: number;
|
|
70
74
|
alt?: string;
|
|
71
|
-
position?:
|
|
75
|
+
position?: string;
|
|
72
76
|
blurhash?: string;
|
|
77
|
+
waveform?: string;
|
|
78
|
+
posterKey?: string;
|
|
79
|
+
summary?: string;
|
|
80
|
+
chars?: number;
|
|
81
|
+
mediaKind?: MediaKind;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
export function createMediaService(db: Database): MediaService {
|
|
85
|
+
async function getLastPosition(postId: string): Promise<string | null> {
|
|
86
|
+
const rows = await db
|
|
87
|
+
.select({ position: media.position })
|
|
88
|
+
.from(media)
|
|
89
|
+
.where(eq(media.postId, postId))
|
|
90
|
+
.orderBy(sql`${media.position} DESC`)
|
|
91
|
+
.limit(1);
|
|
92
|
+
return rows[0]?.position ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildSequentialPositions(count: number): string[] {
|
|
96
|
+
const positions: string[] = [];
|
|
97
|
+
let previous: string | null = null;
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < count; i += 1) {
|
|
100
|
+
previous = generateKeyBetween(previous, null);
|
|
101
|
+
positions.push(previous);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return positions;
|
|
105
|
+
}
|
|
106
|
+
|
|
76
107
|
function toMedia(row: typeof media.$inferSelect): Media {
|
|
77
108
|
return {
|
|
78
109
|
id: row.id,
|
|
@@ -88,7 +119,13 @@ export function createMediaService(db: Database): MediaService {
|
|
|
88
119
|
alt: row.alt,
|
|
89
120
|
position: row.position,
|
|
90
121
|
blurhash: row.blurhash,
|
|
122
|
+
waveform: row.waveform,
|
|
123
|
+
posterKey: row.posterKey,
|
|
124
|
+
summary: row.summary,
|
|
125
|
+
chars: row.chars,
|
|
126
|
+
mediaKind: row.mediaKind as MediaKind,
|
|
91
127
|
createdAt: row.createdAt,
|
|
128
|
+
updatedAt: row.updatedAt,
|
|
92
129
|
};
|
|
93
130
|
}
|
|
94
131
|
|
|
@@ -118,7 +155,7 @@ export function createMediaService(db: Database): MediaService {
|
|
|
118
155
|
},
|
|
119
156
|
|
|
120
157
|
async getByPostIds(postIds) {
|
|
121
|
-
const result = new Map<
|
|
158
|
+
const result = new Map<string, Media[]>();
|
|
122
159
|
if (postIds.length === 0) return result;
|
|
123
160
|
|
|
124
161
|
const rows = await db
|
|
@@ -141,11 +178,13 @@ export function createMediaService(db: Database): MediaService {
|
|
|
141
178
|
return result;
|
|
142
179
|
},
|
|
143
180
|
|
|
144
|
-
async getByStorageKey(storageKey) {
|
|
181
|
+
async getByStorageKey(storageKey, provider) {
|
|
145
182
|
const result = await db
|
|
146
183
|
.select()
|
|
147
184
|
.from(media)
|
|
148
|
-
.where(
|
|
185
|
+
.where(
|
|
186
|
+
and(eq(media.storageKey, storageKey), eq(media.provider, provider)),
|
|
187
|
+
)
|
|
149
188
|
.limit(1);
|
|
150
189
|
return result[0] ? toMedia(result[0]) : null;
|
|
151
190
|
},
|
|
@@ -185,6 +224,11 @@ export function createMediaService(db: Database): MediaService {
|
|
|
185
224
|
async create(data) {
|
|
186
225
|
const id = data.id ?? uuidv7();
|
|
187
226
|
const timestamp = now();
|
|
227
|
+
const mediaKind = data.mediaKind ?? toMediaKind(data.mimeType);
|
|
228
|
+
const lastPosition =
|
|
229
|
+
data.position === undefined && data.postId
|
|
230
|
+
? await getLastPosition(data.postId)
|
|
231
|
+
: null;
|
|
188
232
|
|
|
189
233
|
const result = await db
|
|
190
234
|
.insert(media)
|
|
@@ -200,9 +244,19 @@ export function createMediaService(db: Database): MediaService {
|
|
|
200
244
|
width: data.width ?? null,
|
|
201
245
|
height: data.height ?? null,
|
|
202
246
|
alt: data.alt ?? null,
|
|
203
|
-
position:
|
|
247
|
+
position:
|
|
248
|
+
data.position ??
|
|
249
|
+
(data.postId
|
|
250
|
+
? generateKeyBetween(lastPosition, null)
|
|
251
|
+
: DEFAULT_MEDIA_POSITION),
|
|
204
252
|
blurhash: data.blurhash ?? null,
|
|
253
|
+
waveform: data.waveform ?? null,
|
|
254
|
+
posterKey: data.posterKey ?? null,
|
|
255
|
+
summary: data.summary ?? null,
|
|
256
|
+
chars: data.chars ?? null,
|
|
257
|
+
mediaKind,
|
|
205
258
|
createdAt: timestamp,
|
|
259
|
+
updatedAt: timestamp,
|
|
206
260
|
})
|
|
207
261
|
.returning();
|
|
208
262
|
|
|
@@ -211,9 +265,14 @@ export function createMediaService(db: Database): MediaService {
|
|
|
211
265
|
},
|
|
212
266
|
|
|
213
267
|
async attachToPost(postId, mediaIds) {
|
|
268
|
+
const timestamp = now();
|
|
214
269
|
const clearQuery = db
|
|
215
270
|
.update(media)
|
|
216
|
-
.set({
|
|
271
|
+
.set({
|
|
272
|
+
postId: null,
|
|
273
|
+
position: DEFAULT_MEDIA_POSITION,
|
|
274
|
+
updatedAt: timestamp,
|
|
275
|
+
})
|
|
217
276
|
.where(eq(media.postId, postId));
|
|
218
277
|
|
|
219
278
|
const validIds = mediaIds.filter((id): id is string => Boolean(id));
|
|
@@ -223,13 +282,20 @@ export function createMediaService(db: Database): MediaService {
|
|
|
223
282
|
return;
|
|
224
283
|
}
|
|
225
284
|
|
|
285
|
+
const positions = buildSequentialPositions(validIds.length);
|
|
286
|
+
|
|
226
287
|
// Clear existing + re-attach atomically
|
|
227
|
-
const attachQueries = validIds.map((mediaId, i) =>
|
|
228
|
-
|
|
288
|
+
const attachQueries = validIds.map((mediaId, i) => {
|
|
289
|
+
const position = positions[i];
|
|
290
|
+
if (!position) {
|
|
291
|
+
throw new Error("Failed to assign a media position");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return db
|
|
229
295
|
.update(media)
|
|
230
|
-
.set({ postId, position:
|
|
231
|
-
.where(eq(media.id, mediaId))
|
|
232
|
-
);
|
|
296
|
+
.set({ postId, position, updatedAt: timestamp })
|
|
297
|
+
.where(eq(media.id, mediaId));
|
|
298
|
+
});
|
|
233
299
|
await db.batch([clearQuery, ...attachQueries] as [
|
|
234
300
|
typeof clearQuery,
|
|
235
301
|
...(typeof attachQueries)[number][],
|
|
@@ -239,12 +305,15 @@ export function createMediaService(db: Database): MediaService {
|
|
|
239
305
|
async detachFromPost(postId) {
|
|
240
306
|
await db
|
|
241
307
|
.update(media)
|
|
242
|
-
.set({ postId: null, position:
|
|
308
|
+
.set({ postId: null, position: DEFAULT_MEDIA_POSITION })
|
|
243
309
|
.where(eq(media.postId, postId));
|
|
244
310
|
},
|
|
245
311
|
|
|
246
312
|
async updateAlt(id, alt) {
|
|
247
|
-
await db
|
|
313
|
+
await db
|
|
314
|
+
.update(media)
|
|
315
|
+
.set({ alt, updatedAt: now() })
|
|
316
|
+
.where(eq(media.id, id));
|
|
248
317
|
},
|
|
249
318
|
|
|
250
319
|
async delete(id, storage) {
|
|
@@ -256,6 +325,12 @@ export function createMediaService(db: Database): MediaService {
|
|
|
256
325
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
257
326
|
console.error("Storage delete error:", err);
|
|
258
327
|
});
|
|
328
|
+
if (record.posterKey) {
|
|
329
|
+
await storage.delete(record.posterKey).catch((err) => {
|
|
330
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
331
|
+
console.error("Storage delete poster error:", err);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
259
334
|
}
|
|
260
335
|
|
|
261
336
|
await db.delete(media).where(eq(media.id, id));
|
|
@@ -267,9 +342,12 @@ export function createMediaService(db: Database): MediaService {
|
|
|
267
342
|
|
|
268
343
|
if (storage) {
|
|
269
344
|
const records = await this.getByIds(ids);
|
|
345
|
+
const keys = records.flatMap((r) =>
|
|
346
|
+
r.posterKey ? [r.storageKey, r.posterKey] : [r.storageKey],
|
|
347
|
+
);
|
|
270
348
|
await Promise.all(
|
|
271
|
-
|
|
272
|
-
storage.delete(
|
|
349
|
+
keys.map((key) =>
|
|
350
|
+
storage.delete(key).catch((err) => {
|
|
273
351
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
274
352
|
console.error("Storage delete error:", err);
|
|
275
353
|
}),
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nav Item Service (v2)
|
|
3
3
|
*
|
|
4
|
-
* Manages navigation items (
|
|
4
|
+
* Manages navigation items (external links and system links)
|
|
5
|
+
* with fractional indexing for efficient reordering.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { eq, asc, sql } from "drizzle-orm";
|
|
9
|
+
import { generateKeyBetween } from "fractional-indexing";
|
|
10
|
+
import { uuidv7 } from "uuidv7";
|
|
8
11
|
import type { Database } from "../db/index.js";
|
|
9
12
|
import { navItems } from "../db/schema.js";
|
|
10
13
|
import { now } from "../lib/time.js";
|
|
@@ -15,14 +18,37 @@ import type {
|
|
|
15
18
|
UpdateNavItem,
|
|
16
19
|
} from "../types.js";
|
|
17
20
|
|
|
21
|
+
const POSITION_RETRY_ATTEMPTS = 5;
|
|
22
|
+
|
|
23
|
+
function isUniqueConstraintError(err: unknown): boolean {
|
|
24
|
+
let current: unknown = err;
|
|
25
|
+
while (current) {
|
|
26
|
+
const msg = String(current);
|
|
27
|
+
if (
|
|
28
|
+
msg.includes("UNIQUE constraint") ||
|
|
29
|
+
msg.includes("SQLITE_CONSTRAINT")
|
|
30
|
+
) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
current =
|
|
34
|
+
current instanceof Error && current.cause !== current
|
|
35
|
+
? current.cause
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
export interface NavItemService {
|
|
19
42
|
list(): Promise<NavItem[]>;
|
|
20
|
-
getById(id:
|
|
43
|
+
getById(id: string): Promise<NavItem | null>;
|
|
21
44
|
create(data: CreateNavItem): Promise<NavItem>;
|
|
22
|
-
update(id:
|
|
23
|
-
delete(id:
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
update(id: string, data: UpdateNavItem): Promise<NavItem | null>;
|
|
46
|
+
delete(id: string): Promise<boolean>;
|
|
47
|
+
move(
|
|
48
|
+
id: string,
|
|
49
|
+
afterId: string | null,
|
|
50
|
+
beforeId: string | null,
|
|
51
|
+
): Promise<NavItem | null>;
|
|
26
52
|
}
|
|
27
53
|
|
|
28
54
|
export function createNavItemService(db: Database): NavItemService {
|
|
@@ -32,13 +58,63 @@ export function createNavItemService(db: Database): NavItemService {
|
|
|
32
58
|
type: row.type as NavItemType,
|
|
33
59
|
label: row.label,
|
|
34
60
|
url: row.url,
|
|
35
|
-
pageId: row.pageId,
|
|
36
61
|
position: row.position,
|
|
37
62
|
createdAt: row.createdAt,
|
|
38
63
|
updatedAt: row.updatedAt,
|
|
39
64
|
};
|
|
40
65
|
}
|
|
41
66
|
|
|
67
|
+
async function getLastPosition(): Promise<string | null> {
|
|
68
|
+
const rows = await db
|
|
69
|
+
.select({ position: navItems.position })
|
|
70
|
+
.from(navItems)
|
|
71
|
+
.orderBy(sql`${navItems.position} DESC`)
|
|
72
|
+
.limit(1);
|
|
73
|
+
return rows[0]?.position ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function listOrderedPositions(excludeId?: string) {
|
|
77
|
+
const rows = await db
|
|
78
|
+
.select({ id: navItems.id, position: navItems.position })
|
|
79
|
+
.from(navItems)
|
|
80
|
+
.orderBy(asc(navItems.position));
|
|
81
|
+
return excludeId ? rows.filter((row) => row.id !== excludeId) : rows;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function getAppendPosition(): Promise<string> {
|
|
85
|
+
const lastPos = await getLastPosition();
|
|
86
|
+
return generateKeyBetween(lastPos, null);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function getMovePosition(
|
|
90
|
+
id: string,
|
|
91
|
+
afterId: string | null,
|
|
92
|
+
beforeId: string | null,
|
|
93
|
+
): Promise<string> {
|
|
94
|
+
const rows = await listOrderedPositions(id);
|
|
95
|
+
const afterIndex = afterId
|
|
96
|
+
? rows.findIndex((row) => row.id === afterId)
|
|
97
|
+
: -1;
|
|
98
|
+
if (afterIndex >= 0) {
|
|
99
|
+
return generateKeyBetween(
|
|
100
|
+
rows[afterIndex]?.position ?? null,
|
|
101
|
+
rows[afterIndex + 1]?.position ?? null,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const beforeIndex = beforeId
|
|
106
|
+
? rows.findIndex((row) => row.id === beforeId)
|
|
107
|
+
: -1;
|
|
108
|
+
if (beforeIndex >= 0) {
|
|
109
|
+
return generateKeyBetween(
|
|
110
|
+
rows[beforeIndex - 1]?.position ?? null,
|
|
111
|
+
rows[beforeIndex]?.position ?? null,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return generateKeyBetween(rows.at(-1)?.position ?? null, null);
|
|
116
|
+
}
|
|
117
|
+
|
|
42
118
|
return {
|
|
43
119
|
async list() {
|
|
44
120
|
const rows = await db
|
|
@@ -58,32 +134,55 @@ export function createNavItemService(db: Database): NavItemService {
|
|
|
58
134
|
},
|
|
59
135
|
|
|
60
136
|
async create(data) {
|
|
137
|
+
const id = uuidv7();
|
|
61
138
|
const timestamp = now();
|
|
62
139
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
140
|
+
if (data.position !== undefined) {
|
|
141
|
+
const result = await db
|
|
142
|
+
.insert(navItems)
|
|
143
|
+
.values({
|
|
144
|
+
id,
|
|
145
|
+
type: data.type,
|
|
146
|
+
label: data.label,
|
|
147
|
+
url: data.url,
|
|
148
|
+
position: data.position,
|
|
149
|
+
createdAt: timestamp,
|
|
150
|
+
updatedAt: timestamp,
|
|
151
|
+
})
|
|
152
|
+
.returning();
|
|
153
|
+
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
155
|
+
return toNavItem(result[0]!);
|
|
70
156
|
}
|
|
71
157
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
158
|
+
for (let attempt = 0; attempt < POSITION_RETRY_ATTEMPTS; attempt += 1) {
|
|
159
|
+
try {
|
|
160
|
+
const result = await db
|
|
161
|
+
.insert(navItems)
|
|
162
|
+
.values({
|
|
163
|
+
id,
|
|
164
|
+
type: data.type,
|
|
165
|
+
label: data.label,
|
|
166
|
+
url: data.url,
|
|
167
|
+
position: await getAppendPosition(),
|
|
168
|
+
createdAt: timestamp,
|
|
169
|
+
updatedAt: timestamp,
|
|
170
|
+
})
|
|
171
|
+
.returning();
|
|
84
172
|
|
|
85
|
-
|
|
86
|
-
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
174
|
+
return toNavItem(result[0]!);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (
|
|
177
|
+
!isUniqueConstraintError(err) ||
|
|
178
|
+
attempt === POSITION_RETRY_ATTEMPTS - 1
|
|
179
|
+
) {
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
throw new Error("Failed to assign a unique nav item position");
|
|
87
186
|
},
|
|
88
187
|
|
|
89
188
|
async update(id, data) {
|
|
@@ -101,7 +200,6 @@ export function createNavItemService(db: Database): NavItemService {
|
|
|
101
200
|
...(data.type !== undefined && { type: data.type }),
|
|
102
201
|
...(data.label !== undefined && { label: data.label }),
|
|
103
202
|
...(data.url !== undefined && { url: data.url }),
|
|
104
|
-
...(data.pageId !== undefined && { pageId: data.pageId }),
|
|
105
203
|
...(data.position !== undefined && { position: data.position }),
|
|
106
204
|
updatedAt: timestamp,
|
|
107
205
|
})
|
|
@@ -119,26 +217,39 @@ export function createNavItemService(db: Database): NavItemService {
|
|
|
119
217
|
return result.length > 0;
|
|
120
218
|
},
|
|
121
219
|
|
|
122
|
-
async
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
220
|
+
async move(id, afterId, beforeId) {
|
|
221
|
+
// Look up the item
|
|
222
|
+
const items = await db
|
|
223
|
+
.select()
|
|
224
|
+
.from(navItems)
|
|
225
|
+
.where(eq(navItems.id, id))
|
|
226
|
+
.limit(1);
|
|
227
|
+
if (!items[0]) return null;
|
|
129
228
|
|
|
130
|
-
async reorder(ids) {
|
|
131
|
-
if (ids.length === 0) return;
|
|
132
229
|
const timestamp = now();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
230
|
+
for (let attempt = 0; attempt < POSITION_RETRY_ATTEMPTS; attempt += 1) {
|
|
231
|
+
try {
|
|
232
|
+
const result = await db
|
|
233
|
+
.update(navItems)
|
|
234
|
+
.set({
|
|
235
|
+
position: await getMovePosition(id, afterId, beforeId),
|
|
236
|
+
updatedAt: timestamp,
|
|
237
|
+
})
|
|
238
|
+
.where(eq(navItems.id, id))
|
|
239
|
+
.returning();
|
|
240
|
+
|
|
241
|
+
return result[0] ? toNavItem(result[0]) : null;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (
|
|
244
|
+
!isUniqueConstraintError(err) ||
|
|
245
|
+
attempt === POSITION_RETRY_ATTEMPTS - 1
|
|
246
|
+
) {
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
throw new Error("Failed to assign a unique nav item position");
|
|
142
253
|
},
|
|
143
254
|
};
|
|
144
255
|
}
|