create-feedbax 0.1.0 → 0.2.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/LICENSE +201 -21
- package/README.md +11 -1
- package/dist/cli.js +1261 -26
- package/dist/index.js +1402 -160
- package/package.json +40 -14
- package/template/.feedbax-template.sha256 +1 -0
- package/template/.gitignore.template +6 -0
- package/template/.npmignore.template +5 -0
- package/template/.prettierignore.template +4 -0
- package/template/LICENSE +201 -0
- package/template/components.json +25 -0
- package/template/eslint.config.mjs +25 -0
- package/template/package.json +70 -14
- package/template/public/feedbax-mark.svg +4 -0
- package/template/public/robots.txt +3 -0
- package/template/src/authorized-draft-post.test.tsx +212 -0
- package/template/src/authorized-draft-post.tsx +204 -0
- package/template/src/authorized-draft-query.ts +74 -0
- package/template/src/browser-post-state.test.ts +120 -0
- package/template/src/browser-post-state.ts +134 -0
- package/template/src/browser-vote-state.test.ts +14 -0
- package/template/src/browser-vote-state.ts +27 -0
- package/template/src/capability-authorization-error.ts +3 -0
- package/template/src/changelog-page.test.tsx +139 -0
- package/template/src/changelog-page.tsx +219 -0
- package/template/src/changelog-query.ts +13 -0
- package/template/src/changelog-runtime.ts +27 -0
- package/template/src/cloudflare-turnstile.ts +31 -0
- package/template/src/cloudflare-workers-test.ts +3 -0
- package/template/src/cloudflare-workers.d.ts +18 -0
- package/template/src/comment-creation.test.tsx +270 -0
- package/template/src/comment-profile.ts +5 -0
- package/template/src/comment-thread-page.ts +18 -0
- package/template/src/components/command-palette-context.tsx +71 -0
- package/template/src/components/command-palette.tsx +246 -0
- package/template/src/components/create-post-trigger.tsx +44 -0
- package/template/src/components/device-profile-provider.tsx +309 -0
- package/template/src/components/header.tsx +95 -0
- package/template/src/components/loader.tsx +10 -0
- package/template/src/components/profile-menu.tsx +89 -0
- package/template/src/components/ui/components/alert-dialog.tsx +185 -0
- package/template/src/components/ui/components/attachment.tsx +207 -0
- package/template/src/components/ui/components/avatar.tsx +107 -0
- package/template/src/components/ui/components/badge.tsx +52 -0
- package/template/src/components/ui/components/bubble.tsx +128 -0
- package/template/src/components/ui/components/button-group.tsx +87 -0
- package/template/src/components/ui/components/button.tsx +56 -0
- package/template/src/components/ui/components/card.tsx +103 -0
- package/template/src/components/ui/components/checkbox.tsx +26 -0
- package/template/src/components/ui/components/command.tsx +194 -0
- package/template/src/components/ui/components/dialog.tsx +154 -0
- package/template/src/components/ui/components/drawer.tsx +229 -0
- package/template/src/components/ui/components/dropdown-menu.tsx +270 -0
- package/template/src/components/ui/components/empty.tsx +101 -0
- package/template/src/components/ui/components/field.tsx +236 -0
- package/template/src/components/ui/components/input-group.tsx +155 -0
- package/template/src/components/ui/components/input.tsx +20 -0
- package/template/src/components/ui/components/item.tsx +201 -0
- package/template/src/components/ui/components/kbd.tsx +26 -0
- package/template/src/components/ui/components/label.tsx +18 -0
- package/template/src/components/ui/components/marker.tsx +71 -0
- package/template/src/components/ui/components/message-scroller.tsx +128 -0
- package/template/src/components/ui/components/message.tsx +92 -0
- package/template/src/components/ui/components/native-select.tsx +65 -0
- package/template/src/components/ui/components/popover.tsx +88 -0
- package/template/src/components/ui/components/portal-primitives.test.tsx +85 -0
- package/template/src/components/ui/components/radio-group.tsx +36 -0
- package/template/src/components/ui/components/scroll-area.tsx +52 -0
- package/template/src/components/ui/components/secondary-form-actions.test.tsx +23 -0
- package/template/src/components/ui/components/select.tsx +199 -0
- package/template/src/components/ui/components/separator.tsx +23 -0
- package/template/src/components/ui/components/sheet.tsx +135 -0
- package/template/src/components/ui/components/skeleton.tsx +13 -0
- package/template/src/components/ui/components/sonner.tsx +43 -0
- package/template/src/components/ui/components/spinner.tsx +16 -0
- package/template/src/components/ui/components/tabs.tsx +80 -0
- package/template/src/components/ui/components/textarea.tsx +18 -0
- package/template/src/components/ui/components/tooltip.tsx +64 -0
- package/template/src/components/ui/hooks/.gitkeep +0 -0
- package/template/src/components/ui/lib/utils.ts +6 -0
- package/template/src/components/ui/styles/globals.css +151 -0
- package/template/src/create-post-overlay.tsx +82 -0
- package/template/src/draft-post-list.tsx +39 -0
- package/template/src/feedback-cache-invalidation.test.ts +185 -0
- package/template/src/feedback-cache-invalidation.ts +97 -0
- package/template/src/feedback-runtime.ts +38 -0
- package/template/src/feedbax/changelog/index.test.ts +156 -0
- package/template/src/feedbax/changelog/index.ts +206 -0
- package/template/src/feedbax/changelog/notion-changelog.test.ts +395 -0
- package/template/src/feedbax/changelog/notion-changelog.ts +400 -0
- package/template/src/feedbax/config/index.test.ts +111 -0
- package/template/src/feedbax/config/index.ts +271 -0
- package/template/src/feedbax/feedback/feedback-storage.ts +43 -0
- package/template/src/feedbax/feedback/index.test.ts +900 -0
- package/template/src/feedbax/feedback/index.ts +942 -0
- package/template/src/feedbax/feedback/notion-comments.test.ts +171 -0
- package/template/src/feedbax/feedback/notion-comments.ts +231 -0
- package/template/src/feedbax/feedback/notion-data-source.test.ts +364 -0
- package/template/src/feedbax/feedback/notion-data-source.ts +458 -0
- package/template/src/feedbax/feedback/notion-feedback.test.ts +998 -0
- package/template/src/feedbax/feedback/notion-feedback.ts +651 -0
- package/template/src/feedbax/feedback/notion-request.ts +74 -0
- package/template/src/feedbax.test-fixture.ts +64 -0
- package/template/src/feedbax.ts +69 -0
- package/template/src/index.css +2138 -0
- package/template/src/lazy-create-post-overlay.tsx +6 -0
- package/template/src/lazy-post-detail-overlay.tsx +10 -0
- package/template/src/masked-post-link.tsx +27 -0
- package/template/src/overlay-post-detail.test.tsx +130 -0
- package/template/src/overlay-post-detail.tsx +92 -0
- package/template/src/portal-comments.test.ts +375 -0
- package/template/src/portal-comments.ts +384 -0
- package/template/src/portal-features.test.tsx +150 -0
- package/template/src/portal-feedback-form.tsx +515 -0
- package/template/src/portal-feedback-runtime.test.ts +32 -0
- package/template/src/portal-feedback-runtime.ts +55 -0
- package/template/src/portal-feedback-server-function.ts +93 -0
- package/template/src/portal-feedback-submission.test.ts +307 -0
- package/template/src/portal-feedback-submission.ts +214 -0
- package/template/src/portal-head.ts +58 -0
- package/template/src/portal-server-entry.ts +64 -0
- package/template/src/portal-voting.test.ts +106 -0
- package/template/src/portal-voting.ts +149 -0
- package/template/src/post-badges.tsx +27 -0
- package/template/src/post-creation-flow.test.tsx +230 -0
- package/template/src/post-creation-flow.tsx +88 -0
- package/template/src/post-detail-overlay.test.tsx +114 -0
- package/template/src/post-detail-overlay.tsx +67 -0
- package/template/src/post-feed-controls.test.tsx +239 -0
- package/template/src/post-feed-controls.tsx +298 -0
- package/template/src/post-presentation.ts +37 -0
- package/template/src/post-queries.test.ts +158 -0
- package/template/src/post-queries.ts +26 -0
- package/template/src/post-results.tsx +165 -0
- package/template/src/public-cache-policy.test.ts +141 -0
- package/template/src/public-cache-policy.ts +119 -0
- package/template/src/public-cache-tags.ts +7 -0
- package/template/src/public-changelog-page.ts +15 -0
- package/template/src/public-changelog-server-function.ts +27 -0
- package/template/src/public-date.ts +11 -0
- package/template/src/public-failure-state.test.tsx +69 -0
- package/template/src/public-feedback-index.tsx +124 -0
- package/template/src/public-feedback-page.test.tsx +290 -0
- package/template/src/public-feedback-page.ts +75 -0
- package/template/src/public-feedback-server-function.ts +22 -0
- package/template/src/public-portal-error.tsx +27 -0
- package/template/src/public-post-detail.tsx +682 -0
- package/template/src/public-post-page.test.tsx +173 -0
- package/template/src/public-post-page.ts +15 -0
- package/template/src/public-post-route.test.tsx +214 -0
- package/template/src/public-post-route.tsx +171 -0
- package/template/src/public-post-server-function.ts +48 -0
- package/template/src/public-post-unavailable.tsx +10 -0
- package/template/src/public-roadmap-page.test.tsx +139 -0
- package/template/src/public-roadmap-page.ts +24 -0
- package/template/src/public-roadmap-route.test.tsx +218 -0
- package/template/src/public-roadmap-route.tsx +50 -0
- package/template/src/public-roadmap-server-function.ts +44 -0
- package/template/src/public-roadmap-view.tsx +378 -0
- package/template/src/public-shell.test.tsx +518 -0
- package/template/src/roadmap-query.ts +21 -0
- package/template/src/root-branding.test.ts +53 -0
- package/template/src/routeTree.gen.ts +131 -0
- package/template/src/routed-post-creation-form.tsx +28 -0
- package/template/src/router.tsx +25 -0
- package/template/src/routes/__root.tsx +116 -0
- package/template/src/routes/changelog.tsx +39 -0
- package/template/src/routes/index.tsx +61 -0
- package/template/src/routes/p.$slug.tsx +16 -0
- package/template/src/routes/roadmap.tsx +11 -0
- package/template/src/routes/submit.tsx +28 -0
- package/template/src/safe-public-failure.ts +45 -0
- package/template/src/server.ts +111 -0
- package/template/src/similar-posts.tsx +45 -0
- package/template/src/submit-route.test.tsx +134 -0
- package/template/src/trusted-feedback-handler.test.ts +425 -0
- package/template/src/trusted-feedback-handler.ts +181 -0
- package/template/src/trusted-feedback-runtime.ts +17 -0
- package/template/src/turnstile-configuration.test.ts +37 -0
- package/template/src/turnstile-configuration.ts +19 -0
- package/template/src/use-media-query.ts +17 -0
- package/template/src/vote-toggle.test.tsx +171 -0
- package/template/src/vote-toggle.tsx +211 -0
- package/template/tsconfig.json +45 -0
- package/template/vite.config.ts +20 -0
- package/template/vitest.config.ts +4 -0
- package/template/vitest.setup.ts +3 -0
- package/template/wrangler.jsonc +35 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts +0 -21
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/template/.env.example +0 -4
- package/template/README.md +0 -7
- package/template/apps/portal/components.json +0 -20
- package/template/apps/portal/package.json +0 -31
- package/template/apps/portal/src/lib/utils.ts +0 -6
- package/template/apps/portal/src/routeTree.gen.ts +0 -59
- package/template/apps/portal/src/router.tsx +0 -10
- package/template/apps/portal/src/routes/__root.tsx +0 -31
- package/template/apps/portal/src/routes/index.tsx +0 -35
- package/template/apps/portal/src/styles.css +0 -91
- package/template/apps/portal/tsconfig.json +0 -15
- package/template/apps/portal/vite.config.ts +0 -13
- package/template/feedbax.config.ts +0 -13
- package/template/feedbax.jsonc +0 -11
- package/template/pnpm-workspace.yaml +0 -3
- package/template/turbo.json +0 -8
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createNotionCommentStorage } from "./notion-comments";
|
|
4
|
+
|
|
5
|
+
describe("Notion Comment HTTP boundary", () => {
|
|
6
|
+
it("creates native page Comments and discussion replies with a custom display name", async () => {
|
|
7
|
+
const request = vi
|
|
8
|
+
.fn<typeof fetch>()
|
|
9
|
+
.mockResolvedValueOnce(
|
|
10
|
+
Response.json(
|
|
11
|
+
notionComment({
|
|
12
|
+
id: "created-comment",
|
|
13
|
+
display_name: { type: "custom", resolved_name: "Ari" },
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
.mockResolvedValueOnce(
|
|
18
|
+
Response.json(
|
|
19
|
+
notionComment({
|
|
20
|
+
id: "created-reply",
|
|
21
|
+
display_name: { type: "custom", resolved_name: "Ari" },
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
const comments = createNotionCommentStorage({
|
|
26
|
+
token: "notion-token",
|
|
27
|
+
dataSourceId: "feedback",
|
|
28
|
+
propertyIds: {} as never,
|
|
29
|
+
request,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await comments.createPageComment({
|
|
33
|
+
postId: "page-1",
|
|
34
|
+
body: "A public message",
|
|
35
|
+
displayName: "Ari",
|
|
36
|
+
});
|
|
37
|
+
await comments.createDiscussionReply({
|
|
38
|
+
postId: "page-1",
|
|
39
|
+
discussionId: "discussion-1",
|
|
40
|
+
body: "A reply",
|
|
41
|
+
displayName: "Ari",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(
|
|
45
|
+
request.mock.calls.map((call) => JSON.parse(String(call[1]?.body))),
|
|
46
|
+
).toEqual([
|
|
47
|
+
{
|
|
48
|
+
parent: { page_id: "page-1" },
|
|
49
|
+
rich_text: [{ type: "text", text: { content: "A public message" } }],
|
|
50
|
+
display_name: { type: "custom", custom: { name: "Ari" } },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
discussion_id: "discussion-1",
|
|
54
|
+
rich_text: [{ type: "text", text: { content: "A reply" } }],
|
|
55
|
+
display_name: { type: "custom", custom: { name: "Ari" } },
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
it("requests 50 page Comments, preserves the cursor, and classifies public authors", async () => {
|
|
60
|
+
const request = vi.fn<typeof fetch>().mockResolvedValue(
|
|
61
|
+
Response.json({
|
|
62
|
+
results: [
|
|
63
|
+
notionComment({
|
|
64
|
+
id: "participant",
|
|
65
|
+
display_name: { type: "custom", resolved_name: "Ari" },
|
|
66
|
+
}),
|
|
67
|
+
notionComment({ id: "team", discussion_id: "discussion-1" }),
|
|
68
|
+
notionComment({ id: "resolved", resolved: true }),
|
|
69
|
+
],
|
|
70
|
+
has_more: true,
|
|
71
|
+
next_cursor: "next-native-cursor",
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
const comments = createNotionCommentStorage({
|
|
75
|
+
token: "notion-token",
|
|
76
|
+
dataSourceId: "feedback",
|
|
77
|
+
propertyIds: {} as never,
|
|
78
|
+
request,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await expect(
|
|
82
|
+
comments.listPageComments("page-1", "start-native-cursor"),
|
|
83
|
+
).resolves.toMatchObject({
|
|
84
|
+
items: [
|
|
85
|
+
{ author: { kind: "participant", displayName: "Ari" }, scope: "page" },
|
|
86
|
+
{
|
|
87
|
+
author: { kind: "product-team", displayName: "Product Team" },
|
|
88
|
+
scope: "page",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
nextCursor: "next-native-cursor",
|
|
92
|
+
});
|
|
93
|
+
expect(String(request.mock.calls[0]?.[0])).toBe(
|
|
94
|
+
"https://api.notion.com/v1/comments?block_id=page-1&page_size=50&start_cursor=start-native-cursor",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns actionable repair instructions for missing read permission", async () => {
|
|
99
|
+
const comments = createNotionCommentStorage({
|
|
100
|
+
token: "notion-token",
|
|
101
|
+
dataSourceId: "feedback",
|
|
102
|
+
propertyIds: {} as never,
|
|
103
|
+
request: vi
|
|
104
|
+
.fn<typeof fetch>()
|
|
105
|
+
.mockResolvedValue(new Response(null, { status: 403 })),
|
|
106
|
+
});
|
|
107
|
+
await expect(comments.listPageComments("page-1")).rejects.toThrow(
|
|
108
|
+
"Enable read comment content for the connection, then retry.",
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("updates and deletes native Comments", async () => {
|
|
113
|
+
const request = vi
|
|
114
|
+
.fn<typeof fetch>()
|
|
115
|
+
.mockResolvedValueOnce(Response.json(notionComment({ id: "comment-1" })))
|
|
116
|
+
.mockResolvedValueOnce(new Response(null, { status: 204 }));
|
|
117
|
+
const comments = createNotionCommentStorage({
|
|
118
|
+
token: "notion-token",
|
|
119
|
+
dataSourceId: "feedback",
|
|
120
|
+
propertyIds: {} as never,
|
|
121
|
+
request,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await comments.updateComment("comment-1", "Corrected message");
|
|
125
|
+
await comments.deleteComment("comment-1");
|
|
126
|
+
|
|
127
|
+
expect(String(request.mock.calls[0]?.[0])).toBe(
|
|
128
|
+
"https://api.notion.com/v1/comments/comment-1",
|
|
129
|
+
);
|
|
130
|
+
expect(request.mock.calls[0]?.[1]).toMatchObject({
|
|
131
|
+
method: "PATCH",
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
rich_text: [{ type: "text", text: { content: "Corrected message" } }],
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
expect(request.mock.calls[1]).toMatchObject([
|
|
137
|
+
"https://api.notion.com/v1/comments/comment-1",
|
|
138
|
+
expect.objectContaining({ method: "DELETE" }),
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns actionable permission failures for native Comment mutations", async () => {
|
|
143
|
+
const comments = createNotionCommentStorage({
|
|
144
|
+
token: "notion-token",
|
|
145
|
+
dataSourceId: "feedback",
|
|
146
|
+
propertyIds: {} as never,
|
|
147
|
+
request: vi
|
|
148
|
+
.fn<typeof fetch>()
|
|
149
|
+
.mockResolvedValue(new Response(null, { status: 403 })),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await expect(
|
|
153
|
+
comments.updateComment("comment-1", "Updated"),
|
|
154
|
+
).rejects.toThrow("permission to update this Comment");
|
|
155
|
+
await expect(comments.deleteComment("comment-1")).rejects.toThrow(
|
|
156
|
+
"permission to delete this Comment",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
function notionComment(overrides: Record<string, unknown>) {
|
|
162
|
+
return {
|
|
163
|
+
object: "comment",
|
|
164
|
+
id: "comment-1",
|
|
165
|
+
parent: { type: "page_id", page_id: "page-1" },
|
|
166
|
+
discussion_id: "discussion-1",
|
|
167
|
+
created_time: "2026-08-20T13:00:00.000Z",
|
|
168
|
+
rich_text: [{ plain_text: "A public message" }],
|
|
169
|
+
...overrides,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { CommentAuthor } from "./index";
|
|
2
|
+
import type { NotionFeedbackStorageOptions } from "./notion-feedback";
|
|
3
|
+
import { requestNotion } from "./notion-request";
|
|
4
|
+
|
|
5
|
+
const NOTION_API_URL = "https://api.notion.com/v1";
|
|
6
|
+
const NOTION_API_VERSION = "2026-03-11";
|
|
7
|
+
|
|
8
|
+
export interface StoredComment {
|
|
9
|
+
id: string;
|
|
10
|
+
postId: string;
|
|
11
|
+
discussionId: string;
|
|
12
|
+
body: string;
|
|
13
|
+
author: CommentAuthor;
|
|
14
|
+
createdAt: Date;
|
|
15
|
+
resolved: boolean;
|
|
16
|
+
scope?: "page" | "block";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CommentStorage {
|
|
20
|
+
createPageComment(input: {
|
|
21
|
+
postId: string;
|
|
22
|
+
body: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
}): Promise<StoredComment>;
|
|
25
|
+
createDiscussionReply(input: {
|
|
26
|
+
postId: string;
|
|
27
|
+
discussionId: string;
|
|
28
|
+
body: string;
|
|
29
|
+
displayName: string;
|
|
30
|
+
}): Promise<StoredComment>;
|
|
31
|
+
listPageComments(
|
|
32
|
+
postId: string,
|
|
33
|
+
cursor?: string,
|
|
34
|
+
): Promise<{
|
|
35
|
+
items: StoredComment[];
|
|
36
|
+
nextCursor?: string;
|
|
37
|
+
}>;
|
|
38
|
+
updateComment(commentId: string, body: string): Promise<StoredComment>;
|
|
39
|
+
deleteComment(commentId: string): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createNotionCommentStorage({
|
|
43
|
+
token,
|
|
44
|
+
request = fetch,
|
|
45
|
+
retry,
|
|
46
|
+
}: NotionFeedbackStorageOptions): CommentStorage {
|
|
47
|
+
return {
|
|
48
|
+
createPageComment: (input) =>
|
|
49
|
+
createComment({
|
|
50
|
+
parent: { page_id: input.postId },
|
|
51
|
+
body: input.body,
|
|
52
|
+
displayName: input.displayName,
|
|
53
|
+
postId: input.postId,
|
|
54
|
+
}),
|
|
55
|
+
createDiscussionReply: (input) =>
|
|
56
|
+
createComment({
|
|
57
|
+
discussion_id: input.discussionId,
|
|
58
|
+
body: input.body,
|
|
59
|
+
displayName: input.displayName,
|
|
60
|
+
postId: input.postId,
|
|
61
|
+
}),
|
|
62
|
+
async listPageComments(postId, cursor) {
|
|
63
|
+
const url = new URL(`${NOTION_API_URL}/comments`);
|
|
64
|
+
url.searchParams.set("block_id", postId);
|
|
65
|
+
url.searchParams.set("page_size", "50");
|
|
66
|
+
if (cursor) url.searchParams.set("start_cursor", cursor);
|
|
67
|
+
const response = await requestNotion(
|
|
68
|
+
url,
|
|
69
|
+
{
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${token}`,
|
|
72
|
+
"Notion-Version": NOTION_API_VERSION,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{ request, retry, operation: "idempotent" },
|
|
76
|
+
);
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
throw new Error(
|
|
79
|
+
response.status === 403
|
|
80
|
+
? "Notion cannot read Comments. Enable read comment content for the connection, then retry."
|
|
81
|
+
: `Notion rejected the Comment request (${response.status}).`,
|
|
82
|
+
);
|
|
83
|
+
const value: unknown = await response.json();
|
|
84
|
+
if (!isRecord(value) || !Array.isArray(value.results))
|
|
85
|
+
throw new Error("Notion returned an invalid Comment list.");
|
|
86
|
+
const items = value.results
|
|
87
|
+
.map((entry) => commentFromNotion(entry, postId))
|
|
88
|
+
.filter((comment) => !comment.resolved);
|
|
89
|
+
const nextCursor =
|
|
90
|
+
value.has_more === true ? value.next_cursor : undefined;
|
|
91
|
+
if (nextCursor !== undefined && typeof nextCursor !== "string")
|
|
92
|
+
throw new Error("Notion returned an invalid Comment cursor.");
|
|
93
|
+
return { items, ...(nextCursor ? { nextCursor } : {}) };
|
|
94
|
+
},
|
|
95
|
+
async updateComment(commentId, body) {
|
|
96
|
+
const response = await requestNotion(
|
|
97
|
+
`${NOTION_API_URL}/comments/${encodeURIComponent(commentId)}`,
|
|
98
|
+
{
|
|
99
|
+
method: "PATCH",
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: `Bearer ${token}`,
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
"Notion-Version": NOTION_API_VERSION,
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
rich_text: [{ type: "text", text: { content: body } }],
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
{ request, retry, operation: "idempotent" },
|
|
110
|
+
);
|
|
111
|
+
if (!response.ok) throw commentMutationFailure(response.status, "update");
|
|
112
|
+
return commentFromNotion(await response.json(), "");
|
|
113
|
+
},
|
|
114
|
+
async deleteComment(commentId) {
|
|
115
|
+
const response = await requestNotion(
|
|
116
|
+
`${NOTION_API_URL}/comments/${encodeURIComponent(commentId)}`,
|
|
117
|
+
{
|
|
118
|
+
method: "DELETE",
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Bearer ${token}`,
|
|
121
|
+
"Notion-Version": NOTION_API_VERSION,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{ request, retry, operation: "idempotent" },
|
|
125
|
+
);
|
|
126
|
+
if (!response.ok) throw commentMutationFailure(response.status, "delete");
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
async function createComment(input: {
|
|
131
|
+
parent?: { page_id: string };
|
|
132
|
+
discussion_id?: string;
|
|
133
|
+
body: string;
|
|
134
|
+
displayName: string;
|
|
135
|
+
postId: string;
|
|
136
|
+
}): Promise<StoredComment> {
|
|
137
|
+
const response = await requestNotion(
|
|
138
|
+
`${NOTION_API_URL}/comments`,
|
|
139
|
+
{
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${token}`,
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
"Notion-Version": NOTION_API_VERSION,
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
...(input.parent ? { parent: input.parent } : {}),
|
|
148
|
+
...(input.discussion_id
|
|
149
|
+
? { discussion_id: input.discussion_id }
|
|
150
|
+
: {}),
|
|
151
|
+
rich_text: [{ type: "text", text: { content: input.body } }],
|
|
152
|
+
display_name: {
|
|
153
|
+
type: "custom",
|
|
154
|
+
custom: { name: input.displayName },
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
{ request, retry, operation: "create" },
|
|
159
|
+
);
|
|
160
|
+
if (!response.ok)
|
|
161
|
+
throw new Error(
|
|
162
|
+
response.status === 403
|
|
163
|
+
? "Notion cannot create Comments. Enable insert comment capability for the connection, then retry."
|
|
164
|
+
: `Notion rejected the Comment creation (${response.status}).`,
|
|
165
|
+
);
|
|
166
|
+
return commentFromNotion(await response.json(), input.postId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function commentMutationFailure(status: number, action: "update" | "delete") {
|
|
171
|
+
return new Error(
|
|
172
|
+
status === 403
|
|
173
|
+
? `Notion does not grant permission to ${action} this Comment. Only Comments created by this connection can be changed.`
|
|
174
|
+
: `Notion rejected the Comment ${action} (${status}). The Comment may have been resolved or deleted.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function commentFromNotion(value: unknown, postId: string): StoredComment {
|
|
179
|
+
if (!isRecord(value)) throw new Error("Notion returned an invalid Comment.");
|
|
180
|
+
const id = requiredString(value.id, "Comment ID");
|
|
181
|
+
const discussionId = requiredString(value.discussion_id, "discussion ID");
|
|
182
|
+
const createdAt = new Date(
|
|
183
|
+
requiredString(value.created_time, "created time"),
|
|
184
|
+
);
|
|
185
|
+
if (Number.isNaN(createdAt.getTime()))
|
|
186
|
+
throw new Error("Notion returned an invalid Comment created time.");
|
|
187
|
+
const parent = value.parent;
|
|
188
|
+
const pageLevel =
|
|
189
|
+
!postId ||
|
|
190
|
+
(isRecord(parent) &&
|
|
191
|
+
parent.type === "page_id" &&
|
|
192
|
+
parent.page_id === postId);
|
|
193
|
+
const richText = Array.isArray(value.rich_text) ? value.rich_text : [];
|
|
194
|
+
const body = richText
|
|
195
|
+
.map((part) =>
|
|
196
|
+
isRecord(part) && typeof part.plain_text === "string"
|
|
197
|
+
? part.plain_text
|
|
198
|
+
: "",
|
|
199
|
+
)
|
|
200
|
+
.join("");
|
|
201
|
+
const displayName = value.display_name;
|
|
202
|
+
const participantName =
|
|
203
|
+
isRecord(displayName) &&
|
|
204
|
+
displayName.type === "custom" &&
|
|
205
|
+
typeof displayName.resolved_name === "string" &&
|
|
206
|
+
displayName.resolved_name.trim()
|
|
207
|
+
? displayName.resolved_name.trim()
|
|
208
|
+
: undefined;
|
|
209
|
+
return {
|
|
210
|
+
id,
|
|
211
|
+
postId,
|
|
212
|
+
discussionId,
|
|
213
|
+
body,
|
|
214
|
+
author: participantName
|
|
215
|
+
? { kind: "participant", displayName: participantName }
|
|
216
|
+
: { kind: "product-team", displayName: "Product Team" },
|
|
217
|
+
createdAt,
|
|
218
|
+
resolved: value.resolved === true,
|
|
219
|
+
scope: pageLevel ? "page" : "block",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
224
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function requiredString(value: unknown, label: string): string {
|
|
228
|
+
if (typeof value !== "string" || !value)
|
|
229
|
+
throw new Error(`Notion returned an invalid ${label}.`);
|
|
230
|
+
return value;
|
|
231
|
+
}
|