@zlooks.cn/blog-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blog/blog-application.d.ts +153 -0
- package/dist/blog/blog-application.d.ts.map +1 -0
- package/dist/blog/blog-application.js +115 -0
- package/dist/blog/blog-application.js.map +1 -0
- package/dist/blog/comment-client-metadata.d.ts +19 -0
- package/dist/blog/comment-client-metadata.d.ts.map +1 -0
- package/dist/blog/comment-client-metadata.js +78 -0
- package/dist/blog/comment-client-metadata.js.map +1 -0
- package/dist/blog/errors.d.ts +12 -0
- package/dist/blog/errors.d.ts.map +1 -0
- package/dist/blog/errors.js +21 -0
- package/dist/blog/errors.js.map +1 -0
- package/dist/blog/micro-user-directory.d.ts +11 -0
- package/dist/blog/micro-user-directory.d.ts.map +1 -0
- package/dist/blog/micro-user-directory.js +18 -0
- package/dist/blog/micro-user-directory.js.map +1 -0
- package/dist/blog/post-content-audit.d.ts +34 -0
- package/dist/blog/post-content-audit.d.ts.map +1 -0
- package/dist/blog/post-content-audit.js +190 -0
- package/dist/blog/post-content-audit.js.map +1 -0
- package/dist/blog/post-view-cache.d.ts +15 -0
- package/dist/blog/post-view-cache.d.ts.map +1 -0
- package/dist/blog/post-view-cache.js +23 -0
- package/dist/blog/post-view-cache.js.map +1 -0
- package/dist/blog/public-blog-micro.d.ts +291 -0
- package/dist/blog/public-blog-micro.d.ts.map +1 -0
- package/dist/blog/public-blog-micro.js +71 -0
- package/dist/blog/public-blog-micro.js.map +1 -0
- package/dist/blog/public-blog-service.d.ts +104 -0
- package/dist/blog/public-blog-service.d.ts.map +1 -0
- package/dist/blog/public-blog-service.js +341 -0
- package/dist/blog/public-blog-service.js.map +1 -0
- package/dist/browser-api/dispatch-stream.d.ts +5 -0
- package/dist/browser-api/dispatch-stream.d.ts.map +1 -0
- package/dist/browser-api/dispatch-stream.js +4 -0
- package/dist/browser-api/dispatch-stream.js.map +1 -0
- package/dist/browser-api/dispatcher.d.ts +15 -0
- package/dist/browser-api/dispatcher.d.ts.map +1 -0
- package/dist/browser-api/dispatcher.js +145 -0
- package/dist/browser-api/dispatcher.js.map +1 -0
- package/dist/browser-api/provider-publisher.d.ts +24 -0
- package/dist/browser-api/provider-publisher.d.ts.map +1 -0
- package/dist/browser-api/provider-publisher.js +107 -0
- package/dist/browser-api/provider-publisher.js.map +1 -0
- package/dist/command/index.d.ts +3 -0
- package/dist/command/index.d.ts.map +1 -0
- package/dist/command/index.js +24 -0
- package/dist/command/index.js.map +1 -0
- package/dist/config/schema.d.ts +25 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +19 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/contracts/identity.d.ts +2 -0
- package/dist/contracts/identity.d.ts.map +1 -0
- package/dist/contracts/identity.js +2 -0
- package/dist/contracts/identity.js.map +1 -0
- package/dist/contracts/mcp.d.ts +69 -0
- package/dist/contracts/mcp.d.ts.map +1 -0
- package/dist/contracts/mcp.js +41 -0
- package/dist/contracts/mcp.js.map +1 -0
- package/dist/contracts/public-api.d.ts +3 -0
- package/dist/contracts/public-api.d.ts.map +1 -0
- package/dist/contracts/public-api.js +2 -0
- package/dist/contracts/public-api.js.map +1 -0
- package/dist/mcp/blog-prompts.d.ts +135 -0
- package/dist/mcp/blog-prompts.d.ts.map +1 -0
- package/dist/mcp/blog-prompts.js +107 -0
- package/dist/mcp/blog-prompts.js.map +1 -0
- package/dist/mcp/blog-provider.d.ts +4 -0
- package/dist/mcp/blog-provider.d.ts.map +1 -0
- package/dist/mcp/blog-provider.js +27 -0
- package/dist/mcp/blog-provider.js.map +1 -0
- package/dist/mcp/blog-theme-authoring-prompt.d.ts +19 -0
- package/dist/mcp/blog-theme-authoring-prompt.d.ts.map +1 -0
- package/dist/mcp/blog-theme-authoring-prompt.js +171 -0
- package/dist/mcp/blog-theme-authoring-prompt.js.map +1 -0
- package/dist/mcp/category-tools.d.ts +45 -0
- package/dist/mcp/category-tools.d.ts.map +1 -0
- package/dist/mcp/category-tools.js +14 -0
- package/dist/mcp/category-tools.js.map +1 -0
- package/dist/mcp/comment-tools.d.ts +44 -0
- package/dist/mcp/comment-tools.d.ts.map +1 -0
- package/dist/mcp/comment-tools.js +15 -0
- package/dist/mcp/comment-tools.js.map +1 -0
- package/dist/mcp/friend-link-tools.d.ts +63 -0
- package/dist/mcp/friend-link-tools.d.ts.map +1 -0
- package/dist/mcp/friend-link-tools.js +16 -0
- package/dist/mcp/friend-link-tools.js.map +1 -0
- package/dist/mcp/page-tools.d.ts +67 -0
- package/dist/mcp/page-tools.d.ts.map +1 -0
- package/dist/mcp/page-tools.js +36 -0
- package/dist/mcp/page-tools.js.map +1 -0
- package/dist/mcp/post-tools.d.ts +114 -0
- package/dist/mcp/post-tools.d.ts.map +1 -0
- package/dist/mcp/post-tools.js +122 -0
- package/dist/mcp/post-tools.js.map +1 -0
- package/dist/mcp/tag-tools.d.ts +37 -0
- package/dist/mcp/tag-tools.d.ts.map +1 -0
- package/dist/mcp/tag-tools.js +14 -0
- package/dist/mcp/tag-tools.js.map +1 -0
- package/dist/mcp/theme-tools.d.ts +16 -0
- package/dist/mcp/theme-tools.d.ts.map +1 -0
- package/dist/mcp/theme-tools.js +71 -0
- package/dist/mcp/theme-tools.js.map +1 -0
- package/dist/mcp/tool-descriptions.d.ts +5 -0
- package/dist/mcp/tool-descriptions.d.ts.map +1 -0
- package/dist/mcp/tool-descriptions.js +51 -0
- package/dist/mcp/tool-descriptions.js.map +1 -0
- package/dist/mcp/tool-support.d.ts +242 -0
- package/dist/mcp/tool-support.d.ts.map +1 -0
- package/dist/mcp/tool-support.js +127 -0
- package/dist/mcp/tool-support.js.map +1 -0
- package/dist/messages/browser/dispatch.msg.d.ts +3 -0
- package/dist/messages/browser/dispatch.msg.d.ts.map +1 -0
- package/dist/messages/browser/dispatch.msg.js +9 -0
- package/dist/messages/browser/dispatch.msg.js.map +1 -0
- package/dist/messages/public/categories/list.msg.d.ts +3 -0
- package/dist/messages/public/categories/list.msg.d.ts.map +1 -0
- package/dist/messages/public/categories/list.msg.js +8 -0
- package/dist/messages/public/categories/list.msg.js.map +1 -0
- package/dist/messages/public/comments/list.msg.d.ts +3 -0
- package/dist/messages/public/comments/list.msg.d.ts.map +1 -0
- package/dist/messages/public/comments/list.msg.js +8 -0
- package/dist/messages/public/comments/list.msg.js.map +1 -0
- package/dist/messages/public/comments/recent.msg.d.ts +3 -0
- package/dist/messages/public/comments/recent.msg.d.ts.map +1 -0
- package/dist/messages/public/comments/recent.msg.js +8 -0
- package/dist/messages/public/comments/recent.msg.js.map +1 -0
- package/dist/messages/public/friend-links/list.msg.d.ts +3 -0
- package/dist/messages/public/friend-links/list.msg.d.ts.map +1 -0
- package/dist/messages/public/friend-links/list.msg.js +8 -0
- package/dist/messages/public/friend-links/list.msg.js.map +1 -0
- package/dist/messages/public/pages/list.msg.d.ts +3 -0
- package/dist/messages/public/pages/list.msg.d.ts.map +1 -0
- package/dist/messages/public/pages/list.msg.js +8 -0
- package/dist/messages/public/pages/list.msg.js.map +1 -0
- package/dist/messages/public/pages/read.msg.d.ts +3 -0
- package/dist/messages/public/pages/read.msg.d.ts.map +1 -0
- package/dist/messages/public/pages/read.msg.js +8 -0
- package/dist/messages/public/pages/read.msg.js.map +1 -0
- package/dist/messages/public/posts/archive.msg.d.ts +3 -0
- package/dist/messages/public/posts/archive.msg.d.ts.map +1 -0
- package/dist/messages/public/posts/archive.msg.js +8 -0
- package/dist/messages/public/posts/archive.msg.js.map +1 -0
- package/dist/messages/public/posts/connections.msg.d.ts +3 -0
- package/dist/messages/public/posts/connections.msg.d.ts.map +1 -0
- package/dist/messages/public/posts/connections.msg.js +8 -0
- package/dist/messages/public/posts/connections.msg.js.map +1 -0
- package/dist/messages/public/posts/list.msg.d.ts +3 -0
- package/dist/messages/public/posts/list.msg.d.ts.map +1 -0
- package/dist/messages/public/posts/list.msg.js +8 -0
- package/dist/messages/public/posts/list.msg.js.map +1 -0
- package/dist/messages/public/posts/open.msg.d.ts +3 -0
- package/dist/messages/public/posts/open.msg.d.ts.map +1 -0
- package/dist/messages/public/posts/open.msg.js +8 -0
- package/dist/messages/public/posts/open.msg.js.map +1 -0
- package/dist/messages/public/posts/read.msg.d.ts +3 -0
- package/dist/messages/public/posts/read.msg.d.ts.map +1 -0
- package/dist/messages/public/posts/read.msg.js +8 -0
- package/dist/messages/public/posts/read.msg.js.map +1 -0
- package/dist/messages/public/tags/list.msg.d.ts +3 -0
- package/dist/messages/public/tags/list.msg.d.ts.map +1 -0
- package/dist/messages/public/tags/list.msg.js +8 -0
- package/dist/messages/public/tags/list.msg.js.map +1 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +2 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/persistence/category-store.d.ts +23 -0
- package/dist/persistence/category-store.d.ts.map +1 -0
- package/dist/persistence/category-store.js +86 -0
- package/dist/persistence/category-store.js.map +1 -0
- package/dist/persistence/comment-store.d.ts +41 -0
- package/dist/persistence/comment-store.d.ts.map +1 -0
- package/dist/persistence/comment-store.js +146 -0
- package/dist/persistence/comment-store.js.map +1 -0
- package/dist/persistence/entities.d.ts +103 -0
- package/dist/persistence/entities.d.ts.map +1 -0
- package/dist/persistence/entities.js +188 -0
- package/dist/persistence/entities.js.map +1 -0
- package/dist/persistence/friend-link-store.d.ts +26 -0
- package/dist/persistence/friend-link-store.d.ts.map +1 -0
- package/dist/persistence/friend-link-store.js +80 -0
- package/dist/persistence/friend-link-store.js.map +1 -0
- package/dist/persistence/page-store.d.ts +35 -0
- package/dist/persistence/page-store.d.ts.map +1 -0
- package/dist/persistence/page-store.js +125 -0
- package/dist/persistence/page-store.js.map +1 -0
- package/dist/persistence/post-engagement-store.d.ts +23 -0
- package/dist/persistence/post-engagement-store.d.ts.map +1 -0
- package/dist/persistence/post-engagement-store.js +85 -0
- package/dist/persistence/post-engagement-store.js.map +1 -0
- package/dist/persistence/post-store.d.ts +87 -0
- package/dist/persistence/post-store.d.ts.map +1 -0
- package/dist/persistence/post-store.js +465 -0
- package/dist/persistence/post-store.js.map +1 -0
- package/dist/persistence/tag-store.d.ts +21 -0
- package/dist/persistence/tag-store.d.ts.map +1 -0
- package/dist/persistence/tag-store.js +82 -0
- package/dist/persistence/tag-store.js.map +1 -0
- package/dist/services/application.service.d.ts +4 -0
- package/dist/services/application.service.d.ts.map +1 -0
- package/dist/services/application.service.js +22 -0
- package/dist/services/application.service.js.map +1 -0
- package/dist/services/blog-runtime.service.d.ts +10 -0
- package/dist/services/blog-runtime.service.d.ts.map +1 -0
- package/dist/services/blog-runtime.service.js +32 -0
- package/dist/services/blog-runtime.service.js.map +1 -0
- package/dist/services/blog.boot.d.ts +6 -0
- package/dist/services/blog.boot.d.ts.map +1 -0
- package/dist/services/blog.boot.js +13 -0
- package/dist/services/blog.boot.js.map +1 -0
- package/dist/services/browser-api-provider.service.d.ts +4 -0
- package/dist/services/browser-api-provider.service.d.ts.map +1 -0
- package/dist/services/browser-api-provider.service.js +25 -0
- package/dist/services/browser-api-provider.service.js.map +1 -0
- package/dist/services/config.service.d.ts +3 -0
- package/dist/services/config.service.d.ts.map +1 -0
- package/dist/services/config.service.js +22 -0
- package/dist/services/config.service.js.map +1 -0
- package/dist/services/database.service.d.ts +3 -0
- package/dist/services/database.service.d.ts.map +1 -0
- package/dist/services/database.service.js +16 -0
- package/dist/services/database.service.js.map +1 -0
- package/dist/services/logger.service.d.ts +4 -0
- package/dist/services/logger.service.d.ts.map +1 -0
- package/dist/services/logger.service.js +8 -0
- package/dist/services/logger.service.js.map +1 -0
- package/dist/services/mcp-provider.service.d.ts +3 -0
- package/dist/services/mcp-provider.service.d.ts.map +1 -0
- package/dist/services/mcp-provider.service.js +17 -0
- package/dist/services/mcp-provider.service.js.map +1 -0
- package/dist/services/redis.service.d.ts +4 -0
- package/dist/services/redis.service.d.ts.map +1 -0
- package/dist/services/redis.service.js +18 -0
- package/dist/services/redis.service.js.map +1 -0
- package/dist/services/theme-control.service.d.ts +4 -0
- package/dist/services/theme-control.service.d.ts.map +1 -0
- package/dist/services/theme-control.service.js +53 -0
- package/dist/services/theme-control.service.js.map +1 -0
- package/dist/themes/theme-catalog.d.ts +40 -0
- package/dist/themes/theme-catalog.d.ts.map +1 -0
- package/dist/themes/theme-catalog.js +164 -0
- package/dist/themes/theme-catalog.js.map +1 -0
- package/dist/themes/theme-control.d.ts +16 -0
- package/dist/themes/theme-control.d.ts.map +1 -0
- package/dist/themes/theme-control.js +57 -0
- package/dist/themes/theme-control.js.map +1 -0
- package/dist/themes/theme-discovery-runtime.d.ts +41 -0
- package/dist/themes/theme-discovery-runtime.d.ts.map +1 -0
- package/dist/themes/theme-discovery-runtime.js +109 -0
- package/dist/themes/theme-discovery-runtime.js.map +1 -0
- package/dist/themes/theme-generation-store.d.ts +14 -0
- package/dist/themes/theme-generation-store.d.ts.map +1 -0
- package/dist/themes/theme-generation-store.js +49 -0
- package/dist/themes/theme-generation-store.js.map +1 -0
- package/dist/themes/theme-selection-publisher.d.ts +28 -0
- package/dist/themes/theme-selection-publisher.d.ts.map +1 -0
- package/dist/themes/theme-selection-publisher.js +92 -0
- package/dist/themes/theme-selection-publisher.js.map +1 -0
- package/dist/themes/theme-selection-store.d.ts +22 -0
- package/dist/themes/theme-selection-store.d.ts.map +1 -0
- package/dist/themes/theme-selection-store.js +84 -0
- package/dist/themes/theme-selection-store.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { In } from 'typeorm';
|
|
2
|
+
import { BlogConflictError, BlogNotFoundError } from '../blog/errors.js';
|
|
3
|
+
import { BlogPostEntity, BlogPostLikeEntity, BlogPostViewMarkEntity, } from './entities.js';
|
|
4
|
+
export class PostEngagementStore {
|
|
5
|
+
dataSource;
|
|
6
|
+
constructor(dataSource) {
|
|
7
|
+
this.dataSource = dataSource;
|
|
8
|
+
}
|
|
9
|
+
recordView(postId, subjectHashes, now, windowMs, signal) {
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
const subjects = [...new Set(subjectHashes)];
|
|
12
|
+
if (subjects.length === 0)
|
|
13
|
+
throw new TypeError('A post view requires at least one subject');
|
|
14
|
+
return this.dataSource.transaction(async (manager) => {
|
|
15
|
+
const posts = manager.getRepository(BlogPostEntity);
|
|
16
|
+
const post = await posts.findOne({ where: { id: postId }, lock: { mode: 'pessimistic_write' } });
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
if (!post)
|
|
19
|
+
throw new BlogNotFoundError('Post not found');
|
|
20
|
+
if (post.status !== 'published')
|
|
21
|
+
throw new BlogConflictError('Only published posts accept views');
|
|
22
|
+
const marks = manager.getRepository(BlogPostViewMarkEntity);
|
|
23
|
+
const current = await marks.find({ where: { postId, subjectHash: In(subjects) } });
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
const cutoff = now.getTime() - windowMs;
|
|
26
|
+
const recent = current
|
|
27
|
+
.filter(({ countedAt }) => countedAt.getTime() >= cutoff)
|
|
28
|
+
.sort((left, right) => right.countedAt.getTime() - left.countedAt.getTime())[0];
|
|
29
|
+
if (recent) {
|
|
30
|
+
await this.saveMarks(marks, postId, subjects, current, recent.countedAt);
|
|
31
|
+
signal?.throwIfAborted();
|
|
32
|
+
return { counted: false, countedAt: recent.countedAt, viewCount: post.viewCount };
|
|
33
|
+
}
|
|
34
|
+
await this.saveMarks(marks, postId, subjects, current, now);
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
await posts.increment({ id: postId }, 'viewCount', 1);
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
return { counted: true, countedAt: now, viewCount: post.viewCount + 1 };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async read(postId, userId, signal) {
|
|
42
|
+
signal?.throwIfAborted();
|
|
43
|
+
const posts = this.dataSource.getRepository(BlogPostEntity);
|
|
44
|
+
const likes = this.dataSource.getRepository(BlogPostLikeEntity);
|
|
45
|
+
const [post, likeCount, liked] = await Promise.all([
|
|
46
|
+
posts.findOneBy({ id: postId }),
|
|
47
|
+
likes.countBy({ postId }),
|
|
48
|
+
userId ? likes.existsBy({ postId, userId }) : false,
|
|
49
|
+
]);
|
|
50
|
+
signal?.throwIfAborted();
|
|
51
|
+
if (!post)
|
|
52
|
+
throw new BlogNotFoundError('Post not found');
|
|
53
|
+
return { viewCount: post.viewCount, likeCount, liked };
|
|
54
|
+
}
|
|
55
|
+
setLiked(postId, userId, liked, signal) {
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
|
+
return this.dataSource.transaction(async (manager) => {
|
|
58
|
+
const posts = manager.getRepository(BlogPostEntity);
|
|
59
|
+
const post = await posts.findOne({ where: { id: postId }, lock: { mode: 'pessimistic_write' } });
|
|
60
|
+
signal?.throwIfAborted();
|
|
61
|
+
if (!post)
|
|
62
|
+
throw new BlogNotFoundError('Post not found');
|
|
63
|
+
if (post.status !== 'published')
|
|
64
|
+
throw new BlogConflictError('Only published posts accept likes');
|
|
65
|
+
const likes = manager.getRepository(BlogPostLikeEntity);
|
|
66
|
+
const current = await likes.findOneBy({ postId, userId });
|
|
67
|
+
signal?.throwIfAborted();
|
|
68
|
+
if (liked && !current)
|
|
69
|
+
await likes.insert({ postId, userId });
|
|
70
|
+
if (!liked && current)
|
|
71
|
+
await likes.delete({ postId, userId });
|
|
72
|
+
signal?.throwIfAborted();
|
|
73
|
+
const likeCount = await likes.countBy({ postId });
|
|
74
|
+
signal?.throwIfAborted();
|
|
75
|
+
return { liked, likeCount };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
saveMarks(repository, postId, subjects, current, countedAt) {
|
|
79
|
+
const bySubject = new Map(current.map((mark) => [mark.subjectHash, mark]));
|
|
80
|
+
return repository.save(subjects.map((subjectHash) => repository.create({
|
|
81
|
+
...(bySubject.get(subjectHash) ?? {}), postId, subjectHash, countedAt,
|
|
82
|
+
})));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=post-engagement-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-engagement-store.js","sourceRoot":"","sources":["../../src/persistence/post-engagement-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type DataSource } from 'typeorm';
|
|
2
|
+
import { type BlogPostRow, type BlogTagRow } from './entities.js';
|
|
3
|
+
export interface PostInput {
|
|
4
|
+
readonly categoryId: string;
|
|
5
|
+
readonly title: string;
|
|
6
|
+
readonly slug: string;
|
|
7
|
+
readonly summary: string | null;
|
|
8
|
+
readonly coverUrl: string | null;
|
|
9
|
+
readonly content: string;
|
|
10
|
+
readonly tagIds: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export interface PostView extends BlogPostRow {
|
|
13
|
+
readonly tags: readonly BlogTagRow[];
|
|
14
|
+
}
|
|
15
|
+
export interface PostConnections {
|
|
16
|
+
readonly previousPost: PostView | null;
|
|
17
|
+
readonly nextPost: PostView | null;
|
|
18
|
+
readonly relatedPosts: readonly PostView[];
|
|
19
|
+
}
|
|
20
|
+
export interface PostArchiveMonth {
|
|
21
|
+
readonly year: number;
|
|
22
|
+
readonly month: number;
|
|
23
|
+
readonly count: number;
|
|
24
|
+
}
|
|
25
|
+
export interface PostArchivePage {
|
|
26
|
+
readonly items: readonly PostView[];
|
|
27
|
+
readonly months: readonly PostArchiveMonth[];
|
|
28
|
+
readonly nextCursor: string | null;
|
|
29
|
+
readonly total: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class PostStore {
|
|
32
|
+
private readonly dataSource;
|
|
33
|
+
private readonly clock;
|
|
34
|
+
constructor(dataSource: DataSource, clock?: () => Date);
|
|
35
|
+
countPublishedByCategory(signal?: AbortSignal): Promise<ReadonlyMap<string, number>>;
|
|
36
|
+
list(input: {
|
|
37
|
+
limit: number;
|
|
38
|
+
cursor?: string | undefined;
|
|
39
|
+
status?: BlogPostRow['status'] | undefined;
|
|
40
|
+
categoryId?: string | undefined;
|
|
41
|
+
}, signal?: AbortSignal): Promise<{
|
|
42
|
+
items: PostView[];
|
|
43
|
+
nextCursor: string | null;
|
|
44
|
+
}>;
|
|
45
|
+
listPublished(options: {
|
|
46
|
+
limit: number;
|
|
47
|
+
cursor?: string;
|
|
48
|
+
page?: number;
|
|
49
|
+
categorySlug?: string;
|
|
50
|
+
tagSlug?: string;
|
|
51
|
+
}, signal?: AbortSignal): Promise<{
|
|
52
|
+
items: PostView[];
|
|
53
|
+
nextCursor: string | null;
|
|
54
|
+
total: number;
|
|
55
|
+
}>;
|
|
56
|
+
listPublishedArchive(options: {
|
|
57
|
+
limit: number;
|
|
58
|
+
monthLimit: number;
|
|
59
|
+
cursor?: string;
|
|
60
|
+
page?: number;
|
|
61
|
+
year?: number;
|
|
62
|
+
month?: number;
|
|
63
|
+
startYear?: number;
|
|
64
|
+
startMonth?: number;
|
|
65
|
+
endYear?: number;
|
|
66
|
+
endMonth?: number;
|
|
67
|
+
}, signal?: AbortSignal): Promise<PostArchivePage>;
|
|
68
|
+
search(input: {
|
|
69
|
+
query: string;
|
|
70
|
+
limit: number;
|
|
71
|
+
status?: BlogPostRow['status'];
|
|
72
|
+
}, signal?: AbortSignal): Promise<readonly PostView[]>;
|
|
73
|
+
read(id: string, signal?: AbortSignal): Promise<PostView>;
|
|
74
|
+
readPublishedBySlug(slug: string, signal?: AbortSignal): Promise<PostView | undefined>;
|
|
75
|
+
readPublishedByIds(ids: readonly string[], signal?: AbortSignal): Promise<PostView[]>;
|
|
76
|
+
readPublishedConnections(slug: string, relatedLimit: number, signal?: AbortSignal): Promise<PostConnections | undefined>;
|
|
77
|
+
create(input: PostInput, signal?: AbortSignal): Promise<PostView>;
|
|
78
|
+
update(id: string, input: PostInput, signal?: AbortSignal): Promise<PostView>;
|
|
79
|
+
publish(id: string, signal?: AbortSignal): Promise<PostView>;
|
|
80
|
+
archive(id: string, signal?: AbortSignal): Promise<PostView>;
|
|
81
|
+
delete(id: string, signal?: AbortSignal): Promise<void>;
|
|
82
|
+
private assertReferences;
|
|
83
|
+
private replaceTags;
|
|
84
|
+
private withTags;
|
|
85
|
+
private listRelated;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=post-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-store.d.ts","sourceRoot":"","sources":["../../src/persistence/post-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8D,KAAK,UAAU,EAA6C,MAAM,SAAS,CAAC;AAGjJ,OAAO,EAEL,KAAK,WAAW,EAAE,KAAK,UAAU,EAClC,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9D;AACD,MAAM,WAAW,QAAS,SAAQ,WAAW;IAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE;AACtF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,YAAY,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,SAAS,QAAQ,EAAE,CAAC;CAC5C;AACD,MAAM,WAAW,gBAAgB;IAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC3G,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAID,qBAAa,SAAS;IACD,OAAO,CAAC,QAAQ,CAAC,UAAU;IAAc,OAAO,CAAC,QAAQ,CAAC,KAAK;IAAlF,YAAoC,UAAU,EAAE,UAAU,EAAmB,KAAK,aAAmB,EAAI;IAE5F,wBAAwB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAWhG;IAEY,IAAI,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,EAAE,MAAM,CAAC,EAAE,WAAW;;;OAczK;IACY,aAAa,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,MAAM,CAAC,EAAE,WAAW;;;;OAwDnJ;IACY,oBAAoB,CAAC,OAAO,EAAE;QACzC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QACjG,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;KAC9E,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAkEjD;IACY,MAAM,CACjB,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;KAAE,EACvE,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CAe9B;IACY,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,qBAMjD;IACY,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,iCAKlE;IACY,kBAAkB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,uBAQ3E;IACY,wBAAwB,CACnC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAsCtC;IACM,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,WAAW,qBAcnD;IACM,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,WAAW,qBAmB/D;IACM,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,qBAgB9C;IACM,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,qBAe9C;IACM,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,iBAW7C;YAEa,gBAAgB;YAYhB,WAAW;YASX,QAAQ;YAmBR,WAAW;CAiC1B"}
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { And, Equal, ILike, In, LessThan, MoreThan, MoreThanOrEqual } from 'typeorm';
|
|
2
|
+
import { BlogConflictError, BlogCursorError, BlogNotFoundError, PostContentBlockedError } from '../blog/errors.js';
|
|
3
|
+
import { auditPostContent } from '../blog/post-content-audit.js';
|
|
4
|
+
import { BlogCategoryEntity, BlogPostEntity, BlogPostTagEntity, BlogTagEntity, } from './entities.js';
|
|
5
|
+
const ARCHIVE_INDEX_POST_LIMIT = 10_000;
|
|
6
|
+
export class PostStore {
|
|
7
|
+
dataSource;
|
|
8
|
+
clock;
|
|
9
|
+
constructor(dataSource, clock = () => new Date()) {
|
|
10
|
+
this.dataSource = dataSource;
|
|
11
|
+
this.clock = clock;
|
|
12
|
+
}
|
|
13
|
+
async countPublishedByCategory(signal) {
|
|
14
|
+
signal?.throwIfAborted();
|
|
15
|
+
const rows = await this.dataSource.getRepository(BlogPostEntity)
|
|
16
|
+
.createQueryBuilder('post')
|
|
17
|
+
.select('post.categoryId', 'categoryId')
|
|
18
|
+
.addSelect('COUNT(post.id)', 'postCount')
|
|
19
|
+
.where('post.status = :status', { status: 'published' })
|
|
20
|
+
.groupBy('post.categoryId')
|
|
21
|
+
.getRawMany();
|
|
22
|
+
signal?.throwIfAborted();
|
|
23
|
+
return new Map(rows.map(({ categoryId, postCount }) => [categoryId, parsePostCount(postCount)]));
|
|
24
|
+
}
|
|
25
|
+
async list(input, signal) {
|
|
26
|
+
signal?.throwIfAborted();
|
|
27
|
+
const base = { ...(input.status ? { status: input.status } : {}), ...(input.categoryId ? { categoryId: input.categoryId } : {}) };
|
|
28
|
+
const cursor = input.cursor ? await this.dataSource.getRepository(BlogPostEntity).findOne({ where: { id: input.cursor, ...base } }) : undefined;
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
if (input.cursor && !cursor)
|
|
31
|
+
throw new BlogCursorError('Post cursor not found in current filter');
|
|
32
|
+
const where = cursor
|
|
33
|
+
? [{ ...base, createdAt: LessThan(cursor.createdAt) }, { ...base, createdAt: Equal(cursor.createdAt), id: LessThan(cursor.id) }]
|
|
34
|
+
: base;
|
|
35
|
+
const posts = await this.dataSource.getRepository(BlogPostEntity).find({ where, order: { createdAt: 'DESC', id: 'DESC' }, take: input.limit + 1 });
|
|
36
|
+
signal?.throwIfAborted();
|
|
37
|
+
const hasMore = posts.length > input.limit;
|
|
38
|
+
const bounded = hasMore ? posts.slice(0, input.limit) : posts;
|
|
39
|
+
return { items: await this.withTags(bounded, this.dataSource.manager, signal), nextCursor: hasMore ? bounded.at(-1)?.id ?? null : null };
|
|
40
|
+
}
|
|
41
|
+
async listPublished(options, signal) {
|
|
42
|
+
signal?.throwIfAborted();
|
|
43
|
+
let categoryId;
|
|
44
|
+
let tagId;
|
|
45
|
+
if (options.categorySlug) {
|
|
46
|
+
const category = await this.dataSource.getRepository(BlogCategoryEntity).findOneBy({ slug: options.categorySlug });
|
|
47
|
+
signal?.throwIfAborted();
|
|
48
|
+
if (!category)
|
|
49
|
+
return { items: [], nextCursor: null, total: 0 };
|
|
50
|
+
categoryId = category.id;
|
|
51
|
+
}
|
|
52
|
+
if (options.tagSlug) {
|
|
53
|
+
const tag = await this.dataSource.getRepository(BlogTagEntity).findOneBy({ slug: options.tagSlug });
|
|
54
|
+
signal?.throwIfAborted();
|
|
55
|
+
if (!tag)
|
|
56
|
+
return { items: [], nextCursor: null, total: 0 };
|
|
57
|
+
tagId = tag.id;
|
|
58
|
+
}
|
|
59
|
+
const base = {
|
|
60
|
+
status: 'published', ...(categoryId ? { categoryId } : {}),
|
|
61
|
+
...(tagId ? { postTags: { tagId } } : {}),
|
|
62
|
+
};
|
|
63
|
+
const repository = this.dataSource.getRepository(BlogPostEntity);
|
|
64
|
+
const total = await repository.count({ where: base });
|
|
65
|
+
signal?.throwIfAborted();
|
|
66
|
+
if (options.page) {
|
|
67
|
+
const posts = await repository.find({
|
|
68
|
+
where: base,
|
|
69
|
+
order: { publishedAt: 'DESC', id: 'DESC' },
|
|
70
|
+
skip: (options.page - 1) * options.limit,
|
|
71
|
+
take: options.limit,
|
|
72
|
+
});
|
|
73
|
+
signal?.throwIfAborted();
|
|
74
|
+
return {
|
|
75
|
+
items: await this.withTags(posts, this.dataSource.manager, signal),
|
|
76
|
+
nextCursor: options.page * options.limit < total ? posts.at(-1)?.id ?? null : null,
|
|
77
|
+
total,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const cursor = options.cursor
|
|
81
|
+
? await repository.findOne({ where: { id: options.cursor, ...base } })
|
|
82
|
+
: undefined;
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
if (options.cursor && !cursor)
|
|
85
|
+
throw new BlogCursorError('Post cursor not found in current filter');
|
|
86
|
+
if (cursor && !cursor.publishedAt)
|
|
87
|
+
throw new BlogCursorError('Published post cursor is incomplete');
|
|
88
|
+
const where = cursor?.publishedAt
|
|
89
|
+
? [
|
|
90
|
+
{ ...base, publishedAt: LessThan(cursor.publishedAt) },
|
|
91
|
+
{ ...base, publishedAt: Equal(cursor.publishedAt), id: LessThan(cursor.id) },
|
|
92
|
+
]
|
|
93
|
+
: base;
|
|
94
|
+
const posts = await repository.find({
|
|
95
|
+
where, order: { publishedAt: 'DESC', id: 'DESC' }, take: options.limit + 1,
|
|
96
|
+
});
|
|
97
|
+
signal?.throwIfAborted();
|
|
98
|
+
const hasMore = posts.length > options.limit;
|
|
99
|
+
const bounded = hasMore ? posts.slice(0, options.limit) : posts;
|
|
100
|
+
return { items: await this.withTags(bounded, this.dataSource.manager, signal), nextCursor: hasMore ? bounded.at(-1)?.id ?? null : null, total };
|
|
101
|
+
}
|
|
102
|
+
async listPublishedArchive(options, signal) {
|
|
103
|
+
signal?.throwIfAborted();
|
|
104
|
+
const repository = this.dataSource.getRepository(BlogPostEntity);
|
|
105
|
+
const publishedAt = archivePublishedAtRange(options);
|
|
106
|
+
const base = {
|
|
107
|
+
status: 'published',
|
|
108
|
+
...(publishedAt ? { publishedAt } : {}),
|
|
109
|
+
};
|
|
110
|
+
const total = await repository.count({ where: base });
|
|
111
|
+
signal?.throwIfAborted();
|
|
112
|
+
const archiveIndex = await repository.find({
|
|
113
|
+
select: { id: true, publishedAt: true },
|
|
114
|
+
where: { status: 'published' },
|
|
115
|
+
order: { publishedAt: 'DESC', id: 'DESC' },
|
|
116
|
+
take: ARCHIVE_INDEX_POST_LIMIT + 1,
|
|
117
|
+
});
|
|
118
|
+
signal?.throwIfAborted();
|
|
119
|
+
if (archiveIndex.length > ARCHIVE_INDEX_POST_LIMIT) {
|
|
120
|
+
throw new BlogConflictError('Published archive exceeds the supported index size');
|
|
121
|
+
}
|
|
122
|
+
const months = aggregateArchiveMonths(archiveIndex, options.monthLimit);
|
|
123
|
+
if (options.limit === 0)
|
|
124
|
+
return { items: [], months, nextCursor: null, total };
|
|
125
|
+
if (options.page) {
|
|
126
|
+
const posts = await repository.find({
|
|
127
|
+
where: base,
|
|
128
|
+
order: { publishedAt: 'DESC', id: 'DESC' },
|
|
129
|
+
skip: (options.page - 1) * options.limit,
|
|
130
|
+
take: options.limit,
|
|
131
|
+
});
|
|
132
|
+
signal?.throwIfAborted();
|
|
133
|
+
return {
|
|
134
|
+
items: await this.withTags(posts, this.dataSource.manager, signal),
|
|
135
|
+
months,
|
|
136
|
+
nextCursor: options.page * options.limit < total ? posts.at(-1)?.id ?? null : null,
|
|
137
|
+
total,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const cursor = options.cursor
|
|
141
|
+
? await repository.findOne({ where: { id: options.cursor, ...base } })
|
|
142
|
+
: undefined;
|
|
143
|
+
signal?.throwIfAborted();
|
|
144
|
+
if (options.cursor && !cursor)
|
|
145
|
+
throw new BlogCursorError('Post archive cursor not found in current filter');
|
|
146
|
+
if (cursor && !cursor.publishedAt)
|
|
147
|
+
throw new BlogCursorError('Published post archive cursor is incomplete');
|
|
148
|
+
const where = cursor?.publishedAt
|
|
149
|
+
? [
|
|
150
|
+
{
|
|
151
|
+
...base,
|
|
152
|
+
publishedAt: publishedAt
|
|
153
|
+
? And(publishedAt, LessThan(cursor.publishedAt))
|
|
154
|
+
: LessThan(cursor.publishedAt),
|
|
155
|
+
},
|
|
156
|
+
{ ...base, publishedAt: Equal(cursor.publishedAt), id: LessThan(cursor.id) },
|
|
157
|
+
]
|
|
158
|
+
: base;
|
|
159
|
+
const posts = await repository.find({
|
|
160
|
+
where, order: { publishedAt: 'DESC', id: 'DESC' }, take: options.limit + 1,
|
|
161
|
+
});
|
|
162
|
+
signal?.throwIfAborted();
|
|
163
|
+
const hasMore = posts.length > options.limit;
|
|
164
|
+
const bounded = hasMore ? posts.slice(0, options.limit) : posts;
|
|
165
|
+
return {
|
|
166
|
+
items: await this.withTags(bounded, this.dataSource.manager, signal),
|
|
167
|
+
months,
|
|
168
|
+
nextCursor: hasMore ? bounded.at(-1)?.id ?? null : null,
|
|
169
|
+
total,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async search(input, signal) {
|
|
173
|
+
signal?.throwIfAborted();
|
|
174
|
+
const match = ILike(`%${escapeLikePattern(input.query.trim())}%`);
|
|
175
|
+
const base = input.status ? { status: input.status } : {};
|
|
176
|
+
const posts = await this.dataSource.getRepository(BlogPostEntity).find({
|
|
177
|
+
where: [
|
|
178
|
+
{ ...base, title: match },
|
|
179
|
+
{ ...base, summary: match },
|
|
180
|
+
{ ...base, content: match },
|
|
181
|
+
],
|
|
182
|
+
order: { updatedAt: 'DESC', id: 'DESC' },
|
|
183
|
+
take: input.limit,
|
|
184
|
+
});
|
|
185
|
+
signal?.throwIfAborted();
|
|
186
|
+
return this.withTags(posts, this.dataSource.manager, signal);
|
|
187
|
+
}
|
|
188
|
+
async read(id, signal) {
|
|
189
|
+
signal?.throwIfAborted();
|
|
190
|
+
const post = await this.dataSource.getRepository(BlogPostEntity).findOneBy({ id });
|
|
191
|
+
signal?.throwIfAborted();
|
|
192
|
+
if (!post)
|
|
193
|
+
throw new BlogNotFoundError('Post not found');
|
|
194
|
+
return (await this.withTags([post], this.dataSource.manager, signal))[0];
|
|
195
|
+
}
|
|
196
|
+
async readPublishedBySlug(slug, signal) {
|
|
197
|
+
signal?.throwIfAborted();
|
|
198
|
+
const post = await this.dataSource.getRepository(BlogPostEntity).findOneBy({ slug, status: 'published' });
|
|
199
|
+
signal?.throwIfAborted();
|
|
200
|
+
return post ? (await this.withTags([post], this.dataSource.manager, signal))[0] : undefined;
|
|
201
|
+
}
|
|
202
|
+
async readPublishedByIds(ids, signal) {
|
|
203
|
+
signal?.throwIfAborted();
|
|
204
|
+
if (ids.length === 0)
|
|
205
|
+
return [];
|
|
206
|
+
const posts = await this.dataSource.getRepository(BlogPostEntity).find({
|
|
207
|
+
where: { id: In([...new Set(ids)]), status: 'published' },
|
|
208
|
+
});
|
|
209
|
+
signal?.throwIfAborted();
|
|
210
|
+
return this.withTags(posts, this.dataSource.manager, signal);
|
|
211
|
+
}
|
|
212
|
+
async readPublishedConnections(slug, relatedLimit, signal) {
|
|
213
|
+
signal?.throwIfAborted();
|
|
214
|
+
const repository = this.dataSource.getRepository(BlogPostEntity);
|
|
215
|
+
const current = await repository.findOneBy({ slug, status: 'published' });
|
|
216
|
+
signal?.throwIfAborted();
|
|
217
|
+
if (!current?.publishedAt)
|
|
218
|
+
return undefined;
|
|
219
|
+
const previous = await repository.find({
|
|
220
|
+
where: [
|
|
221
|
+
{ status: 'published', publishedAt: LessThan(current.publishedAt) },
|
|
222
|
+
{ status: 'published', publishedAt: Equal(current.publishedAt), id: LessThan(current.id) },
|
|
223
|
+
],
|
|
224
|
+
order: { publishedAt: 'DESC', id: 'DESC' },
|
|
225
|
+
take: 1,
|
|
226
|
+
});
|
|
227
|
+
signal?.throwIfAborted();
|
|
228
|
+
const next = await repository.find({
|
|
229
|
+
where: [
|
|
230
|
+
{ status: 'published', publishedAt: MoreThan(current.publishedAt) },
|
|
231
|
+
{ status: 'published', publishedAt: Equal(current.publishedAt), id: MoreThan(current.id) },
|
|
232
|
+
],
|
|
233
|
+
order: { publishedAt: 'ASC', id: 'ASC' },
|
|
234
|
+
take: 1,
|
|
235
|
+
});
|
|
236
|
+
signal?.throwIfAborted();
|
|
237
|
+
const related = current.id
|
|
238
|
+
? await this.listRelated(current.id, relatedLimit, signal)
|
|
239
|
+
: [];
|
|
240
|
+
signal?.throwIfAborted();
|
|
241
|
+
const adjacentPosts = await this.withTags([
|
|
242
|
+
...(previous[0] ? [previous[0]] : []),
|
|
243
|
+
...(next[0] ? [next[0]] : []),
|
|
244
|
+
], this.dataSource.manager, signal);
|
|
245
|
+
const adjacentById = new Map(adjacentPosts.map((post) => [post.id, post]));
|
|
246
|
+
return {
|
|
247
|
+
previousPost: previous[0] ? adjacentById.get(previous[0].id) ?? null : null,
|
|
248
|
+
nextPost: next[0] ? adjacentById.get(next[0].id) ?? null : null,
|
|
249
|
+
relatedPosts: related,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
create(input, signal) {
|
|
253
|
+
signal?.throwIfAborted();
|
|
254
|
+
return this.dataSource.transaction(async (manager) => {
|
|
255
|
+
await this.assertReferences(manager, input, signal);
|
|
256
|
+
signal?.throwIfAborted();
|
|
257
|
+
const repository = manager.getRepository(BlogPostEntity);
|
|
258
|
+
const post = await repository.save(repository.create({
|
|
259
|
+
...withoutTagIds(input), status: 'draft', publishedAt: null, viewCount: 0,
|
|
260
|
+
}));
|
|
261
|
+
signal?.throwIfAborted();
|
|
262
|
+
await this.replaceTags(manager, post.id, input.tagIds, signal);
|
|
263
|
+
signal?.throwIfAborted();
|
|
264
|
+
return (await this.withTags([post], manager, signal))[0];
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
update(id, input, signal) {
|
|
268
|
+
signal?.throwIfAborted();
|
|
269
|
+
return this.dataSource.transaction(async (manager) => {
|
|
270
|
+
const current = await manager.getRepository(BlogPostEntity).findOne({
|
|
271
|
+
where: { id }, lock: { mode: 'pessimistic_write' },
|
|
272
|
+
});
|
|
273
|
+
signal?.throwIfAborted();
|
|
274
|
+
if (!current)
|
|
275
|
+
throw new BlogNotFoundError('Post not found');
|
|
276
|
+
if (current.status === 'published')
|
|
277
|
+
assertPublishableContent(input);
|
|
278
|
+
await this.assertReferences(manager, input, signal);
|
|
279
|
+
signal?.throwIfAborted();
|
|
280
|
+
await manager.getRepository(BlogPostEntity).update({ id }, withoutTagIds(input));
|
|
281
|
+
signal?.throwIfAborted();
|
|
282
|
+
await this.replaceTags(manager, id, input.tagIds, signal);
|
|
283
|
+
signal?.throwIfAborted();
|
|
284
|
+
const post = await manager.getRepository(BlogPostEntity).findOneByOrFail({ id });
|
|
285
|
+
signal?.throwIfAborted();
|
|
286
|
+
return (await this.withTags([post], manager, signal))[0];
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
publish(id, signal) {
|
|
290
|
+
signal?.throwIfAborted();
|
|
291
|
+
return this.dataSource.transaction(async (manager) => {
|
|
292
|
+
const repository = manager.getRepository(BlogPostEntity);
|
|
293
|
+
const post = await repository.findOne({ where: { id }, lock: { mode: 'pessimistic_write' } });
|
|
294
|
+
signal?.throwIfAborted();
|
|
295
|
+
if (!post)
|
|
296
|
+
throw new BlogNotFoundError('Post not found');
|
|
297
|
+
assertPublishableContent(post);
|
|
298
|
+
if (post.status !== 'published') {
|
|
299
|
+
await repository.update({ id }, { status: 'published', publishedAt: this.clock() });
|
|
300
|
+
signal?.throwIfAborted();
|
|
301
|
+
}
|
|
302
|
+
const updated = await repository.findOneByOrFail({ id });
|
|
303
|
+
signal?.throwIfAborted();
|
|
304
|
+
return (await this.withTags([updated], manager, signal))[0];
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
archive(id, signal) {
|
|
308
|
+
signal?.throwIfAborted();
|
|
309
|
+
return this.dataSource.transaction(async (manager) => {
|
|
310
|
+
const repository = manager.getRepository(BlogPostEntity);
|
|
311
|
+
const post = await repository.findOne({ where: { id }, lock: { mode: 'pessimistic_write' } });
|
|
312
|
+
signal?.throwIfAborted();
|
|
313
|
+
if (!post)
|
|
314
|
+
throw new BlogNotFoundError('Post not found');
|
|
315
|
+
if (post.status !== 'archived') {
|
|
316
|
+
await repository.update({ id }, { status: 'archived' });
|
|
317
|
+
signal?.throwIfAborted();
|
|
318
|
+
}
|
|
319
|
+
const updated = await repository.findOneByOrFail({ id });
|
|
320
|
+
signal?.throwIfAborted();
|
|
321
|
+
return (await this.withTags([updated], manager, signal))[0];
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
delete(id, signal) {
|
|
325
|
+
signal?.throwIfAborted();
|
|
326
|
+
return this.dataSource.transaction(async (manager) => {
|
|
327
|
+
const repository = manager.getRepository(BlogPostEntity);
|
|
328
|
+
const post = await repository.findOne({ where: { id }, lock: { mode: 'pessimistic_write' } });
|
|
329
|
+
signal?.throwIfAborted();
|
|
330
|
+
if (!post)
|
|
331
|
+
throw new BlogNotFoundError('Post not found');
|
|
332
|
+
if (post.status === 'published')
|
|
333
|
+
throw new BlogConflictError('Published post must be archived before deletion');
|
|
334
|
+
await repository.delete({ id });
|
|
335
|
+
signal?.throwIfAborted();
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
async assertReferences(manager, input, signal) {
|
|
339
|
+
signal?.throwIfAborted();
|
|
340
|
+
const category = await manager.getRepository(BlogCategoryEntity).findOneBy({ id: input.categoryId });
|
|
341
|
+
signal?.throwIfAborted();
|
|
342
|
+
if (!category)
|
|
343
|
+
throw new BlogNotFoundError('Category not found');
|
|
344
|
+
if (new Set(input.tagIds).size !== input.tagIds.length)
|
|
345
|
+
throw new BlogConflictError('Post tags must be unique');
|
|
346
|
+
if (input.tagIds.length > 20)
|
|
347
|
+
throw new BlogConflictError('Post cannot contain more than 20 tags');
|
|
348
|
+
if (!input.tagIds.length)
|
|
349
|
+
return;
|
|
350
|
+
const tags = await manager.getRepository(BlogTagEntity).countBy({ id: In([...input.tagIds]) });
|
|
351
|
+
signal?.throwIfAborted();
|
|
352
|
+
if (tags !== input.tagIds.length)
|
|
353
|
+
throw new BlogNotFoundError('Tag not found');
|
|
354
|
+
}
|
|
355
|
+
async replaceTags(manager, postId, tagIds, signal) {
|
|
356
|
+
signal?.throwIfAborted();
|
|
357
|
+
await manager.getRepository(BlogPostTagEntity).delete({ postId });
|
|
358
|
+
signal?.throwIfAborted();
|
|
359
|
+
if (tagIds.length) {
|
|
360
|
+
await manager.getRepository(BlogPostTagEntity).insert(tagIds.map((tagId) => ({ postId, tagId })));
|
|
361
|
+
signal?.throwIfAborted();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async withTags(posts, manager = this.dataSource.manager, signal) {
|
|
365
|
+
signal?.throwIfAborted();
|
|
366
|
+
if (!posts.length)
|
|
367
|
+
return [];
|
|
368
|
+
const postIds = posts.map(({ id }) => id);
|
|
369
|
+
const links = await manager.getRepository(BlogPostTagEntity).findBy({ postId: In(postIds) });
|
|
370
|
+
signal?.throwIfAborted();
|
|
371
|
+
const tagIds = [...new Set(links.map(({ tagId }) => tagId))];
|
|
372
|
+
const tags = tagIds.length ? await manager.getRepository(BlogTagEntity).findBy({ id: In(tagIds) }) : [];
|
|
373
|
+
signal?.throwIfAborted();
|
|
374
|
+
const tagById = new Map(tags.map((tag) => [tag.id, tag]));
|
|
375
|
+
const tagsByPost = new Map();
|
|
376
|
+
for (const link of links) {
|
|
377
|
+
const tag = tagById.get(link.tagId);
|
|
378
|
+
if (!tag)
|
|
379
|
+
continue;
|
|
380
|
+
const current = tagsByPost.get(link.postId) ?? [];
|
|
381
|
+
current.push(tag);
|
|
382
|
+
tagsByPost.set(link.postId, current);
|
|
383
|
+
}
|
|
384
|
+
return posts.map((post) => ({ ...post, tags: tagsByPost.get(post.id) ?? [] }));
|
|
385
|
+
}
|
|
386
|
+
async listRelated(postId, limit, signal) {
|
|
387
|
+
const currentLinks = await this.dataSource.getRepository(BlogPostTagEntity).findBy({ postId });
|
|
388
|
+
signal?.throwIfAborted();
|
|
389
|
+
if (!currentLinks.length)
|
|
390
|
+
return [];
|
|
391
|
+
const tagIds = currentLinks.map(({ tagId }) => tagId);
|
|
392
|
+
const ranked = await this.dataSource.getRepository(BlogPostTagEntity)
|
|
393
|
+
.createQueryBuilder('postTag')
|
|
394
|
+
.select('postTag.postId', 'postId')
|
|
395
|
+
.addSelect('COUNT(postTag.tagId)', 'sharedTagCount')
|
|
396
|
+
.innerJoin('postTag.post', 'post')
|
|
397
|
+
.where('post.status = :status', { status: 'published' })
|
|
398
|
+
.andWhere('post.id != :postId', { postId })
|
|
399
|
+
.andWhere('postTag.tagId IN (:...tagIds)', { tagIds })
|
|
400
|
+
.groupBy('postTag.postId')
|
|
401
|
+
.addGroupBy('post.publishedAt')
|
|
402
|
+
.addGroupBy('post.id')
|
|
403
|
+
.orderBy('sharedTagCount', 'DESC')
|
|
404
|
+
.addOrderBy('post.publishedAt', 'DESC')
|
|
405
|
+
.addOrderBy('post.id', 'DESC')
|
|
406
|
+
.limit(limit)
|
|
407
|
+
.getRawMany();
|
|
408
|
+
signal?.throwIfAborted();
|
|
409
|
+
const ids = ranked.map(({ postId: id }) => id);
|
|
410
|
+
if (!ids.length)
|
|
411
|
+
return [];
|
|
412
|
+
const posts = await this.dataSource.getRepository(BlogPostEntity).findBy({ id: In(ids) });
|
|
413
|
+
signal?.throwIfAborted();
|
|
414
|
+
const byId = new Map(posts.map((post) => [post.id, post]));
|
|
415
|
+
return this.withTags(ids.flatMap((id) => byId.get(id) ? [byId.get(id)] : []), this.dataSource.manager, signal);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function parsePostCount(value) {
|
|
419
|
+
const count = Number(value);
|
|
420
|
+
if (!Number.isSafeInteger(count) || count < 0)
|
|
421
|
+
throw new Error('Published post count is invalid');
|
|
422
|
+
return count;
|
|
423
|
+
}
|
|
424
|
+
function assertPublishableContent(input) {
|
|
425
|
+
const report = auditPostContent(input);
|
|
426
|
+
if (report.publishable)
|
|
427
|
+
return;
|
|
428
|
+
throw new PostContentBlockedError(report);
|
|
429
|
+
}
|
|
430
|
+
function escapeLikePattern(value) {
|
|
431
|
+
return value.replace(/[\\%_]/g, (character) => `\\${character}`);
|
|
432
|
+
}
|
|
433
|
+
function archivePublishedAtRange(options) {
|
|
434
|
+
const isRange = options.startYear !== undefined && options.startMonth !== undefined
|
|
435
|
+
&& options.endYear !== undefined && options.endMonth !== undefined;
|
|
436
|
+
if (!isRange && options.year === undefined)
|
|
437
|
+
return undefined;
|
|
438
|
+
const startYear = isRange ? options.startYear : options.year;
|
|
439
|
+
const startMonth = isRange ? options.startMonth - 1 : options.month === undefined ? 0 : options.month - 1;
|
|
440
|
+
const selectedEndYear = isRange ? options.endYear : options.year;
|
|
441
|
+
const selectedEndMonth = isRange ? options.endMonth : options.month;
|
|
442
|
+
const endYear = selectedEndMonth === undefined || selectedEndMonth === 12
|
|
443
|
+
? selectedEndYear + 1
|
|
444
|
+
: selectedEndYear;
|
|
445
|
+
const endMonth = selectedEndMonth === undefined || selectedEndMonth === 12 ? 0 : selectedEndMonth;
|
|
446
|
+
return And(MoreThanOrEqual(new Date(Date.UTC(startYear, startMonth, 1))), LessThan(new Date(Date.UTC(endYear, endMonth, 1))));
|
|
447
|
+
}
|
|
448
|
+
function aggregateArchiveMonths(posts, limit) {
|
|
449
|
+
const counts = new Map();
|
|
450
|
+
for (const post of posts) {
|
|
451
|
+
if (!post.publishedAt)
|
|
452
|
+
throw new BlogConflictError('Published post archive timestamp is incomplete');
|
|
453
|
+
const year = post.publishedAt.getUTCFullYear();
|
|
454
|
+
const month = post.publishedAt.getUTCMonth() + 1;
|
|
455
|
+
const key = `${year}-${month}`;
|
|
456
|
+
const current = counts.get(key);
|
|
457
|
+
counts.set(key, { year, month, count: (current?.count ?? 0) + 1 });
|
|
458
|
+
}
|
|
459
|
+
return [...counts.values()].slice(0, limit);
|
|
460
|
+
}
|
|
461
|
+
function withoutTagIds(input) {
|
|
462
|
+
const { tagIds: _tagIds, ...post } = input;
|
|
463
|
+
return post;
|
|
464
|
+
}
|
|
465
|
+
//# sourceMappingURL=post-store.js.map
|