founder-schema 0.4.0 → 0.6.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/README.md +64 -1
- package/dist/access/permissions.d.ts +24 -2
- package/dist/access/permissions.d.ts.map +1 -1
- package/dist/access/permissions.js +9 -0
- package/dist/access/permissions.js.map +1 -1
- package/dist/core/common.d.ts +37 -0
- package/dist/core/common.d.ts.map +1 -1
- package/dist/core/common.js +49 -0
- package/dist/core/common.js.map +1 -1
- package/dist/core/errors.d.ts +41 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +75 -4
- package/dist/core/errors.js.map +1 -1
- package/dist/core/limits.d.ts +76 -4
- package/dist/core/limits.d.ts.map +1 -1
- package/dist/core/limits.js +80 -4
- package/dist/core/limits.js.map +1 -1
- package/dist/core/responses.d.ts +55 -0
- package/dist/core/responses.d.ts.map +1 -1
- package/dist/core/responses.js +3 -1
- package/dist/core/responses.js.map +1 -1
- package/dist/core/validation.d.ts +16 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +16 -0
- package/dist/core/validation.js.map +1 -1
- package/dist/domains/account/account.d.ts +2 -0
- package/dist/domains/account/account.d.ts.map +1 -1
- package/dist/domains/account/account.js +2 -0
- package/dist/domains/account/account.js.map +1 -1
- package/dist/domains/audit/audit.d.ts +4 -0
- package/dist/domains/audit/audit.d.ts.map +1 -1
- package/dist/domains/audit/audit.js +6 -3
- package/dist/domains/audit/audit.js.map +1 -1
- package/dist/domains/contributions/contributions.d.ts +158 -0
- package/dist/domains/contributions/contributions.d.ts.map +1 -0
- package/dist/domains/contributions/contributions.js +93 -0
- package/dist/domains/contributions/contributions.js.map +1 -0
- package/dist/domains/discover/discover.d.ts +5 -0
- package/dist/domains/discover/discover.d.ts.map +1 -1
- package/dist/domains/discover/discover.js +3 -13
- package/dist/domains/discover/discover.js.map +1 -1
- package/dist/domains/feed/feed.d.ts +293 -32
- package/dist/domains/feed/feed.d.ts.map +1 -1
- package/dist/domains/feed/feed.js +109 -38
- package/dist/domains/feed/feed.js.map +1 -1
- package/dist/domains/media/media.d.ts +159 -0
- package/dist/domains/media/media.d.ts.map +1 -0
- package/dist/domains/media/media.js +136 -0
- package/dist/domains/media/media.js.map +1 -0
- package/dist/domains/members/members.d.ts +5 -5
- package/dist/domains/moderation/moderation.d.ts +53 -3
- package/dist/domains/moderation/moderation.d.ts.map +1 -1
- package/dist/domains/moderation/moderation.js +47 -8
- package/dist/domains/moderation/moderation.js.map +1 -1
- package/dist/domains/notifications/notifications.d.ts +212 -0
- package/dist/domains/notifications/notifications.d.ts.map +1 -0
- package/dist/domains/notifications/notifications.js +144 -0
- package/dist/domains/notifications/notifications.js.map +1 -0
- package/dist/domains/onboarding/onboarding.d.ts +21 -24
- package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
- package/dist/domains/onboarding/onboarding.js +21 -20
- package/dist/domains/onboarding/onboarding.js.map +1 -1
- package/dist/domains/products/products.d.ts +1449 -0
- package/dist/domains/products/products.d.ts.map +1 -0
- package/dist/domains/products/products.js +453 -0
- package/dist/domains/products/products.js.map +1 -0
- package/dist/domains/profile/founder-profile.d.ts +66 -0
- package/dist/domains/profile/founder-profile.d.ts.map +1 -1
- package/dist/domains/profile/founder-profile.js +70 -1
- package/dist/domains/profile/founder-profile.js.map +1 -1
- package/dist/domains/reviews/reviews.d.ts +184 -0
- package/dist/domains/reviews/reviews.d.ts.map +1 -0
- package/dist/domains/reviews/reviews.js +99 -0
- package/dist/domains/reviews/reviews.js.map +1 -0
- package/dist/domains/uploads/uploads.d.ts +54 -64
- package/dist/domains/uploads/uploads.d.ts.map +1 -1
- package/dist/domains/uploads/uploads.js +43 -60
- package/dist/domains/uploads/uploads.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -56,6 +56,39 @@ export declare const ventureSchema: z.ZodObject<{
|
|
|
56
56
|
foundedYear: z.ZodOptional<z.ZodNumber>;
|
|
57
57
|
}, z.core.$strip>;
|
|
58
58
|
export type Venture = z.infer<typeof ventureSchema>;
|
|
59
|
+
/**
|
|
60
|
+
* A member as a byline renders them: on a post (`feedAuthorSchema` is this), a
|
|
61
|
+
* product's team, a review, a notification, the top-helpers list.
|
|
62
|
+
*
|
|
63
|
+
* Every field is LIVE, joined against the profile at read time rather than
|
|
64
|
+
* frozen onto the post or review at write time. So a renamed handle, a new
|
|
65
|
+
* photo or an edited headline updates everywhere, the link always targets the
|
|
66
|
+
* current username, and nothing personal survives in other people's rows once
|
|
67
|
+
* an account is deleted.
|
|
68
|
+
*/
|
|
69
|
+
export declare const memberBylineSchema: z.ZodObject<{
|
|
70
|
+
userId: z.ZodString;
|
|
71
|
+
username: z.ZodString;
|
|
72
|
+
displayName: z.ZodString;
|
|
73
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
74
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
75
|
+
}, z.core.$strip>;
|
|
76
|
+
export type MemberByline = z.infer<typeof memberBylineSchema>;
|
|
77
|
+
/**
|
|
78
|
+
* What a member has given the community, on their profile and card.
|
|
79
|
+
*
|
|
80
|
+
* `score` is points earned from OTHER members' recognition only: a product's
|
|
81
|
+
* team marking your feedback helpful, confirming you tested their product,
|
|
82
|
+
* members finding your review helpful, and writing reviews. Nothing a member
|
|
83
|
+
* does to their own product counts, so the score cannot be bought by having a
|
|
84
|
+
* popular one. `helpedCount` is how many different founders recognised that
|
|
85
|
+
* help ("Helped 12 founders"). See `domains/contributions`.
|
|
86
|
+
*/
|
|
87
|
+
export declare const contributionSummarySchema: z.ZodObject<{
|
|
88
|
+
score: z.ZodNumber;
|
|
89
|
+
helpedCount: z.ZodNumber;
|
|
90
|
+
}, z.core.$strip>;
|
|
91
|
+
export type ContributionSummary = z.infer<typeof contributionSummarySchema>;
|
|
59
92
|
/**
|
|
60
93
|
* The founder's off-platform links. ONE shape: onboarding's contact step picks
|
|
61
94
|
* its three from this, so a link one side knows can never be missing on the
|
|
@@ -86,6 +119,18 @@ export declare const founderProfileSchema: z.ZodObject<{
|
|
|
86
119
|
headline: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
87
120
|
about: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
88
121
|
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
122
|
+
avatar: z.ZodOptional<z.ZodObject<{
|
|
123
|
+
id: z.ZodString;
|
|
124
|
+
url: z.ZodURL;
|
|
125
|
+
width: z.ZodNumber;
|
|
126
|
+
height: z.ZodNumber;
|
|
127
|
+
variants: z.ZodArray<z.ZodObject<{
|
|
128
|
+
width: z.ZodNumber;
|
|
129
|
+
height: z.ZodNumber;
|
|
130
|
+
url: z.ZodURL;
|
|
131
|
+
}, z.core.$strip>>;
|
|
132
|
+
placeholder: z.ZodOptional<z.ZodString>;
|
|
133
|
+
}, z.core.$strip>>;
|
|
89
134
|
location: z.ZodOptional<z.ZodObject<{
|
|
90
135
|
city: z.ZodString;
|
|
91
136
|
state: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
@@ -168,6 +213,10 @@ export declare const founderProfileSchema: z.ZodObject<{
|
|
|
168
213
|
github: z.ZodOptional<z.ZodURL>;
|
|
169
214
|
website: z.ZodOptional<z.ZodURL>;
|
|
170
215
|
}, z.core.$strip>>;
|
|
216
|
+
contribution: z.ZodObject<{
|
|
217
|
+
score: z.ZodNumber;
|
|
218
|
+
helpedCount: z.ZodNumber;
|
|
219
|
+
}, z.core.$strip>;
|
|
171
220
|
isOnboarded: z.ZodDefault<z.ZodBoolean>;
|
|
172
221
|
isActive: z.ZodDefault<z.ZodBoolean>;
|
|
173
222
|
createdAt: z.ZodISODateTime;
|
|
@@ -189,9 +238,14 @@ export declare const founderCardSchema: z.ZodObject<{
|
|
|
189
238
|
country: z.ZodString;
|
|
190
239
|
}, z.core.$strip>>;
|
|
191
240
|
displayName: z.ZodString;
|
|
241
|
+
userId: z.ZodString;
|
|
192
242
|
username: z.ZodString;
|
|
193
243
|
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
194
244
|
headline: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
245
|
+
contribution: z.ZodObject<{
|
|
246
|
+
score: z.ZodNumber;
|
|
247
|
+
helpedCount: z.ZodNumber;
|
|
248
|
+
}, z.core.$strip>;
|
|
195
249
|
venture: z.ZodOptional<z.ZodObject<{
|
|
196
250
|
name: z.ZodString;
|
|
197
251
|
tagline: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
@@ -384,4 +438,16 @@ export declare const usernameAvailabilitySchema: z.ZodObject<{
|
|
|
384
438
|
}>>;
|
|
385
439
|
}, z.core.$strip>;
|
|
386
440
|
export type UsernameAvailability = z.infer<typeof usernameAvailabilitySchema>;
|
|
441
|
+
/**
|
|
442
|
+
* `PUT /api/v1/profiles/me/avatar`: use a completed upload (purpose `avatar`)
|
|
443
|
+
* as the member's photo. Answers with the updated `founderProfileSchema`.
|
|
444
|
+
*
|
|
445
|
+
* The previous photo is deleted from storage, not merely unlinked, so its old
|
|
446
|
+
* address stops working. `DELETE /api/v1/profiles/me/avatar` removes the photo
|
|
447
|
+
* the same way and the member falls back to their sign-in provider's picture.
|
|
448
|
+
*/
|
|
449
|
+
export declare const setAvatarBodySchema: z.ZodObject<{
|
|
450
|
+
mediaId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
451
|
+
}, z.core.$strip>;
|
|
452
|
+
export type SetAvatarBody = z.infer<typeof setAvatarBodySchema>;
|
|
387
453
|
//# sourceMappingURL=founder-profile.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"founder-profile.d.ts","sourceRoot":"","sources":["../../../src/domains/profile/founder-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"founder-profile.d.ts","sourceRoot":"","sources":["../../../src/domains/profile/founder-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA8BxB;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BxB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB;;;;;;iBAS7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB;;;iBAGpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB;;;;;;iBAM7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqD/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgB5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAqB5D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAWzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF;;;GAGG;AACH,eAAO,MAAM,wBAAwB;;iBAA+C,CAAC;AACrF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,oDAAoD;AACpD,eAAO,MAAM,0BAA0B;;;;;;;iBAKrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB;;iBAAwC,CAAC;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* @module founder-schema/domains/profile
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
import { chosenUsernameSchema, GOAL_VALUES, goalSchema, httpUrlSchema, industrySchema, locationSchema, optionalTrimmedString, skillSchema, startupStageSchema, usernameSchema, } from '../../core/common.js';
|
|
10
|
+
import { chosenUsernameSchema, GOAL_VALUES, goalSchema, httpUrlSchema, industrySchema, locationSchema, objectIdSchema, optionalTrimmedString, skillSchema, startupStageSchema, usernameSchema, } from '../../core/common.js';
|
|
11
11
|
import { ABOUT_MAX, DISPLAY_NAME_MAX, FOUNDED_YEAR_MIN, HEADLINE_MAX, SKILLS_MAX, TEAM_SIZE_MAX, TEAM_SIZE_MIN, VENTURE_NAME_MAX, VENTURE_TAGLINE_MAX, } from '../../core/limits.js';
|
|
12
12
|
import { VALIDATION_MESSAGE } from '../../core/validation.js';
|
|
13
|
+
import { mediaImageSchema } from '../media/media.js';
|
|
13
14
|
// ─── Venture ─────────────────────────────────────────────────────────────────
|
|
14
15
|
/**
|
|
15
16
|
* ⚠ NUMBERS ARE NUMBERS, never coerced. This is a BODY, which arrives as JSON
|
|
@@ -46,6 +47,41 @@ export const ventureSchema = z.object({
|
|
|
46
47
|
})
|
|
47
48
|
.optional(),
|
|
48
49
|
});
|
|
50
|
+
// ─── A member as other surfaces name them ────────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* A member as a byline renders them: on a post (`feedAuthorSchema` is this), a
|
|
53
|
+
* product's team, a review, a notification, the top-helpers list.
|
|
54
|
+
*
|
|
55
|
+
* Every field is LIVE, joined against the profile at read time rather than
|
|
56
|
+
* frozen onto the post or review at write time. So a renamed handle, a new
|
|
57
|
+
* photo or an edited headline updates everywhere, the link always targets the
|
|
58
|
+
* current username, and nothing personal survives in other people's rows once
|
|
59
|
+
* an account is deleted.
|
|
60
|
+
*/
|
|
61
|
+
export const memberBylineSchema = z.object({
|
|
62
|
+
userId: z.string(),
|
|
63
|
+
username: usernameSchema,
|
|
64
|
+
displayName: z.string(),
|
|
65
|
+
/** Server-generated or from the login provider, never member-typed, so a
|
|
66
|
+
* plain URL check. Same reasoning as `avatarUrl` on the profile. */
|
|
67
|
+
avatarUrl: z.url().optional(),
|
|
68
|
+
/** The member's one-line headline, live-joined. */
|
|
69
|
+
headline: z.string().optional(),
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* What a member has given the community, on their profile and card.
|
|
73
|
+
*
|
|
74
|
+
* `score` is points earned from OTHER members' recognition only: a product's
|
|
75
|
+
* team marking your feedback helpful, confirming you tested their product,
|
|
76
|
+
* members finding your review helpful, and writing reviews. Nothing a member
|
|
77
|
+
* does to their own product counts, so the score cannot be bought by having a
|
|
78
|
+
* popular one. `helpedCount` is how many different founders recognised that
|
|
79
|
+
* help ("Helped 12 founders"). See `domains/contributions`.
|
|
80
|
+
*/
|
|
81
|
+
export const contributionSummarySchema = z.object({
|
|
82
|
+
score: z.number().int().nonnegative(),
|
|
83
|
+
helpedCount: z.number().int().nonnegative(),
|
|
84
|
+
});
|
|
49
85
|
// ─── Profile ─────────────────────────────────────────────────────────────────
|
|
50
86
|
/**
|
|
51
87
|
* The founder's off-platform links. ONE shape: onboarding's contact step picks
|
|
@@ -91,13 +127,30 @@ export const founderProfileSchema = z.object({
|
|
|
91
127
|
* `user.updated` webhook, which carries the Google OAuth picture. If a
|
|
92
128
|
* "paste a link to your photo" control is ever added, this goes back to
|
|
93
129
|
* `httpUrlSchema` and the ruling behind it is void.
|
|
130
|
+
*
|
|
131
|
+
* WHICH picture: the member's uploaded photo at its small size (192, see
|
|
132
|
+
* `MEDIA_SIZES.avatar`), else their sign-in provider's picture, else absent.
|
|
133
|
+
* Every surface that shows this member resolves it the same way.
|
|
94
134
|
*/
|
|
95
135
|
avatarUrl: z.url().optional(),
|
|
136
|
+
/**
|
|
137
|
+
* The uploaded photo itself, every size, when the member has one. Absent
|
|
138
|
+
* means `avatarUrl` (if any) is the sign-in provider's picture, so a client
|
|
139
|
+
* offers "Remove photo" only when this is present. Not on `founderCardSchema`:
|
|
140
|
+
* a card needs one small picture, not the set.
|
|
141
|
+
*/
|
|
142
|
+
avatar: mediaImageSchema.optional(),
|
|
96
143
|
location: locationSchema.optional(),
|
|
97
144
|
venture: ventureSchema.optional(),
|
|
98
145
|
skills: z.array(skillSchema).max(SKILLS_MAX).default([]),
|
|
99
146
|
goals: z.array(goalSchema).max(GOAL_VALUES.length).default([]),
|
|
100
147
|
links: founderLinksSchema.default({}),
|
|
148
|
+
/**
|
|
149
|
+
* Their contribution to other founders. Server-computed, never written by a
|
|
150
|
+
* member. REQUIRED, no default: a read-side default would let a server that
|
|
151
|
+
* forgot the field pass as a real zero.
|
|
152
|
+
*/
|
|
153
|
+
contribution: contributionSummarySchema,
|
|
101
154
|
/** Onboarding is finished and the member is visible in Discover. */
|
|
102
155
|
isOnboarded: z.boolean().default(false),
|
|
103
156
|
/** Member paused their own profile. Hidden from Discover, keeps their data. */
|
|
@@ -114,6 +167,11 @@ export const founderProfileSchema = z.object({
|
|
|
114
167
|
*/
|
|
115
168
|
export const founderCardSchema = founderProfileSchema.pick({
|
|
116
169
|
id: true,
|
|
170
|
+
/**
|
|
171
|
+
* The MEMBER's id (0.6.0): what a team invitation, a chat and every byline
|
|
172
|
+
* key on. `id` is the profile's own id, which nothing takes as input.
|
|
173
|
+
*/
|
|
174
|
+
userId: true,
|
|
117
175
|
username: true,
|
|
118
176
|
displayName: true,
|
|
119
177
|
headline: true,
|
|
@@ -122,6 +180,7 @@ export const founderCardSchema = founderProfileSchema.pick({
|
|
|
122
180
|
venture: true,
|
|
123
181
|
skills: true,
|
|
124
182
|
goals: true,
|
|
183
|
+
contribution: true,
|
|
125
184
|
});
|
|
126
185
|
// ─── Write bodies ────────────────────────────────────────────────────────────
|
|
127
186
|
/**
|
|
@@ -177,4 +236,14 @@ export const usernameAvailabilitySchema = z.object({
|
|
|
177
236
|
/** Present when unavailable: `taken` or `reserved`. */
|
|
178
237
|
reason: z.enum(['taken', 'reserved']).optional(),
|
|
179
238
|
});
|
|
239
|
+
// ─── The member's own photo ──────────────────────────────────────────────────
|
|
240
|
+
/**
|
|
241
|
+
* `PUT /api/v1/profiles/me/avatar`: use a completed upload (purpose `avatar`)
|
|
242
|
+
* as the member's photo. Answers with the updated `founderProfileSchema`.
|
|
243
|
+
*
|
|
244
|
+
* The previous photo is deleted from storage, not merely unlinked, so its old
|
|
245
|
+
* address stops working. `DELETE /api/v1/profiles/me/avatar` removes the photo
|
|
246
|
+
* the same way and the member falls back to their sign-in provider's picture.
|
|
247
|
+
*/
|
|
248
|
+
export const setAvatarBodySchema = z.object({ mediaId: objectIdSchema });
|
|
180
249
|
//# sourceMappingURL=founder-profile.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"founder-profile.js","sourceRoot":"","sources":["../../../src/domains/profile/founder-profile.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — The founder profile\n *\n * One member, one profile. The central auth account owns identity (email,\n * verification, deletion); this owns everything Founder Circle shows.\n *\n * @module founder-schema/domains/profile\n */\n\nimport { z } from 'zod';\nimport {\n chosenUsernameSchema,\n GOAL_VALUES,\n goalSchema,\n httpUrlSchema,\n industrySchema,\n locationSchema,\n optionalTrimmedString,\n skillSchema,\n startupStageSchema,\n usernameSchema,\n} from '../../core/common.js';\nimport {\n ABOUT_MAX,\n DISPLAY_NAME_MAX,\n FOUNDED_YEAR_MIN,\n HEADLINE_MAX,\n SKILLS_MAX,\n TEAM_SIZE_MAX,\n TEAM_SIZE_MIN,\n VENTURE_NAME_MAX,\n VENTURE_TAGLINE_MAX,\n} from '../../core/limits.js';\nimport { VALIDATION_MESSAGE } from '../../core/validation.js';\n\n// ─── Venture ─────────────────────────────────────────────────────────────────\n\n/**\n * ⚠ NUMBERS ARE NUMBERS, never coerced. This is a BODY, which arrives as JSON\n * and already carries types. `z.coerce.number()` here accepted `teamSize: true`\n * as 1 and `\" \"` as 0, and made the schema's input type `unknown`, so a form\n * typed against it could put anything in the field. Coercion belongs to query\n * strings, where everything is text; a form converts its text input before it\n * builds the body.\n */\nexport const ventureSchema = z.object({\n name: z.string().trim().min(1).max(VENTURE_NAME_MAX),\n tagline: optionalTrimmedString(VENTURE_TAGLINE_MAX),\n website: httpUrlSchema.optional(),\n industry: industrySchema,\n stage: startupStageSchema,\n /** Team size including the founder. Optional: a solo pre-idea founder has none to report. */\n teamSize: z.number().int().min(TEAM_SIZE_MIN).max(TEAM_SIZE_MAX).optional(),\n /**\n * Not in the future. The bound is read per parse, never captured once.\n *\n * `.max(new Date().getUTCFullYear())` looks equivalent and is not: it is\n * evaluated when this module is imported, so a server process still up on\n * 1 January rejects the new year until somebody restarts it. The backend's\n * Mongoose model deliberately carries no upper bound for the same reason, a\n * `max` there being frozen at schema construction. This is where the ceiling\n * lives.\n */\n foundedYear: z\n .number()\n .int()\n .min(FOUNDED_YEAR_MIN)\n .refine((year) => year <= new Date().getUTCFullYear(), {\n error: VALIDATION_MESSAGE.foundedYearFuture,\n })\n .optional(),\n});\nexport type Venture = z.infer<typeof ventureSchema>;\n\n// ─── Profile ─────────────────────────────────────────────────────────────────\n\n/**\n * The founder's off-platform links. ONE shape: onboarding's contact step picks\n * its three from this, so a link one side knows can never be missing on the\n * other. (It was written twice until 0.2.0, and the profile side lacked\n * Instagram, which onboarding collected.)\n *\n * Defined without a default, because the read side wants one and the write\n * side must not have one; each adds its own.\n */\nexport const founderLinksSchema = z.object({\n linkedin: httpUrlSchema.optional(),\n x: httpUrlSchema.optional(),\n instagram: httpUrlSchema.optional(),\n github: httpUrlSchema.optional(),\n website: httpUrlSchema.optional(),\n});\nexport type FounderLinks = z.infer<typeof founderLinksSchema>;\n\n/**\n * The profile as the owner sees it. A peer sees the redacted projection\n * (`founderCardSchema`), which is built on the server and never by dropping\n * fields on the client.\n */\nexport const founderProfileSchema = z.object({\n id: z.string(),\n userId: z.string(),\n username: usernameSchema,\n displayName: z.string().trim().min(1).max(DISPLAY_NAME_MAX),\n headline: optionalTrimmedString(HEADLINE_MAX),\n about: optionalTrimmedString(ABOUT_MAX),\n /**\n * Plain `z.url()`, NOT `httpUrlSchema`, and the difference is deliberate.\n *\n * No member can write this field: it appears in no write body, and\n * `public-surface.test.ts` asserts that and will fail the day somebody adds it\n * to a form. So the scheme restriction would be defending a field that takes\n * no member input, while refusing hosts we legitimately serve avatars from,\n * our own dev host among them. A read schema that rejects our own response is\n * a self-inflicted outage.\n *\n * ⚠️ That argument holds only while nothing member-supplied reaches this\n * field. Two sources write an avatar today and NEITHER is a member typing a\n * URL: our own upload path, and the central-auth `user.created` /\n * `user.updated` webhook, which carries the Google OAuth picture. If a\n * \"paste a link to your photo\" control is ever added, this goes back to\n * `httpUrlSchema` and the ruling behind it is void.\n */\n avatarUrl: z.url().optional(),\n location: locationSchema.optional(),\n venture: ventureSchema.optional(),\n skills: z.array(skillSchema).max(SKILLS_MAX).default([]),\n goals: z.array(goalSchema).max(GOAL_VALUES.length).default([]),\n links: founderLinksSchema.default({}),\n /** Onboarding is finished and the member is visible in Discover. */\n isOnboarded: z.boolean().default(false),\n /** Member paused their own profile. Hidden from Discover, keeps their data. */\n isActive: z.boolean().default(true),\n createdAt: z.iso.datetime(),\n updatedAt: z.iso.datetime(),\n});\nexport type FounderProfile = z.infer<typeof founderProfileSchema>;\n\n/**\n * What one founder sees of another in Discover and on a profile page.\n *\n * Built server-side. Keep it a strict subset of `founderProfileSchema` — if a\n * private field ever needs to show here, that is a product decision, not an\n * accident of spreading the full document.\n */\nexport const founderCardSchema = founderProfileSchema.pick({\n id: true,\n username: true,\n displayName: true,\n headline: true,\n avatarUrl: true,\n location: true,\n venture: true,\n skills: true,\n goals: true,\n});\nexport type FounderCard = z.infer<typeof founderCardSchema>;\n\n// ─── Write bodies ────────────────────────────────────────────────────────────\n\n/**\n * A text field the member is allowed to empty.\n *\n * Three states, and they are deliberately distinct:\n * absent do not touch this field\n * null clear it (the backend turns this into an `$unset`)\n * \"\" REJECTED\n *\n * The empty string is refused on purpose. A form that forgets to convert a\n * cleared input to `null` then fails loudly at the boundary, instead of being\n * silently dropped and leaving the member staring at text they just deleted.\n * `optionalTrimmedString` does the dropping, which is right for a create body\n * and wrong for a patch.\n */\nconst clearableText = (maxLength: number) =>\n z.string().trim().min(1, VALIDATION_MESSAGE.clearWithNull).max(maxLength).nullable().optional();\n\n/**\n * Patch body for the Me tab. Only the keys the member actually changed.\n *\n * ⚠️ Written as its own object ON PURPOSE. Do NOT rebuild it as\n * `founderProfileSchema.pick({...}).partial()`: `.partial()` does not switch\n * off `.default()`, so `skills`, `goals` and `links` would materialise as\n * `[]`, `[]` and `{}` on every request, including an empty one. Under a `$set`\n * that wipes a member's skills and every one of their links the moment they\n * edit their display name. `tsc` cannot see it and the request looks correct in\n * the network tab.\n *\n * The rule this comes from: defaults belong on the READ side. A write body\n * states what the caller sent and nothing else.\n */\nexport const updateFounderProfileBodySchema = z.object({\n displayName: z.string().trim().min(1).max(DISPLAY_NAME_MAX).optional(),\n headline: clearableText(HEADLINE_MAX),\n about: clearableText(ABOUT_MAX),\n /** Carries the whole location; `locationSchema` has no default to fill a gap. */\n location: locationSchema.optional(),\n venture: ventureSchema.optional(),\n skills: z.array(skillSchema).max(SKILLS_MAX).optional(),\n goals: z.array(goalSchema).max(GOAL_VALUES.length).optional(),\n /** Sent whole. To drop one link, send the object without that key. */\n links: founderLinksSchema.optional(),\n});\nexport type UpdateFounderProfileBody = z.infer<typeof updateFounderProfileBodySchema>;\n\n/**\n * Username change. Separate from the profile patch because it needs a\n * uniqueness check, and it refuses a reserved handle (`chosenUsernameSchema`).\n */\nexport const updateUsernameBodySchema = z.object({ username: chosenUsernameSchema });\nexport type UpdateUsernameBody = z.infer<typeof updateUsernameBodySchema>;\n\n/** Availability probe behind the username field. */\nexport const usernameAvailabilitySchema = z.object({\n username: usernameSchema,\n available: z.boolean(),\n /** Present when unavailable: `taken` or `reserved`. */\n reason: z.enum(['taken', 'reserved']).optional(),\n});\nexport type UsernameAvailability = z.infer<typeof usernameAvailabilitySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"founder-profile.js","sourceRoot":"","sources":["../../../src/domains/profile/founder-profile.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — The founder profile\n *\n * One member, one profile. The central auth account owns identity (email,\n * verification, deletion); this owns everything Founder Circle shows.\n *\n * @module founder-schema/domains/profile\n */\n\nimport { z } from 'zod';\nimport {\n chosenUsernameSchema,\n GOAL_VALUES,\n goalSchema,\n httpUrlSchema,\n industrySchema,\n locationSchema,\n objectIdSchema,\n optionalTrimmedString,\n skillSchema,\n startupStageSchema,\n usernameSchema,\n} from '../../core/common.js';\nimport {\n ABOUT_MAX,\n DISPLAY_NAME_MAX,\n FOUNDED_YEAR_MIN,\n HEADLINE_MAX,\n SKILLS_MAX,\n TEAM_SIZE_MAX,\n TEAM_SIZE_MIN,\n VENTURE_NAME_MAX,\n VENTURE_TAGLINE_MAX,\n} from '../../core/limits.js';\nimport { VALIDATION_MESSAGE } from '../../core/validation.js';\nimport { mediaImageSchema } from '../media/media.js';\n\n// ─── Venture ─────────────────────────────────────────────────────────────────\n\n/**\n * ⚠ NUMBERS ARE NUMBERS, never coerced. This is a BODY, which arrives as JSON\n * and already carries types. `z.coerce.number()` here accepted `teamSize: true`\n * as 1 and `\" \"` as 0, and made the schema's input type `unknown`, so a form\n * typed against it could put anything in the field. Coercion belongs to query\n * strings, where everything is text; a form converts its text input before it\n * builds the body.\n */\nexport const ventureSchema = z.object({\n name: z.string().trim().min(1).max(VENTURE_NAME_MAX),\n tagline: optionalTrimmedString(VENTURE_TAGLINE_MAX),\n website: httpUrlSchema.optional(),\n industry: industrySchema,\n stage: startupStageSchema,\n /** Team size including the founder. Optional: a solo pre-idea founder has none to report. */\n teamSize: z.number().int().min(TEAM_SIZE_MIN).max(TEAM_SIZE_MAX).optional(),\n /**\n * Not in the future. The bound is read per parse, never captured once.\n *\n * `.max(new Date().getUTCFullYear())` looks equivalent and is not: it is\n * evaluated when this module is imported, so a server process still up on\n * 1 January rejects the new year until somebody restarts it. The backend's\n * Mongoose model deliberately carries no upper bound for the same reason, a\n * `max` there being frozen at schema construction. This is where the ceiling\n * lives.\n */\n foundedYear: z\n .number()\n .int()\n .min(FOUNDED_YEAR_MIN)\n .refine((year) => year <= new Date().getUTCFullYear(), {\n error: VALIDATION_MESSAGE.foundedYearFuture,\n })\n .optional(),\n});\nexport type Venture = z.infer<typeof ventureSchema>;\n\n// ─── A member as other surfaces name them ────────────────────────────────────\n\n/**\n * A member as a byline renders them: on a post (`feedAuthorSchema` is this), a\n * product's team, a review, a notification, the top-helpers list.\n *\n * Every field is LIVE, joined against the profile at read time rather than\n * frozen onto the post or review at write time. So a renamed handle, a new\n * photo or an edited headline updates everywhere, the link always targets the\n * current username, and nothing personal survives in other people's rows once\n * an account is deleted.\n */\nexport const memberBylineSchema = z.object({\n userId: z.string(),\n username: usernameSchema,\n displayName: z.string(),\n /** Server-generated or from the login provider, never member-typed, so a\n * plain URL check. Same reasoning as `avatarUrl` on the profile. */\n avatarUrl: z.url().optional(),\n /** The member's one-line headline, live-joined. */\n headline: z.string().optional(),\n});\nexport type MemberByline = z.infer<typeof memberBylineSchema>;\n\n/**\n * What a member has given the community, on their profile and card.\n *\n * `score` is points earned from OTHER members' recognition only: a product's\n * team marking your feedback helpful, confirming you tested their product,\n * members finding your review helpful, and writing reviews. Nothing a member\n * does to their own product counts, so the score cannot be bought by having a\n * popular one. `helpedCount` is how many different founders recognised that\n * help (\"Helped 12 founders\"). See `domains/contributions`.\n */\nexport const contributionSummarySchema = z.object({\n score: z.number().int().nonnegative(),\n helpedCount: z.number().int().nonnegative(),\n});\nexport type ContributionSummary = z.infer<typeof contributionSummarySchema>;\n\n// ─── Profile ─────────────────────────────────────────────────────────────────\n\n/**\n * The founder's off-platform links. ONE shape: onboarding's contact step picks\n * its three from this, so a link one side knows can never be missing on the\n * other. (It was written twice until 0.2.0, and the profile side lacked\n * Instagram, which onboarding collected.)\n *\n * Defined without a default, because the read side wants one and the write\n * side must not have one; each adds its own.\n */\nexport const founderLinksSchema = z.object({\n linkedin: httpUrlSchema.optional(),\n x: httpUrlSchema.optional(),\n instagram: httpUrlSchema.optional(),\n github: httpUrlSchema.optional(),\n website: httpUrlSchema.optional(),\n});\nexport type FounderLinks = z.infer<typeof founderLinksSchema>;\n\n/**\n * The profile as the owner sees it. A peer sees the redacted projection\n * (`founderCardSchema`), which is built on the server and never by dropping\n * fields on the client.\n */\nexport const founderProfileSchema = z.object({\n id: z.string(),\n userId: z.string(),\n username: usernameSchema,\n displayName: z.string().trim().min(1).max(DISPLAY_NAME_MAX),\n headline: optionalTrimmedString(HEADLINE_MAX),\n about: optionalTrimmedString(ABOUT_MAX),\n /**\n * Plain `z.url()`, NOT `httpUrlSchema`, and the difference is deliberate.\n *\n * No member can write this field: it appears in no write body, and\n * `public-surface.test.ts` asserts that and will fail the day somebody adds it\n * to a form. So the scheme restriction would be defending a field that takes\n * no member input, while refusing hosts we legitimately serve avatars from,\n * our own dev host among them. A read schema that rejects our own response is\n * a self-inflicted outage.\n *\n * ⚠️ That argument holds only while nothing member-supplied reaches this\n * field. Two sources write an avatar today and NEITHER is a member typing a\n * URL: our own upload path, and the central-auth `user.created` /\n * `user.updated` webhook, which carries the Google OAuth picture. If a\n * \"paste a link to your photo\" control is ever added, this goes back to\n * `httpUrlSchema` and the ruling behind it is void.\n *\n * WHICH picture: the member's uploaded photo at its small size (192, see\n * `MEDIA_SIZES.avatar`), else their sign-in provider's picture, else absent.\n * Every surface that shows this member resolves it the same way.\n */\n avatarUrl: z.url().optional(),\n /**\n * The uploaded photo itself, every size, when the member has one. Absent\n * means `avatarUrl` (if any) is the sign-in provider's picture, so a client\n * offers \"Remove photo\" only when this is present. Not on `founderCardSchema`:\n * a card needs one small picture, not the set.\n */\n avatar: mediaImageSchema.optional(),\n location: locationSchema.optional(),\n venture: ventureSchema.optional(),\n skills: z.array(skillSchema).max(SKILLS_MAX).default([]),\n goals: z.array(goalSchema).max(GOAL_VALUES.length).default([]),\n links: founderLinksSchema.default({}),\n /**\n * Their contribution to other founders. Server-computed, never written by a\n * member. REQUIRED, no default: a read-side default would let a server that\n * forgot the field pass as a real zero.\n */\n contribution: contributionSummarySchema,\n /** Onboarding is finished and the member is visible in Discover. */\n isOnboarded: z.boolean().default(false),\n /** Member paused their own profile. Hidden from Discover, keeps their data. */\n isActive: z.boolean().default(true),\n createdAt: z.iso.datetime(),\n updatedAt: z.iso.datetime(),\n});\nexport type FounderProfile = z.infer<typeof founderProfileSchema>;\n\n/**\n * What one founder sees of another in Discover and on a profile page.\n *\n * Built server-side. Keep it a strict subset of `founderProfileSchema` — if a\n * private field ever needs to show here, that is a product decision, not an\n * accident of spreading the full document.\n */\nexport const founderCardSchema = founderProfileSchema.pick({\n id: true,\n /**\n * The MEMBER's id (0.6.0): what a team invitation, a chat and every byline\n * key on. `id` is the profile's own id, which nothing takes as input.\n */\n userId: true,\n username: true,\n displayName: true,\n headline: true,\n avatarUrl: true,\n location: true,\n venture: true,\n skills: true,\n goals: true,\n contribution: true,\n});\nexport type FounderCard = z.infer<typeof founderCardSchema>;\n\n// ─── Write bodies ────────────────────────────────────────────────────────────\n\n/**\n * A text field the member is allowed to empty.\n *\n * Three states, and they are deliberately distinct:\n * absent do not touch this field\n * null clear it (the backend turns this into an `$unset`)\n * \"\" REJECTED\n *\n * The empty string is refused on purpose. A form that forgets to convert a\n * cleared input to `null` then fails loudly at the boundary, instead of being\n * silently dropped and leaving the member staring at text they just deleted.\n * `optionalTrimmedString` does the dropping, which is right for a create body\n * and wrong for a patch.\n */\nconst clearableText = (maxLength: number) =>\n z.string().trim().min(1, VALIDATION_MESSAGE.clearWithNull).max(maxLength).nullable().optional();\n\n/**\n * Patch body for the Me tab. Only the keys the member actually changed.\n *\n * ⚠️ Written as its own object ON PURPOSE. Do NOT rebuild it as\n * `founderProfileSchema.pick({...}).partial()`: `.partial()` does not switch\n * off `.default()`, so `skills`, `goals` and `links` would materialise as\n * `[]`, `[]` and `{}` on every request, including an empty one. Under a `$set`\n * that wipes a member's skills and every one of their links the moment they\n * edit their display name. `tsc` cannot see it and the request looks correct in\n * the network tab.\n *\n * The rule this comes from: defaults belong on the READ side. A write body\n * states what the caller sent and nothing else.\n */\nexport const updateFounderProfileBodySchema = z.object({\n displayName: z.string().trim().min(1).max(DISPLAY_NAME_MAX).optional(),\n headline: clearableText(HEADLINE_MAX),\n about: clearableText(ABOUT_MAX),\n /** Carries the whole location; `locationSchema` has no default to fill a gap. */\n location: locationSchema.optional(),\n venture: ventureSchema.optional(),\n skills: z.array(skillSchema).max(SKILLS_MAX).optional(),\n goals: z.array(goalSchema).max(GOAL_VALUES.length).optional(),\n /** Sent whole. To drop one link, send the object without that key. */\n links: founderLinksSchema.optional(),\n});\nexport type UpdateFounderProfileBody = z.infer<typeof updateFounderProfileBodySchema>;\n\n/**\n * Username change. Separate from the profile patch because it needs a\n * uniqueness check, and it refuses a reserved handle (`chosenUsernameSchema`).\n */\nexport const updateUsernameBodySchema = z.object({ username: chosenUsernameSchema });\nexport type UpdateUsernameBody = z.infer<typeof updateUsernameBodySchema>;\n\n/** Availability probe behind the username field. */\nexport const usernameAvailabilitySchema = z.object({\n username: usernameSchema,\n available: z.boolean(),\n /** Present when unavailable: `taken` or `reserved`. */\n reason: z.enum(['taken', 'reserved']).optional(),\n});\nexport type UsernameAvailability = z.infer<typeof usernameAvailabilitySchema>;\n\n// ─── The member's own photo ──────────────────────────────────────────────────\n\n/**\n * `PUT /api/v1/profiles/me/avatar`: use a completed upload (purpose `avatar`)\n * as the member's photo. Answers with the updated `founderProfileSchema`.\n *\n * The previous photo is deleted from storage, not merely unlinked, so its old\n * address stops working. `DELETE /api/v1/profiles/me/avatar` removes the photo\n * the same way and the member falls back to their sign-in provider's picture.\n */\nexport const setAvatarBodySchema = z.object({ mediaId: objectIdSchema });\nexport type SetAvatarBody = z.infer<typeof setAvatarBodySchema>;\n"]}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Reviews: stars and words on a product
|
|
3
|
+
*
|
|
4
|
+
* One review per member per product: 1 to 5 stars AND text of at least
|
|
5
|
+
* `REVIEW_TEXT_MIN` characters, because a star with no reason does not help
|
|
6
|
+
* the founder. Nobody on a product's team reviews it. The writer may edit it
|
|
7
|
+
* any time (it then shows as edited) or delete it. The team may answer each
|
|
8
|
+
* review once, publicly; other members may find it Helpful.
|
|
9
|
+
*
|
|
10
|
+
* `testedIt` marks a review by a member the team confirmed as a tester of the
|
|
11
|
+
* product, so a reader can tell a review from use from a review from a glance.
|
|
12
|
+
*
|
|
13
|
+
* The product's average shows from `REVIEW_AVERAGE_MIN_COUNT` reviews. Hidden
|
|
14
|
+
* reviews (staff) are not counted.
|
|
15
|
+
*
|
|
16
|
+
* Routes (all signed in):
|
|
17
|
+
* GET /api/v1/products/:productId/reviews reviewsQuerySchema -> reviewPageSchema
|
|
18
|
+
* POST /api/v1/products/:productId/reviews createReviewBodySchema -> productReviewSchema
|
|
19
|
+
* GET /api/v1/reviews/:reviewId -> productReviewSchema (yours, to edit it)
|
|
20
|
+
* PATCH /api/v1/reviews/:reviewId updateReviewBodySchema -> productReviewSchema
|
|
21
|
+
* DELETE /api/v1/reviews/:reviewId -> IdResponse
|
|
22
|
+
* PUT /api/v1/reviews/:reviewId/reply reviewReplyBodySchema -> productReviewSchema
|
|
23
|
+
* DELETE /api/v1/reviews/:reviewId/reply -> productReviewSchema
|
|
24
|
+
* PUT /api/v1/reviews/:reviewId/helpful -> reviewHelpfulResponseSchema
|
|
25
|
+
* DELETE /api/v1/reviews/:reviewId/helpful -> reviewHelpfulResponseSchema
|
|
26
|
+
* POST /api/v1/reviews/:reviewId/report createReportBodySchema -> CreateReportResponse
|
|
27
|
+
*
|
|
28
|
+
* @module founder-schema/domains/reviews
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
/** Whole stars. */
|
|
32
|
+
export declare const reviewRatingSchema: z.ZodNumber;
|
|
33
|
+
export type ReviewRating = z.infer<typeof reviewRatingSchema>;
|
|
34
|
+
/** `new` (the default) or `helpful` first. */
|
|
35
|
+
export declare const REVIEW_SORT_VALUES: readonly ["new", "helpful"];
|
|
36
|
+
export declare const reviewSortSchema: z.ZodEnum<{
|
|
37
|
+
new: "new";
|
|
38
|
+
helpful: "helpful";
|
|
39
|
+
}>;
|
|
40
|
+
export type ReviewSort = z.infer<typeof reviewSortSchema>;
|
|
41
|
+
/** The team's public answer to a review. */
|
|
42
|
+
export declare const reviewReplySchema: z.ZodObject<{
|
|
43
|
+
text: z.ZodString;
|
|
44
|
+
author: z.ZodNullable<z.ZodObject<{
|
|
45
|
+
userId: z.ZodString;
|
|
46
|
+
username: z.ZodString;
|
|
47
|
+
displayName: z.ZodString;
|
|
48
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
49
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
50
|
+
}, z.core.$strip>>;
|
|
51
|
+
createdAt: z.ZodISODateTime;
|
|
52
|
+
editedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
export type ReviewReply = z.infer<typeof reviewReplySchema>;
|
|
55
|
+
/** What this viewer may do with a review, decided by the server. */
|
|
56
|
+
export declare const reviewViewerSchema: z.ZodObject<{
|
|
57
|
+
isAuthor: z.ZodBoolean;
|
|
58
|
+
votedHelpful: z.ZodBoolean;
|
|
59
|
+
canEdit: z.ZodBoolean;
|
|
60
|
+
canDelete: z.ZodBoolean;
|
|
61
|
+
canReply: z.ZodBoolean;
|
|
62
|
+
canVoteHelpful: z.ZodBoolean;
|
|
63
|
+
canReport: z.ZodBoolean;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
export type ReviewViewer = z.infer<typeof reviewViewerSchema>;
|
|
66
|
+
export declare const productReviewSchema: z.ZodObject<{
|
|
67
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
68
|
+
productId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
69
|
+
author: z.ZodObject<{
|
|
70
|
+
userId: z.ZodString;
|
|
71
|
+
username: z.ZodString;
|
|
72
|
+
displayName: z.ZodString;
|
|
73
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
74
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
75
|
+
}, z.core.$strip>;
|
|
76
|
+
rating: z.ZodNumber;
|
|
77
|
+
text: z.ZodString;
|
|
78
|
+
testedIt: z.ZodBoolean;
|
|
79
|
+
helpfulCount: z.ZodNumber;
|
|
80
|
+
reply: z.ZodNullable<z.ZodObject<{
|
|
81
|
+
text: z.ZodString;
|
|
82
|
+
author: z.ZodNullable<z.ZodObject<{
|
|
83
|
+
userId: z.ZodString;
|
|
84
|
+
username: z.ZodString;
|
|
85
|
+
displayName: z.ZodString;
|
|
86
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
87
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
88
|
+
}, z.core.$strip>>;
|
|
89
|
+
createdAt: z.ZodISODateTime;
|
|
90
|
+
editedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
91
|
+
}, z.core.$strip>>;
|
|
92
|
+
createdAt: z.ZodISODateTime;
|
|
93
|
+
editedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
94
|
+
viewer: z.ZodOptional<z.ZodObject<{
|
|
95
|
+
isAuthor: z.ZodBoolean;
|
|
96
|
+
votedHelpful: z.ZodBoolean;
|
|
97
|
+
canEdit: z.ZodBoolean;
|
|
98
|
+
canDelete: z.ZodBoolean;
|
|
99
|
+
canReply: z.ZodBoolean;
|
|
100
|
+
canVoteHelpful: z.ZodBoolean;
|
|
101
|
+
canReport: z.ZodBoolean;
|
|
102
|
+
}, z.core.$strip>>;
|
|
103
|
+
}, z.core.$strip>;
|
|
104
|
+
export type ProductReview = z.infer<typeof productReviewSchema>;
|
|
105
|
+
export declare const reviewPageSchema: z.ZodObject<{
|
|
106
|
+
items: z.ZodArray<z.ZodObject<{
|
|
107
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
108
|
+
productId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
109
|
+
author: z.ZodObject<{
|
|
110
|
+
userId: z.ZodString;
|
|
111
|
+
username: z.ZodString;
|
|
112
|
+
displayName: z.ZodString;
|
|
113
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
114
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
115
|
+
}, z.core.$strip>;
|
|
116
|
+
rating: z.ZodNumber;
|
|
117
|
+
text: z.ZodString;
|
|
118
|
+
testedIt: z.ZodBoolean;
|
|
119
|
+
helpfulCount: z.ZodNumber;
|
|
120
|
+
reply: z.ZodNullable<z.ZodObject<{
|
|
121
|
+
text: z.ZodString;
|
|
122
|
+
author: z.ZodNullable<z.ZodObject<{
|
|
123
|
+
userId: z.ZodString;
|
|
124
|
+
username: z.ZodString;
|
|
125
|
+
displayName: z.ZodString;
|
|
126
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
127
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
128
|
+
}, z.core.$strip>>;
|
|
129
|
+
createdAt: z.ZodISODateTime;
|
|
130
|
+
editedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
131
|
+
}, z.core.$strip>>;
|
|
132
|
+
createdAt: z.ZodISODateTime;
|
|
133
|
+
editedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
134
|
+
viewer: z.ZodOptional<z.ZodObject<{
|
|
135
|
+
isAuthor: z.ZodBoolean;
|
|
136
|
+
votedHelpful: z.ZodBoolean;
|
|
137
|
+
canEdit: z.ZodBoolean;
|
|
138
|
+
canDelete: z.ZodBoolean;
|
|
139
|
+
canReply: z.ZodBoolean;
|
|
140
|
+
canVoteHelpful: z.ZodBoolean;
|
|
141
|
+
canReport: z.ZodBoolean;
|
|
142
|
+
}, z.core.$strip>>;
|
|
143
|
+
}, z.core.$strip>>;
|
|
144
|
+
nextCursor: z.ZodNullable<z.ZodString>;
|
|
145
|
+
}, z.core.$strip>;
|
|
146
|
+
export type ReviewPage = z.infer<typeof reviewPageSchema>;
|
|
147
|
+
export declare const reviewsQuerySchema: z.ZodObject<{
|
|
148
|
+
limit: z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
|
|
149
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
150
|
+
sort: z.ZodPreprocess<z.ZodDefault<z.ZodEnum<{
|
|
151
|
+
new: "new";
|
|
152
|
+
helpful: "helpful";
|
|
153
|
+
}>>, unknown>;
|
|
154
|
+
}, z.core.$strip>;
|
|
155
|
+
export type ReviewsQuery = z.infer<typeof reviewsQuerySchema>;
|
|
156
|
+
export type ReviewsQueryInput = z.input<typeof reviewsQuerySchema>;
|
|
157
|
+
export declare const reviewParamsSchema: z.ZodObject<{
|
|
158
|
+
reviewId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
159
|
+
}, z.core.$strip>;
|
|
160
|
+
export type ReviewParams = z.infer<typeof reviewParamsSchema>;
|
|
161
|
+
export declare const createReviewBodySchema: z.ZodObject<{
|
|
162
|
+
rating: z.ZodNumber;
|
|
163
|
+
text: z.ZodString;
|
|
164
|
+
}, z.core.$strip>;
|
|
165
|
+
export type CreateReviewBody = z.infer<typeof createReviewBodySchema>;
|
|
166
|
+
/** Only what changed. An empty body changes nothing and answers the review as it is. */
|
|
167
|
+
export declare const updateReviewBodySchema: z.ZodObject<{
|
|
168
|
+
rating: z.ZodOptional<z.ZodNumber>;
|
|
169
|
+
text: z.ZodOptional<z.ZodString>;
|
|
170
|
+
}, z.core.$strip>;
|
|
171
|
+
export type UpdateReviewBody = z.infer<typeof updateReviewBodySchema>;
|
|
172
|
+
/** The team's reply: written once, then edited in place by sending it again. */
|
|
173
|
+
export declare const reviewReplyBodySchema: z.ZodObject<{
|
|
174
|
+
text: z.ZodString;
|
|
175
|
+
}, z.core.$strip>;
|
|
176
|
+
export type ReviewReplyBody = z.infer<typeof reviewReplyBodySchema>;
|
|
177
|
+
/** `PUT|DELETE /reviews/:id/helpful`: the state it settled on and the fresh count. */
|
|
178
|
+
export declare const reviewHelpfulResponseSchema: z.ZodObject<{
|
|
179
|
+
reviewId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
180
|
+
votedHelpful: z.ZodBoolean;
|
|
181
|
+
helpfulCount: z.ZodNumber;
|
|
182
|
+
}, z.core.$strip>;
|
|
183
|
+
export type ReviewHelpfulResponse = z.infer<typeof reviewHelpfulResponseSchema>;
|
|
184
|
+
//# sourceMappingURL=reviews.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviews.d.ts","sourceRoot":"","sources":["../../../src/domains/reviews/reviews.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,mBAAmB;AACnB,eAAO,MAAM,kBAAkB,aAAiC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,6BAA8B,CAAC;AAC9D,eAAO,MAAM,gBAAgB;;;EAA6B,CAAC;AAC3D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAM5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,oEAAoE;AACpE,eAAO,MAAM,kBAAkB;;;;;;;;iBAU7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAa9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAuC,CAAC;AACrE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,kBAAkB;;;;;;;iBAE7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEnE,eAAO,MAAM,kBAAkB;;iBAAyC,CAAC;AACzE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,eAAO,MAAM,sBAAsB;;;iBAGjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,wFAAwF;AACxF,eAAO,MAAM,sBAAsB;;;iBAGjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,gFAAgF;AAChF,eAAO,MAAM,qBAAqB;;iBAEhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,sFAAsF;AACtF,eAAO,MAAM,2BAA2B;;;;iBAItC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Reviews: stars and words on a product
|
|
3
|
+
*
|
|
4
|
+
* One review per member per product: 1 to 5 stars AND text of at least
|
|
5
|
+
* `REVIEW_TEXT_MIN` characters, because a star with no reason does not help
|
|
6
|
+
* the founder. Nobody on a product's team reviews it. The writer may edit it
|
|
7
|
+
* any time (it then shows as edited) or delete it. The team may answer each
|
|
8
|
+
* review once, publicly; other members may find it Helpful.
|
|
9
|
+
*
|
|
10
|
+
* `testedIt` marks a review by a member the team confirmed as a tester of the
|
|
11
|
+
* product, so a reader can tell a review from use from a review from a glance.
|
|
12
|
+
*
|
|
13
|
+
* The product's average shows from `REVIEW_AVERAGE_MIN_COUNT` reviews. Hidden
|
|
14
|
+
* reviews (staff) are not counted.
|
|
15
|
+
*
|
|
16
|
+
* Routes (all signed in):
|
|
17
|
+
* GET /api/v1/products/:productId/reviews reviewsQuerySchema -> reviewPageSchema
|
|
18
|
+
* POST /api/v1/products/:productId/reviews createReviewBodySchema -> productReviewSchema
|
|
19
|
+
* GET /api/v1/reviews/:reviewId -> productReviewSchema (yours, to edit it)
|
|
20
|
+
* PATCH /api/v1/reviews/:reviewId updateReviewBodySchema -> productReviewSchema
|
|
21
|
+
* DELETE /api/v1/reviews/:reviewId -> IdResponse
|
|
22
|
+
* PUT /api/v1/reviews/:reviewId/reply reviewReplyBodySchema -> productReviewSchema
|
|
23
|
+
* DELETE /api/v1/reviews/:reviewId/reply -> productReviewSchema
|
|
24
|
+
* PUT /api/v1/reviews/:reviewId/helpful -> reviewHelpfulResponseSchema
|
|
25
|
+
* DELETE /api/v1/reviews/:reviewId/helpful -> reviewHelpfulResponseSchema
|
|
26
|
+
* POST /api/v1/reviews/:reviewId/report createReportBodySchema -> CreateReportResponse
|
|
27
|
+
*
|
|
28
|
+
* @module founder-schema/domains/reviews
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { emptyStringToUndefined, objectIdSchema, paginationQuerySchema, } from '../../core/common.js';
|
|
32
|
+
import { REVIEW_REPLY_MAX, REVIEW_TEXT_MAX, REVIEW_TEXT_MIN } from '../../core/limits.js';
|
|
33
|
+
import { paginatedSchema } from '../../core/responses.js';
|
|
34
|
+
import { memberBylineSchema } from '../profile/founder-profile.js';
|
|
35
|
+
/** Whole stars. */
|
|
36
|
+
export const reviewRatingSchema = z.number().int().min(1).max(5);
|
|
37
|
+
/** `new` (the default) or `helpful` first. */
|
|
38
|
+
export const REVIEW_SORT_VALUES = ['new', 'helpful'];
|
|
39
|
+
export const reviewSortSchema = z.enum(REVIEW_SORT_VALUES);
|
|
40
|
+
/** The team's public answer to a review. */
|
|
41
|
+
export const reviewReplySchema = z.object({
|
|
42
|
+
text: z.string().max(REVIEW_REPLY_MAX),
|
|
43
|
+
/** Who on the team wrote it; null when they have since deleted their account. */
|
|
44
|
+
author: memberBylineSchema.nullable(),
|
|
45
|
+
createdAt: z.iso.datetime(),
|
|
46
|
+
editedAt: z.iso.datetime().nullable(),
|
|
47
|
+
});
|
|
48
|
+
/** What this viewer may do with a review, decided by the server. */
|
|
49
|
+
export const reviewViewerSchema = z.object({
|
|
50
|
+
isAuthor: z.boolean(),
|
|
51
|
+
votedHelpful: z.boolean(),
|
|
52
|
+
canEdit: z.boolean(),
|
|
53
|
+
canDelete: z.boolean(),
|
|
54
|
+
/** On the product's team: write, edit or remove the team's reply. */
|
|
55
|
+
canReply: z.boolean(),
|
|
56
|
+
/** Not the writer. */
|
|
57
|
+
canVoteHelpful: z.boolean(),
|
|
58
|
+
canReport: z.boolean(),
|
|
59
|
+
});
|
|
60
|
+
export const productReviewSchema = z.object({
|
|
61
|
+
id: objectIdSchema,
|
|
62
|
+
productId: objectIdSchema,
|
|
63
|
+
author: memberBylineSchema,
|
|
64
|
+
rating: reviewRatingSchema,
|
|
65
|
+
text: z.string().max(REVIEW_TEXT_MAX),
|
|
66
|
+
/** The team confirmed the writer as a tester of this product. */
|
|
67
|
+
testedIt: z.boolean(),
|
|
68
|
+
helpfulCount: z.number().int().nonnegative(),
|
|
69
|
+
reply: reviewReplySchema.nullable(),
|
|
70
|
+
createdAt: z.iso.datetime(),
|
|
71
|
+
editedAt: z.iso.datetime().nullable(),
|
|
72
|
+
viewer: reviewViewerSchema.optional(),
|
|
73
|
+
});
|
|
74
|
+
export const reviewPageSchema = paginatedSchema(productReviewSchema);
|
|
75
|
+
export const reviewsQuerySchema = paginationQuerySchema.extend({
|
|
76
|
+
sort: z.preprocess(emptyStringToUndefined, reviewSortSchema.default('new')),
|
|
77
|
+
});
|
|
78
|
+
export const reviewParamsSchema = z.object({ reviewId: objectIdSchema });
|
|
79
|
+
const reviewTextSchema = z.string().trim().min(REVIEW_TEXT_MIN).max(REVIEW_TEXT_MAX);
|
|
80
|
+
export const createReviewBodySchema = z.object({
|
|
81
|
+
rating: reviewRatingSchema,
|
|
82
|
+
text: reviewTextSchema,
|
|
83
|
+
});
|
|
84
|
+
/** Only what changed. An empty body changes nothing and answers the review as it is. */
|
|
85
|
+
export const updateReviewBodySchema = z.object({
|
|
86
|
+
rating: reviewRatingSchema.optional(),
|
|
87
|
+
text: reviewTextSchema.optional(),
|
|
88
|
+
});
|
|
89
|
+
/** The team's reply: written once, then edited in place by sending it again. */
|
|
90
|
+
export const reviewReplyBodySchema = z.object({
|
|
91
|
+
text: z.string().trim().min(1).max(REVIEW_REPLY_MAX),
|
|
92
|
+
});
|
|
93
|
+
/** `PUT|DELETE /reviews/:id/helpful`: the state it settled on and the fresh count. */
|
|
94
|
+
export const reviewHelpfulResponseSchema = z.object({
|
|
95
|
+
reviewId: objectIdSchema,
|
|
96
|
+
votedHelpful: z.boolean(),
|
|
97
|
+
helpfulCount: z.number().int().nonnegative(),
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=reviews.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviews.js","sourceRoot":"","sources":["../../../src/domains/reviews/reviews.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — Reviews: stars and words on a product\n *\n * One review per member per product: 1 to 5 stars AND text of at least\n * `REVIEW_TEXT_MIN` characters, because a star with no reason does not help\n * the founder. Nobody on a product's team reviews it. The writer may edit it\n * any time (it then shows as edited) or delete it. The team may answer each\n * review once, publicly; other members may find it Helpful.\n *\n * `testedIt` marks a review by a member the team confirmed as a tester of the\n * product, so a reader can tell a review from use from a review from a glance.\n *\n * The product's average shows from `REVIEW_AVERAGE_MIN_COUNT` reviews. Hidden\n * reviews (staff) are not counted.\n *\n * Routes (all signed in):\n * GET /api/v1/products/:productId/reviews reviewsQuerySchema -> reviewPageSchema\n * POST /api/v1/products/:productId/reviews createReviewBodySchema -> productReviewSchema\n * GET /api/v1/reviews/:reviewId -> productReviewSchema (yours, to edit it)\n * PATCH /api/v1/reviews/:reviewId updateReviewBodySchema -> productReviewSchema\n * DELETE /api/v1/reviews/:reviewId -> IdResponse\n * PUT /api/v1/reviews/:reviewId/reply reviewReplyBodySchema -> productReviewSchema\n * DELETE /api/v1/reviews/:reviewId/reply -> productReviewSchema\n * PUT /api/v1/reviews/:reviewId/helpful -> reviewHelpfulResponseSchema\n * DELETE /api/v1/reviews/:reviewId/helpful -> reviewHelpfulResponseSchema\n * POST /api/v1/reviews/:reviewId/report createReportBodySchema -> CreateReportResponse\n *\n * @module founder-schema/domains/reviews\n */\n\nimport { z } from 'zod';\nimport {\n emptyStringToUndefined,\n objectIdSchema,\n paginationQuerySchema,\n} from '../../core/common.js';\nimport { REVIEW_REPLY_MAX, REVIEW_TEXT_MAX, REVIEW_TEXT_MIN } from '../../core/limits.js';\nimport { paginatedSchema } from '../../core/responses.js';\nimport { memberBylineSchema } from '../profile/founder-profile.js';\n\n/** Whole stars. */\nexport const reviewRatingSchema = z.number().int().min(1).max(5);\nexport type ReviewRating = z.infer<typeof reviewRatingSchema>;\n\n/** `new` (the default) or `helpful` first. */\nexport const REVIEW_SORT_VALUES = ['new', 'helpful'] as const;\nexport const reviewSortSchema = z.enum(REVIEW_SORT_VALUES);\nexport type ReviewSort = z.infer<typeof reviewSortSchema>;\n\n/** The team's public answer to a review. */\nexport const reviewReplySchema = z.object({\n text: z.string().max(REVIEW_REPLY_MAX),\n /** Who on the team wrote it; null when they have since deleted their account. */\n author: memberBylineSchema.nullable(),\n createdAt: z.iso.datetime(),\n editedAt: z.iso.datetime().nullable(),\n});\nexport type ReviewReply = z.infer<typeof reviewReplySchema>;\n\n/** What this viewer may do with a review, decided by the server. */\nexport const reviewViewerSchema = z.object({\n isAuthor: z.boolean(),\n votedHelpful: z.boolean(),\n canEdit: z.boolean(),\n canDelete: z.boolean(),\n /** On the product's team: write, edit or remove the team's reply. */\n canReply: z.boolean(),\n /** Not the writer. */\n canVoteHelpful: z.boolean(),\n canReport: z.boolean(),\n});\nexport type ReviewViewer = z.infer<typeof reviewViewerSchema>;\n\nexport const productReviewSchema = z.object({\n id: objectIdSchema,\n productId: objectIdSchema,\n author: memberBylineSchema,\n rating: reviewRatingSchema,\n text: z.string().max(REVIEW_TEXT_MAX),\n /** The team confirmed the writer as a tester of this product. */\n testedIt: z.boolean(),\n helpfulCount: z.number().int().nonnegative(),\n reply: reviewReplySchema.nullable(),\n createdAt: z.iso.datetime(),\n editedAt: z.iso.datetime().nullable(),\n viewer: reviewViewerSchema.optional(),\n});\nexport type ProductReview = z.infer<typeof productReviewSchema>;\n\nexport const reviewPageSchema = paginatedSchema(productReviewSchema);\nexport type ReviewPage = z.infer<typeof reviewPageSchema>;\n\nexport const reviewsQuerySchema = paginationQuerySchema.extend({\n sort: z.preprocess(emptyStringToUndefined, reviewSortSchema.default('new')),\n});\nexport type ReviewsQuery = z.infer<typeof reviewsQuerySchema>;\nexport type ReviewsQueryInput = z.input<typeof reviewsQuerySchema>;\n\nexport const reviewParamsSchema = z.object({ reviewId: objectIdSchema });\nexport type ReviewParams = z.infer<typeof reviewParamsSchema>;\n\nconst reviewTextSchema = z.string().trim().min(REVIEW_TEXT_MIN).max(REVIEW_TEXT_MAX);\n\nexport const createReviewBodySchema = z.object({\n rating: reviewRatingSchema,\n text: reviewTextSchema,\n});\nexport type CreateReviewBody = z.infer<typeof createReviewBodySchema>;\n\n/** Only what changed. An empty body changes nothing and answers the review as it is. */\nexport const updateReviewBodySchema = z.object({\n rating: reviewRatingSchema.optional(),\n text: reviewTextSchema.optional(),\n});\nexport type UpdateReviewBody = z.infer<typeof updateReviewBodySchema>;\n\n/** The team's reply: written once, then edited in place by sending it again. */\nexport const reviewReplyBodySchema = z.object({\n text: z.string().trim().min(1).max(REVIEW_REPLY_MAX),\n});\nexport type ReviewReplyBody = z.infer<typeof reviewReplyBodySchema>;\n\n/** `PUT|DELETE /reviews/:id/helpful`: the state it settled on and the fresh count. */\nexport const reviewHelpfulResponseSchema = z.object({\n reviewId: objectIdSchema,\n votedHelpful: z.boolean(),\n helpfulCount: z.number().int().nonnegative(),\n});\nexport type ReviewHelpfulResponse = z.infer<typeof reviewHelpfulResponseSchema>;\n"]}
|