create-feedbax 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +201 -21
  2. package/README.md +11 -1
  3. package/dist/cli.js +3283 -22
  4. package/dist/index.js +3394 -160
  5. package/package.json +40 -14
  6. package/template/.feedbax-template.sha256 +1 -0
  7. package/template/.gitignore.template +6 -0
  8. package/template/.npmignore.template +5 -0
  9. package/template/.prettierignore.template +4 -0
  10. package/template/AGENTS.md +13 -0
  11. package/template/CONNECTED.md +404 -0
  12. package/template/LICENSE +201 -0
  13. package/template/README.md +56 -5
  14. package/template/components.json +25 -0
  15. package/template/eslint.config.mjs +48 -0
  16. package/template/migrations/0001_connected.sql +6 -0
  17. package/template/migrations/0002_participation.sql +8 -0
  18. package/template/migrations/0003_images.sql +3 -0
  19. package/template/migrations/0004_followed_status.sql +9 -0
  20. package/template/package.json +74 -14
  21. package/template/public/feedbax-mark.svg +4 -0
  22. package/template/public/robots.txt +3 -0
  23. package/template/src/authorized-draft-post.test.tsx +212 -0
  24. package/template/src/authorized-draft-post.tsx +205 -0
  25. package/template/src/authorized-draft-query.ts +74 -0
  26. package/template/src/browser-post-state.test.ts +120 -0
  27. package/template/src/browser-post-state.ts +134 -0
  28. package/template/src/browser-vote-state.test.ts +14 -0
  29. package/template/src/browser-vote-state.ts +27 -0
  30. package/template/src/capability-authorization-error.ts +3 -0
  31. package/template/src/changelog-page.test.tsx +139 -0
  32. package/template/src/changelog-page.tsx +228 -0
  33. package/template/src/changelog-query.ts +13 -0
  34. package/template/src/changelog-runtime.ts +27 -0
  35. package/template/src/cloudflare-turnstile.ts +31 -0
  36. package/template/src/cloudflare-workers-test.ts +3 -0
  37. package/template/src/cloudflare-workers.d.ts +24 -0
  38. package/template/src/comment-creation.test.tsx +311 -0
  39. package/template/src/comment-profile.ts +5 -0
  40. package/template/src/comment-thread-page.ts +28 -0
  41. package/template/src/components/command-palette-context.tsx +71 -0
  42. package/template/src/components/command-palette.tsx +275 -0
  43. package/template/src/components/connected-identity-provider.tsx +108 -0
  44. package/template/src/components/create-post-trigger.tsx +44 -0
  45. package/template/src/components/device-profile-provider.tsx +324 -0
  46. package/template/src/components/follow-post-button.tsx +89 -0
  47. package/template/src/components/header.tsx +97 -0
  48. package/template/src/components/image-attachments.tsx +187 -0
  49. package/template/src/components/loader.tsx +53 -0
  50. package/template/src/components/notification-inbox.tsx +232 -0
  51. package/template/src/components/portal-footer.tsx +14 -0
  52. package/template/src/components/profile-menu.tsx +119 -0
  53. package/template/src/components/ui/components/alert-dialog.tsx +185 -0
  54. package/template/src/components/ui/components/attachment.tsx +207 -0
  55. package/template/src/components/ui/components/avatar.tsx +107 -0
  56. package/template/src/components/ui/components/badge.tsx +52 -0
  57. package/template/src/components/ui/components/bubble.tsx +128 -0
  58. package/template/src/components/ui/components/button-group.tsx +87 -0
  59. package/template/src/components/ui/components/button.tsx +56 -0
  60. package/template/src/components/ui/components/card.tsx +103 -0
  61. package/template/src/components/ui/components/checkbox.tsx +26 -0
  62. package/template/src/components/ui/components/command.tsx +195 -0
  63. package/template/src/components/ui/components/dialog.tsx +154 -0
  64. package/template/src/components/ui/components/drawer.tsx +229 -0
  65. package/template/src/components/ui/components/dropdown-menu.tsx +270 -0
  66. package/template/src/components/ui/components/empty.tsx +101 -0
  67. package/template/src/components/ui/components/field.tsx +236 -0
  68. package/template/src/components/ui/components/input-group.tsx +155 -0
  69. package/template/src/components/ui/components/input.tsx +20 -0
  70. package/template/src/components/ui/components/item.tsx +201 -0
  71. package/template/src/components/ui/components/kbd.tsx +26 -0
  72. package/template/src/components/ui/components/label.tsx +18 -0
  73. package/template/src/components/ui/components/marker.tsx +71 -0
  74. package/template/src/components/ui/components/message-scroller.tsx +128 -0
  75. package/template/src/components/ui/components/message.tsx +92 -0
  76. package/template/src/components/ui/components/native-select.tsx +65 -0
  77. package/template/src/components/ui/components/popover.tsx +88 -0
  78. package/template/src/components/ui/components/portal-primitives.test.tsx +85 -0
  79. package/template/src/components/ui/components/radio-group.tsx +36 -0
  80. package/template/src/components/ui/components/scroll-area.tsx +51 -0
  81. package/template/src/components/ui/components/secondary-form-actions.test.tsx +23 -0
  82. package/template/src/components/ui/components/select.tsx +199 -0
  83. package/template/src/components/ui/components/separator.tsx +23 -0
  84. package/template/src/components/ui/components/sheet.tsx +135 -0
  85. package/template/src/components/ui/components/skeleton.tsx +13 -0
  86. package/template/src/components/ui/components/sonner.tsx +43 -0
  87. package/template/src/components/ui/components/spinner.tsx +16 -0
  88. package/template/src/components/ui/components/switch.tsx +29 -0
  89. package/template/src/components/ui/components/tabs.tsx +80 -0
  90. package/template/src/components/ui/components/textarea.tsx +18 -0
  91. package/template/src/components/ui/components/tooltip.tsx +64 -0
  92. package/template/src/components/ui/hooks/.gitkeep +0 -0
  93. package/template/src/components/ui/lib/utils.ts +6 -0
  94. package/template/src/components/ui/styles/globals.css +151 -0
  95. package/template/src/connected-identity.test.ts +144 -0
  96. package/template/src/connected-identity.ts +122 -0
  97. package/template/src/connected-participation.test.tsx +67 -0
  98. package/template/src/connected-runtime.test.ts +241 -0
  99. package/template/src/connected-runtime.ts +176 -0
  100. package/template/src/connected-social.test.tsx +158 -0
  101. package/template/src/create-post-overlay.tsx +82 -0
  102. package/template/src/draft-post-list.tsx +39 -0
  103. package/template/src/feedback-cache-invalidation.test.ts +185 -0
  104. package/template/src/feedback-cache-invalidation.ts +98 -0
  105. package/template/src/feedback-runtime.ts +105 -0
  106. package/template/src/feedbax/changelog/index.test.ts +156 -0
  107. package/template/src/feedbax/changelog/index.ts +206 -0
  108. package/template/src/feedbax/changelog/notion-changelog.test.ts +395 -0
  109. package/template/src/feedbax/changelog/notion-changelog.ts +400 -0
  110. package/template/src/feedbax/config/index.test.ts +139 -0
  111. package/template/src/feedbax/config/index.ts +364 -0
  112. package/template/src/feedbax/feedback/connected-feedback.ts +206 -0
  113. package/template/src/feedbax/feedback/connected.test.ts +272 -0
  114. package/template/src/feedbax/feedback/connected.ts +215 -0
  115. package/template/src/feedbax/feedback/email-delivery.test.ts +65 -0
  116. package/template/src/feedbax/feedback/email-delivery.ts +66 -0
  117. package/template/src/feedbax/feedback/feedback-storage.ts +43 -0
  118. package/template/src/feedbax/feedback/identity-handoff.test.ts +81 -0
  119. package/template/src/feedbax/feedback/identity-handoff.ts +119 -0
  120. package/template/src/feedbax/feedback/image-feedback.ts +101 -0
  121. package/template/src/feedbax/feedback/images.test.ts +223 -0
  122. package/template/src/feedbax/feedback/images.ts +324 -0
  123. package/template/src/feedbax/feedback/index.test.ts +900 -0
  124. package/template/src/feedbax/feedback/index.ts +996 -0
  125. package/template/src/feedbax/feedback/notion-comments.test.ts +171 -0
  126. package/template/src/feedbax/feedback/notion-comments.ts +231 -0
  127. package/template/src/feedbax/feedback/notion-data-source.test.ts +364 -0
  128. package/template/src/feedbax/feedback/notion-data-source.ts +458 -0
  129. package/template/src/feedbax/feedback/notion-feedback.test.ts +998 -0
  130. package/template/src/feedbax/feedback/notion-feedback.ts +651 -0
  131. package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
  132. package/template/src/feedbax/feedback/notion-request.ts +74 -0
  133. package/template/src/feedbax/feedback/participation.test.ts +653 -0
  134. package/template/src/feedbax/feedback/participation.ts +502 -0
  135. package/template/src/feedbax.test-fixture.ts +64 -0
  136. package/template/src/feedbax.ts +69 -0
  137. package/template/src/images-runtime.test.ts +133 -0
  138. package/template/src/images-runtime.ts +125 -0
  139. package/template/src/index.css +1795 -0
  140. package/template/src/lazy-create-post-overlay.tsx +6 -0
  141. package/template/src/lazy-post-detail-overlay.tsx +21 -0
  142. package/template/src/masked-post-link.tsx +27 -0
  143. package/template/src/overlay-post-detail.test.tsx +181 -0
  144. package/template/src/overlay-post-detail.tsx +43 -0
  145. package/template/src/participation-client.ts +63 -0
  146. package/template/src/participation-runtime.test.ts +35 -0
  147. package/template/src/participation-runtime.ts +255 -0
  148. package/template/src/portal-comments.test.ts +375 -0
  149. package/template/src/portal-comments.ts +403 -0
  150. package/template/src/portal-features.test.tsx +150 -0
  151. package/template/src/portal-feedback-form.tsx +530 -0
  152. package/template/src/portal-feedback-runtime.test.ts +32 -0
  153. package/template/src/portal-feedback-runtime.ts +58 -0
  154. package/template/src/portal-feedback-server-function.ts +96 -0
  155. package/template/src/portal-feedback-submission.test.ts +307 -0
  156. package/template/src/portal-feedback-submission.ts +224 -0
  157. package/template/src/portal-head.ts +58 -0
  158. package/template/src/portal-server-entry.ts +68 -0
  159. package/template/src/portal-voting.test.ts +106 -0
  160. package/template/src/portal-voting.ts +149 -0
  161. package/template/src/post-badges.tsx +27 -0
  162. package/template/src/post-creation-flow.test.tsx +230 -0
  163. package/template/src/post-creation-flow.tsx +88 -0
  164. package/template/src/post-detail-content.test.tsx +233 -0
  165. package/template/src/post-detail-content.tsx +162 -0
  166. package/template/src/post-detail-overlay.test.tsx +114 -0
  167. package/template/src/post-detail-overlay.tsx +67 -0
  168. package/template/src/post-detail-query.ts +34 -0
  169. package/template/src/post-feed-controls.test.tsx +239 -0
  170. package/template/src/post-feed-controls.tsx +298 -0
  171. package/template/src/post-presentation.ts +37 -0
  172. package/template/src/post-queries.test.ts +158 -0
  173. package/template/src/post-queries.ts +26 -0
  174. package/template/src/post-results.tsx +165 -0
  175. package/template/src/public-cache-policy.test.ts +148 -0
  176. package/template/src/public-cache-policy.ts +119 -0
  177. package/template/src/public-cache-tags.ts +7 -0
  178. package/template/src/public-changelog-page.ts +15 -0
  179. package/template/src/public-changelog-server-function.ts +27 -0
  180. package/template/src/public-date.ts +11 -0
  181. package/template/src/public-failure-state.test.tsx +69 -0
  182. package/template/src/public-feedback-index.tsx +94 -0
  183. package/template/src/public-feedback-page.test.tsx +355 -0
  184. package/template/src/public-feedback-page.ts +86 -0
  185. package/template/src/public-feedback-server-function.ts +22 -0
  186. package/template/src/public-portal-error.tsx +27 -0
  187. package/template/src/public-post-detail-skeleton.tsx +54 -0
  188. package/template/src/public-post-detail.tsx +772 -0
  189. package/template/src/public-post-page.test.tsx +173 -0
  190. package/template/src/public-post-page.ts +15 -0
  191. package/template/src/public-post-route.test.tsx +250 -0
  192. package/template/src/public-post-route.tsx +158 -0
  193. package/template/src/public-post-server-function.ts +48 -0
  194. package/template/src/public-post-unavailable.tsx +10 -0
  195. package/template/src/public-roadmap-page.test.tsx +139 -0
  196. package/template/src/public-roadmap-page.ts +24 -0
  197. package/template/src/public-roadmap-route.test.tsx +254 -0
  198. package/template/src/public-roadmap-route.tsx +50 -0
  199. package/template/src/public-roadmap-server-function.ts +44 -0
  200. package/template/src/public-roadmap-view.tsx +411 -0
  201. package/template/src/public-shell.test.tsx +544 -0
  202. package/template/src/roadmap-query.ts +21 -0
  203. package/template/src/root-branding.test.ts +53 -0
  204. package/template/src/routeTree.gen.ts +177 -0
  205. package/template/src/routed-post-creation-form.tsx +28 -0
  206. package/template/src/router.tsx +27 -0
  207. package/template/src/routes/__root.tsx +114 -0
  208. package/template/src/routes/changelog.tsx +39 -0
  209. package/template/src/routes/email-preferences.tsx +77 -0
  210. package/template/src/routes/index.tsx +61 -0
  211. package/template/src/routes/p.$slug.tsx +16 -0
  212. package/template/src/routes/roadmap.tsx +11 -0
  213. package/template/src/routes/submit.tsx +28 -0
  214. package/template/src/safe-public-failure.ts +47 -0
  215. package/template/src/server.ts +151 -0
  216. package/template/src/similar-posts.tsx +45 -0
  217. package/template/src/submit-route.test.tsx +134 -0
  218. package/template/src/trusted-feedback-handler.test.ts +425 -0
  219. package/template/src/trusted-feedback-handler.ts +181 -0
  220. package/template/src/trusted-feedback-runtime.ts +17 -0
  221. package/template/src/turnstile-configuration.test.ts +37 -0
  222. package/template/src/turnstile-configuration.ts +19 -0
  223. package/template/src/use-media-query.ts +17 -0
  224. package/template/src/vote-toggle.test.tsx +171 -0
  225. package/template/src/vote-toggle.tsx +244 -0
  226. package/template/tsconfig.json +45 -0
  227. package/template/vite.config.ts +20 -0
  228. package/template/vitest.config.ts +4 -0
  229. package/template/vitest.setup.ts +3 -0
  230. package/template/wrangler.jsonc +35 -0
  231. package/dist/cli.d.ts +0 -3
  232. package/dist/cli.d.ts.map +0 -1
  233. package/dist/cli.js.map +0 -1
  234. package/dist/index.d.ts +0 -21
  235. package/dist/index.d.ts.map +0 -1
  236. package/dist/index.js.map +0 -1
  237. package/template/.env.example +0 -4
  238. package/template/apps/portal/components.json +0 -20
  239. package/template/apps/portal/package.json +0 -31
  240. package/template/apps/portal/src/lib/utils.ts +0 -6
  241. package/template/apps/portal/src/routeTree.gen.ts +0 -59
  242. package/template/apps/portal/src/router.tsx +0 -10
  243. package/template/apps/portal/src/routes/__root.tsx +0 -31
  244. package/template/apps/portal/src/routes/index.tsx +0 -35
  245. package/template/apps/portal/src/styles.css +0 -91
  246. package/template/apps/portal/tsconfig.json +0 -15
  247. package/template/apps/portal/vite.config.ts +0 -13
  248. package/template/feedbax.config.ts +0 -13
  249. package/template/feedbax.jsonc +0 -11
  250. package/template/pnpm-workspace.yaml +0 -3
  251. package/template/turbo.json +0 -8
@@ -0,0 +1,458 @@
1
+ import { requestNotion, type NotionRetryOptions } from "./notion-request";
2
+
3
+ const NOTION_API_URL = "https://api.notion.com/v1";
4
+ const NOTION_API_VERSION = "2026-03-11";
5
+
6
+ const canonicalProperties = {
7
+ title: { name: "Title", type: "title", schema: { title: {} } },
8
+ slug: { name: "Slug", type: "rich_text", schema: { rich_text: {} } },
9
+ description: {
10
+ name: "Description",
11
+ type: "rich_text",
12
+ schema: { rich_text: {} },
13
+ },
14
+ type: {
15
+ name: "Type",
16
+ type: "select",
17
+ schema: {
18
+ select: {
19
+ options: [
20
+ { name: "Feature Request" },
21
+ { name: "Bug Report" },
22
+ { name: "General Feedback" },
23
+ ],
24
+ },
25
+ },
26
+ },
27
+ status: {
28
+ name: "Status",
29
+ type: "select",
30
+ schema: {
31
+ select: {
32
+ options: [
33
+ { name: "New" },
34
+ { name: "Reviewing" },
35
+ { name: "Planned" },
36
+ { name: "In Progress" },
37
+ { name: "Shipped" },
38
+ { name: "Closed" },
39
+ ],
40
+ },
41
+ },
42
+ },
43
+ published: {
44
+ name: "Published",
45
+ type: "checkbox",
46
+ schema: { checkbox: {} },
47
+ },
48
+ voteCount: {
49
+ name: "Vote Count",
50
+ type: "number",
51
+ schema: { number: { format: "number" } },
52
+ },
53
+ teamResponse: {
54
+ name: "Team Response",
55
+ type: "rich_text",
56
+ schema: { rich_text: {} },
57
+ },
58
+ releaseNotesUrl: {
59
+ name: "Release Notes URL",
60
+ type: "url",
61
+ schema: { url: {} },
62
+ },
63
+ submitterName: {
64
+ name: "Submitter Name",
65
+ type: "rich_text",
66
+ schema: { rich_text: {} },
67
+ },
68
+ submitterEmail: {
69
+ name: "Submitter Email",
70
+ type: "email",
71
+ schema: { email: {} },
72
+ },
73
+ source: {
74
+ name: "Source",
75
+ type: "select",
76
+ schema: {
77
+ select: {
78
+ options: [{ name: "Portal" }, { name: "API" }, { name: "Team" }],
79
+ },
80
+ },
81
+ },
82
+ externalId: {
83
+ name: "External ID",
84
+ type: "rich_text",
85
+ schema: { rich_text: {} },
86
+ },
87
+ editTokenHash: {
88
+ name: "Edit Token Hash",
89
+ type: "rich_text",
90
+ schema: { rich_text: {} },
91
+ },
92
+ createdAt: {
93
+ name: "Created At",
94
+ type: "created_time",
95
+ schema: { created_time: {} },
96
+ },
97
+ updatedAt: {
98
+ name: "Updated At",
99
+ type: "last_edited_time",
100
+ schema: { last_edited_time: {} },
101
+ },
102
+ } as const;
103
+
104
+ export const feedbackWorkflowProperties = {
105
+ Owner: { people: {} },
106
+ Priority: {
107
+ select: {
108
+ options: [
109
+ { name: "Urgent" },
110
+ { name: "High" },
111
+ { name: "Medium" },
112
+ { name: "Low" },
113
+ ],
114
+ },
115
+ },
116
+ "Strategic Theme": { multi_select: { options: [] } },
117
+ "Participant Segment": { multi_select: { options: [] } },
118
+ Impact: {
119
+ select: {
120
+ options: [{ name: "High" }, { name: "Medium" }, { name: "Low" }],
121
+ },
122
+ },
123
+ Effort: {
124
+ select: {
125
+ options: [{ name: "Small" }, { name: "Medium" }, { name: "Large" }],
126
+ },
127
+ },
128
+ "Decision Note": { rich_text: {} },
129
+ "Needs Follow-up": { checkbox: {} },
130
+ } as const;
131
+
132
+ export type FeedbackPropertyIds = {
133
+ [Key in keyof typeof canonicalProperties]: string;
134
+ };
135
+
136
+ export interface FeedbackDataSourceConfiguration {
137
+ databaseId: string;
138
+ dataSourceId: string;
139
+ propertyIds: FeedbackPropertyIds;
140
+ }
141
+
142
+ interface CreateNotionFeedbackDataSourceOptions {
143
+ token: string;
144
+ parentPageId: string;
145
+ request?: typeof fetch;
146
+ retry?: NotionRetryOptions;
147
+ }
148
+
149
+ interface ValidateNotionFeedbackDataSourceOptions {
150
+ token: string;
151
+ dataSourceId: string;
152
+ propertyIds: FeedbackPropertyIds;
153
+ request?: typeof fetch;
154
+ retry?: NotionRetryOptions;
155
+ }
156
+
157
+ interface NotionProperty {
158
+ id?: unknown;
159
+ name?: unknown;
160
+ type?: unknown;
161
+ }
162
+
163
+ interface NotionObjectResponse {
164
+ id?: unknown;
165
+ parent?: unknown;
166
+ properties?: unknown;
167
+ }
168
+
169
+ export async function createNotionFeedbackDataSource({
170
+ token,
171
+ parentPageId,
172
+ request = fetch,
173
+ retry,
174
+ }: CreateNotionFeedbackDataSourceOptions): Promise<FeedbackDataSourceConfiguration> {
175
+ const response = await requestNotion(
176
+ `${NOTION_API_URL}/databases`,
177
+ {
178
+ method: "POST",
179
+ headers: notionHeaders(token),
180
+ body: JSON.stringify({
181
+ parent: { type: "page_id", page_id: parentPageId },
182
+ title: [{ type: "text", text: { content: "Feedback" } }],
183
+ initial_data_source: {
184
+ properties: {
185
+ ...Object.fromEntries(
186
+ Object.values(canonicalProperties).map(({ name, schema }) => [
187
+ name,
188
+ schema,
189
+ ]),
190
+ ),
191
+ ...feedbackWorkflowProperties,
192
+ },
193
+ },
194
+ }),
195
+ },
196
+ { request, retry, operation: "create" },
197
+ );
198
+
199
+ const database = await readNotionResponse(response);
200
+ if (typeof database.id !== "string") {
201
+ throw new Error("Notion did not return the created Feedback database.");
202
+ }
203
+ const dataSourceId = firstDataSourceId(database);
204
+ const dataSourceResponse = await requestNotion(
205
+ `${NOTION_API_URL}/data_sources/${dataSourceId}`,
206
+ { method: "GET", headers: notionHeaders(token) },
207
+ { request, retry, operation: "idempotent" },
208
+ );
209
+ const dataSource = await readNotionResponse(dataSourceResponse);
210
+ return configurationFromCanonicalNames(dataSource, database.id);
211
+ }
212
+
213
+ export async function createFeedbackWorkflowViews({
214
+ token,
215
+ databaseId,
216
+ dataSourceId,
217
+ propertyIds,
218
+ request = fetch,
219
+ }: {
220
+ token: string;
221
+ databaseId: string;
222
+ dataSourceId: string;
223
+ propertyIds: FeedbackPropertyIds;
224
+ request?: typeof fetch;
225
+ }): Promise<void> {
226
+ const views = [
227
+ {
228
+ name: "Inbox",
229
+ type: "table",
230
+ filter: selectFilter(propertyIds.status, "New"),
231
+ sorts: [{ property: propertyIds.createdAt, direction: "descending" }],
232
+ },
233
+ {
234
+ name: "Reviewing",
235
+ type: "table",
236
+ filter: selectFilter(propertyIds.status, "Reviewing"),
237
+ sorts: [{ property: propertyIds.updatedAt, direction: "descending" }],
238
+ },
239
+ {
240
+ name: "Prioritization",
241
+ type: "table",
242
+ filter: {
243
+ or: [
244
+ selectFilter(propertyIds.status, "Reviewing"),
245
+ selectFilter(propertyIds.status, "Planned"),
246
+ ],
247
+ },
248
+ sorts: [
249
+ { property: "Priority", direction: "ascending" },
250
+ { property: propertyIds.voteCount, direction: "descending" },
251
+ ],
252
+ },
253
+ {
254
+ name: "Follow-up",
255
+ type: "table",
256
+ filter: { property: "Needs Follow-up", checkbox: { equals: true } },
257
+ },
258
+ {
259
+ name: "Roadmap",
260
+ type: "board",
261
+ filter: {
262
+ or: ["Planned", "In Progress", "Shipped"].map((status) =>
263
+ selectFilter(propertyIds.status, status),
264
+ ),
265
+ },
266
+ configuration: {
267
+ type: "board",
268
+ group_by: {
269
+ type: "select",
270
+ property_id: propertyIds.status,
271
+ group_by: "option",
272
+ sort: { type: "manual" },
273
+ },
274
+ },
275
+ },
276
+ {
277
+ name: "Shipped",
278
+ type: "table",
279
+ filter: selectFilter(propertyIds.status, "Shipped"),
280
+ sorts: [{ property: propertyIds.updatedAt, direction: "descending" }],
281
+ },
282
+ ];
283
+ for (const view of views) {
284
+ const response = await requestNotion(
285
+ `${NOTION_API_URL}/views`,
286
+ {
287
+ method: "POST",
288
+ headers: notionHeaders(token),
289
+ body: JSON.stringify({
290
+ database_id: databaseId,
291
+ data_source_id: dataSourceId,
292
+ ...view,
293
+ }),
294
+ },
295
+ { request, operation: "create" },
296
+ );
297
+ await readNotionResponse(response);
298
+ }
299
+ }
300
+
301
+ function selectFilter(property: string, value: string) {
302
+ return { property, select: { equals: value } };
303
+ }
304
+
305
+ export async function validateNotionFeedbackDataSource({
306
+ token,
307
+ dataSourceId,
308
+ propertyIds,
309
+ request = fetch,
310
+ retry,
311
+ }: ValidateNotionFeedbackDataSourceOptions): Promise<FeedbackDataSourceConfiguration> {
312
+ const response = await requestNotion(
313
+ `${NOTION_API_URL}/data_sources/${dataSourceId}`,
314
+ {
315
+ method: "GET",
316
+ headers: notionHeaders(token),
317
+ },
318
+ { request, retry, operation: "idempotent" },
319
+ );
320
+ const dataSource = await readNotionResponse(response);
321
+ const properties = dataSource.properties;
322
+
323
+ if (typeof dataSource.id !== "string" || !isPropertyMap(properties)) {
324
+ throw new Error(
325
+ "Notion returned an invalid Feedback Data Source response.",
326
+ );
327
+ }
328
+
329
+ const propertiesById = new Map(
330
+ Object.values(properties)
331
+ .filter(
332
+ (property): property is NotionProperty & { id: string } =>
333
+ Boolean(property) && typeof property.id === "string",
334
+ )
335
+ .map((property) => [property.id, property]),
336
+ );
337
+ const repairs: string[] = [];
338
+
339
+ for (const [key, { name, type }] of Object.entries(canonicalProperties)) {
340
+ const configuredId = propertyIds[key as keyof FeedbackPropertyIds];
341
+ const property = propertiesById.get(configuredId);
342
+
343
+ if (!property) {
344
+ repairs.push(
345
+ `Restore the "${name}" property with ID "${configuredId}" and type "${type}", then update the configured property ID if Notion assigns a new one.`,
346
+ );
347
+ } else if (property.type !== type) {
348
+ repairs.push(
349
+ `Change the property configured as "${name}" (ID "${configuredId}") from "${String(property.type)}" to "${type}", or restore a ${type} property and configure its ID.`,
350
+ );
351
+ }
352
+ }
353
+
354
+ if (repairs.length > 0) {
355
+ throw new Error(repairs.join("\n"));
356
+ }
357
+
358
+ return {
359
+ databaseId: databaseIdFromDataSource(dataSource),
360
+ dataSourceId,
361
+ propertyIds: { ...propertyIds },
362
+ };
363
+ }
364
+
365
+ function notionHeaders(token: string): Record<string, string> {
366
+ return {
367
+ Authorization: `Bearer ${token}`,
368
+ "Content-Type": "application/json",
369
+ "Notion-Version": NOTION_API_VERSION,
370
+ };
371
+ }
372
+
373
+ async function readNotionResponse(
374
+ response: Response,
375
+ ): Promise<NotionObjectResponse> {
376
+ const body: unknown = await response.json();
377
+
378
+ if (!response.ok) {
379
+ throw new Error(
380
+ `Notion rejected the Feedback Data Source request (${response.status}).`,
381
+ );
382
+ }
383
+
384
+ if (!body || typeof body !== "object") {
385
+ throw new Error(
386
+ "Notion returned an invalid Feedback Data Source response.",
387
+ );
388
+ }
389
+
390
+ return body;
391
+ }
392
+
393
+ function firstDataSourceId(database: NotionObjectResponse): string {
394
+ const dataSources = Reflect.get(database, "data_sources");
395
+ const firstDataSource = Array.isArray(dataSources)
396
+ ? dataSources[0]
397
+ : undefined;
398
+ const id =
399
+ firstDataSource && typeof firstDataSource === "object"
400
+ ? Reflect.get(firstDataSource, "id")
401
+ : undefined;
402
+
403
+ if (typeof id !== "string") {
404
+ throw new Error("Notion did not return the created Feedback Data Source.");
405
+ }
406
+
407
+ return id;
408
+ }
409
+
410
+ function configurationFromCanonicalNames(
411
+ dataSource: NotionObjectResponse,
412
+ databaseId: string,
413
+ ): FeedbackDataSourceConfiguration {
414
+ const properties = dataSource.properties;
415
+ if (typeof dataSource.id !== "string" || !isPropertyMap(properties)) {
416
+ throw new Error(
417
+ "Notion returned an invalid Feedback Data Source response.",
418
+ );
419
+ }
420
+
421
+ return {
422
+ databaseId,
423
+ dataSourceId: dataSource.id,
424
+ propertyIds: Object.fromEntries(
425
+ Object.entries(canonicalProperties).map(([key, { name }]) => {
426
+ const property = properties[name];
427
+ if (!property || typeof property.id !== "string") {
428
+ throw new Error(
429
+ `Notion did not return the required ${name} property.`,
430
+ );
431
+ }
432
+ return [key, property.id];
433
+ }),
434
+ ) as FeedbackPropertyIds,
435
+ };
436
+ }
437
+
438
+ function databaseIdFromDataSource(dataSource: NotionObjectResponse): string {
439
+ const parent = dataSource.parent;
440
+ const databaseId =
441
+ parent && typeof parent === "object"
442
+ ? Reflect.get(parent, "database_id")
443
+ : undefined;
444
+
445
+ if (typeof databaseId !== "string") {
446
+ throw new Error(
447
+ "Notion did not return the Feedback Data Source parent database.",
448
+ );
449
+ }
450
+
451
+ return databaseId;
452
+ }
453
+
454
+ function isPropertyMap(
455
+ value: unknown,
456
+ ): value is Record<string, NotionProperty> {
457
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
458
+ }