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,271 @@
|
|
|
1
|
+
export type PortalFeatures = {
|
|
2
|
+
readonly voting: boolean;
|
|
3
|
+
readonly comments: boolean;
|
|
4
|
+
readonly changelog: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type ProductConfiguration = {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
readonly logo: string;
|
|
10
|
+
readonly siteUrl?: string;
|
|
11
|
+
readonly supportUrl?: string;
|
|
12
|
+
readonly privacyUrl?: string;
|
|
13
|
+
};
|
|
14
|
+
export type FeedbackPropertyIds = {
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly slug: string;
|
|
17
|
+
readonly description: string;
|
|
18
|
+
readonly type: string;
|
|
19
|
+
readonly status: string;
|
|
20
|
+
readonly published: string;
|
|
21
|
+
readonly voteCount: string;
|
|
22
|
+
readonly teamResponse: string;
|
|
23
|
+
readonly releaseNotesUrl: string;
|
|
24
|
+
readonly submitterName: string;
|
|
25
|
+
readonly submitterEmail: string;
|
|
26
|
+
readonly source: string;
|
|
27
|
+
readonly externalId: string;
|
|
28
|
+
readonly editTokenHash: string;
|
|
29
|
+
readonly createdAt: string;
|
|
30
|
+
readonly updatedAt: string;
|
|
31
|
+
};
|
|
32
|
+
export type ChangelogPropertyIds = {
|
|
33
|
+
readonly title: string;
|
|
34
|
+
readonly slug: string;
|
|
35
|
+
readonly date: string;
|
|
36
|
+
readonly summary: string;
|
|
37
|
+
readonly body: string;
|
|
38
|
+
readonly labels: string;
|
|
39
|
+
readonly image: string;
|
|
40
|
+
readonly published: string;
|
|
41
|
+
readonly createdAt: string;
|
|
42
|
+
readonly updatedAt: string;
|
|
43
|
+
};
|
|
44
|
+
export type NotionConfiguration = {
|
|
45
|
+
readonly databaseId: string;
|
|
46
|
+
readonly dataSourceId: string;
|
|
47
|
+
readonly properties: FeedbackPropertyIds;
|
|
48
|
+
};
|
|
49
|
+
export type ChangelogConfiguration = {
|
|
50
|
+
readonly databaseId: string;
|
|
51
|
+
readonly dataSourceId: string;
|
|
52
|
+
readonly properties: ChangelogPropertyIds;
|
|
53
|
+
};
|
|
54
|
+
export type FeedbaxConfig = {
|
|
55
|
+
readonly product: ProductConfiguration;
|
|
56
|
+
readonly theme: { readonly preset: string };
|
|
57
|
+
readonly notion: NotionConfiguration;
|
|
58
|
+
readonly changelog?: ChangelogConfiguration;
|
|
59
|
+
readonly features: PortalFeatures;
|
|
60
|
+
readonly submissions: {
|
|
61
|
+
readonly moderation: "required";
|
|
62
|
+
readonly maxTitleLength: number;
|
|
63
|
+
readonly maxDescriptionLength: number;
|
|
64
|
+
};
|
|
65
|
+
readonly turnstile: { readonly enabled: boolean; readonly siteKey?: string };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const rootKeys = [
|
|
69
|
+
"product",
|
|
70
|
+
"theme",
|
|
71
|
+
"notion",
|
|
72
|
+
"changelog",
|
|
73
|
+
"features",
|
|
74
|
+
"submissions",
|
|
75
|
+
"turnstile",
|
|
76
|
+
];
|
|
77
|
+
const featureKeys = ["voting", "comments", "changelog"] as const;
|
|
78
|
+
const feedbackPropertyKeys = [
|
|
79
|
+
"title",
|
|
80
|
+
"slug",
|
|
81
|
+
"description",
|
|
82
|
+
"type",
|
|
83
|
+
"status",
|
|
84
|
+
"published",
|
|
85
|
+
"voteCount",
|
|
86
|
+
"teamResponse",
|
|
87
|
+
"releaseNotesUrl",
|
|
88
|
+
"submitterName",
|
|
89
|
+
"submitterEmail",
|
|
90
|
+
"source",
|
|
91
|
+
"externalId",
|
|
92
|
+
"editTokenHash",
|
|
93
|
+
"createdAt",
|
|
94
|
+
"updatedAt",
|
|
95
|
+
] as const;
|
|
96
|
+
const changelogPropertyKeys = [
|
|
97
|
+
"title",
|
|
98
|
+
"slug",
|
|
99
|
+
"date",
|
|
100
|
+
"summary",
|
|
101
|
+
"body",
|
|
102
|
+
"labels",
|
|
103
|
+
"image",
|
|
104
|
+
"published",
|
|
105
|
+
"createdAt",
|
|
106
|
+
"updatedAt",
|
|
107
|
+
] as const;
|
|
108
|
+
|
|
109
|
+
export function defineFeedbax(config: FeedbaxConfig): FeedbaxConfig;
|
|
110
|
+
export function defineFeedbax(config: unknown): FeedbaxConfig {
|
|
111
|
+
const value = object(config, "Feedbax configuration");
|
|
112
|
+
rejectUnknown(value, rootKeys, "Feedbax configuration");
|
|
113
|
+
const product = object(value.product, "product");
|
|
114
|
+
rejectUnknown(
|
|
115
|
+
product,
|
|
116
|
+
["name", "description", "logo", "siteUrl", "supportUrl", "privacyUrl"],
|
|
117
|
+
"product",
|
|
118
|
+
);
|
|
119
|
+
const validatedProduct: ProductConfiguration = {
|
|
120
|
+
name: text(product.name, "product.name"),
|
|
121
|
+
description: text(product.description, "product.description"),
|
|
122
|
+
logo: publicUrl(product.logo, "product.logo", true),
|
|
123
|
+
...optionalPublicUrl(product, "siteUrl"),
|
|
124
|
+
...optionalPublicUrl(product, "supportUrl"),
|
|
125
|
+
...optionalPublicUrl(product, "privacyUrl"),
|
|
126
|
+
};
|
|
127
|
+
const theme = object(value.theme, "theme");
|
|
128
|
+
rejectUnknown(theme, ["preset"], "theme");
|
|
129
|
+
const notion = storage(
|
|
130
|
+
value.notion,
|
|
131
|
+
feedbackPropertyKeys,
|
|
132
|
+
"notion",
|
|
133
|
+
) as NotionConfiguration;
|
|
134
|
+
const features = object(value.features, "features");
|
|
135
|
+
rejectUnknown(features, featureKeys, "features");
|
|
136
|
+
const validatedFeatures = Object.fromEntries(
|
|
137
|
+
featureKeys.map((key) => {
|
|
138
|
+
if (typeof features[key] !== "boolean")
|
|
139
|
+
throw new Error(`features.${key} must be true or false.`);
|
|
140
|
+
return [key, features[key]];
|
|
141
|
+
}),
|
|
142
|
+
) as PortalFeatures;
|
|
143
|
+
const changelog =
|
|
144
|
+
value.changelog === undefined
|
|
145
|
+
? undefined
|
|
146
|
+
: (storage(
|
|
147
|
+
value.changelog,
|
|
148
|
+
changelogPropertyKeys,
|
|
149
|
+
"changelog",
|
|
150
|
+
) as ChangelogConfiguration);
|
|
151
|
+
if (validatedFeatures.changelog && !changelog)
|
|
152
|
+
throw new Error(
|
|
153
|
+
"Changelog is enabled but changelog storage is not configured. Add changelog identifiers and property mappings, or set features.changelog to false.",
|
|
154
|
+
);
|
|
155
|
+
const submissions = object(value.submissions, "submissions");
|
|
156
|
+
rejectUnknown(
|
|
157
|
+
submissions,
|
|
158
|
+
["moderation", "maxTitleLength", "maxDescriptionLength"],
|
|
159
|
+
"submissions",
|
|
160
|
+
);
|
|
161
|
+
if (submissions.moderation !== "required")
|
|
162
|
+
throw new Error('submissions.moderation must be "required".');
|
|
163
|
+
const turnstile = object(value.turnstile, "turnstile");
|
|
164
|
+
rejectUnknown(turnstile, ["enabled", "siteKey"], "turnstile");
|
|
165
|
+
if (typeof turnstile.enabled !== "boolean")
|
|
166
|
+
throw new Error("turnstile.enabled must be true or false.");
|
|
167
|
+
const siteKey =
|
|
168
|
+
turnstile.siteKey === undefined
|
|
169
|
+
? undefined
|
|
170
|
+
: text(turnstile.siteKey, "turnstile.siteKey");
|
|
171
|
+
if (turnstile.enabled && !siteKey)
|
|
172
|
+
throw new Error("Turnstile is enabled but turnstile.siteKey is missing.");
|
|
173
|
+
return {
|
|
174
|
+
product: validatedProduct,
|
|
175
|
+
theme: { preset: text(theme.preset, "theme.preset") },
|
|
176
|
+
notion,
|
|
177
|
+
...(changelog ? { changelog } : {}),
|
|
178
|
+
features: validatedFeatures,
|
|
179
|
+
submissions: {
|
|
180
|
+
moderation: "required",
|
|
181
|
+
maxTitleLength: positiveInteger(
|
|
182
|
+
submissions.maxTitleLength,
|
|
183
|
+
"submissions.maxTitleLength",
|
|
184
|
+
),
|
|
185
|
+
maxDescriptionLength: positiveInteger(
|
|
186
|
+
submissions.maxDescriptionLength,
|
|
187
|
+
"submissions.maxDescriptionLength",
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
turnstile: { enabled: turnstile.enabled, ...(siteKey ? { siteKey } : {}) },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function assertProductionConfiguration(
|
|
195
|
+
config: FeedbaxConfig,
|
|
196
|
+
environment = process.env.NODE_ENV,
|
|
197
|
+
): void {
|
|
198
|
+
if (environment === "production" && !config.product.siteUrl)
|
|
199
|
+
throw new Error(
|
|
200
|
+
"product.siteUrl is required in production so canonical and social metadata use an absolute URL.",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
function storage(value: unknown, keys: readonly string[], label: string) {
|
|
204
|
+
const input = object(value, label);
|
|
205
|
+
rejectUnknown(input, ["databaseId", "dataSourceId", "properties"], label);
|
|
206
|
+
const properties = object(input.properties, `${label}.properties`);
|
|
207
|
+
rejectUnknown(properties, keys, `${label}.properties`);
|
|
208
|
+
for (const key of keys) text(properties[key], `${label}.properties.${key}`);
|
|
209
|
+
return {
|
|
210
|
+
databaseId: text(input.databaseId, `${label}.databaseId`),
|
|
211
|
+
dataSourceId: text(input.dataSourceId, `${label}.dataSourceId`),
|
|
212
|
+
properties: { ...properties },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function object(value: unknown, label: string): Record<string, unknown> {
|
|
216
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
217
|
+
throw new Error(`${label} must be an object.`);
|
|
218
|
+
return value as Record<string, unknown>;
|
|
219
|
+
}
|
|
220
|
+
function text(value: unknown, label: string): string {
|
|
221
|
+
if (typeof value !== "string" || !value.trim())
|
|
222
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
223
|
+
return value.trim();
|
|
224
|
+
}
|
|
225
|
+
function positiveInteger(value: unknown, label: string): number {
|
|
226
|
+
if (!Number.isInteger(value) || Number(value) <= 0)
|
|
227
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
228
|
+
return Number(value);
|
|
229
|
+
}
|
|
230
|
+
function rejectUnknown(
|
|
231
|
+
value: Record<string, unknown>,
|
|
232
|
+
allowed: readonly string[],
|
|
233
|
+
label: string,
|
|
234
|
+
) {
|
|
235
|
+
const key = Object.keys(value).find(
|
|
236
|
+
(candidate) => !allowed.includes(candidate),
|
|
237
|
+
);
|
|
238
|
+
if (key) throw new Error(`Unknown ${label} key "${key}".`);
|
|
239
|
+
}
|
|
240
|
+
function optionalPublicUrl(
|
|
241
|
+
value: Record<string, unknown>,
|
|
242
|
+
key: "siteUrl" | "supportUrl" | "privacyUrl",
|
|
243
|
+
) {
|
|
244
|
+
return value[key] === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: { [key]: publicUrl(value[key], `product.${key}`, false) };
|
|
247
|
+
}
|
|
248
|
+
function publicUrl(
|
|
249
|
+
value: unknown,
|
|
250
|
+
label: string,
|
|
251
|
+
allowRelative: boolean,
|
|
252
|
+
): string {
|
|
253
|
+
const url = text(value, label);
|
|
254
|
+
if (
|
|
255
|
+
allowRelative &&
|
|
256
|
+
url.startsWith("/") &&
|
|
257
|
+
!url.startsWith("//") &&
|
|
258
|
+
!url.includes("\\") &&
|
|
259
|
+
!Array.from(url).some((character) => character.charCodeAt(0) <= 32)
|
|
260
|
+
)
|
|
261
|
+
return url;
|
|
262
|
+
try {
|
|
263
|
+
if (new URL(url).protocol === "https:") return url;
|
|
264
|
+
} catch {
|
|
265
|
+
// The actionable validation error below is intentionally shared by parse
|
|
266
|
+
// failures and unsupported protocols.
|
|
267
|
+
}
|
|
268
|
+
throw new Error(
|
|
269
|
+
`${label} must be ${allowRelative ? "root-relative or " : ""}an HTTPS URL.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Post, PublicPostQuery, RoadmapStatus } from "./index";
|
|
2
|
+
|
|
3
|
+
export type PostSource = "Portal" | "API";
|
|
4
|
+
|
|
5
|
+
export type StoredPost = Post &
|
|
6
|
+
Partial<{
|
|
7
|
+
browserCapabilityHash: string;
|
|
8
|
+
source: PostSource;
|
|
9
|
+
externalId: string;
|
|
10
|
+
}> &
|
|
11
|
+
Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
export type NewStoredPost = Omit<Post, "id"> &
|
|
14
|
+
Partial<{
|
|
15
|
+
browserCapabilityHash: string;
|
|
16
|
+
source: PostSource;
|
|
17
|
+
externalId: string;
|
|
18
|
+
}> &
|
|
19
|
+
Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
export interface FeedbackStorage {
|
|
22
|
+
create(item: NewStoredPost): Promise<StoredPost>;
|
|
23
|
+
save(item: StoredPost): Promise<StoredPost>;
|
|
24
|
+
updateVoteCount(item: StoredPost, voteCount: number): Promise<StoredPost>;
|
|
25
|
+
find(id: string): Promise<StoredPost | undefined>;
|
|
26
|
+
findBySlug(slug: string): Promise<StoredPost | undefined>;
|
|
27
|
+
findPublicBySlug(slug: string): Promise<StoredPost | undefined>;
|
|
28
|
+
findByExternalId(externalId: string): Promise<StoredPost | undefined>;
|
|
29
|
+
list(): Promise<StoredPost[]>;
|
|
30
|
+
listPublic(query: PublicPostQuery): Promise<{
|
|
31
|
+
items: StoredPost[];
|
|
32
|
+
nextCursor?: string;
|
|
33
|
+
}>;
|
|
34
|
+
listPublicRoadmap(query: {
|
|
35
|
+
status: RoadmapStatus;
|
|
36
|
+
cursor?: string;
|
|
37
|
+
}): Promise<{
|
|
38
|
+
items: StoredPost[];
|
|
39
|
+
nextCursor?: string;
|
|
40
|
+
totalCount: number;
|
|
41
|
+
}>;
|
|
42
|
+
remove(id: string): Promise<void>;
|
|
43
|
+
}
|