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
package/dist/core/validation.js
CHANGED
|
@@ -57,6 +57,8 @@ export const VALIDATION_MESSAGE = {
|
|
|
57
57
|
foundedYearFuture: 'foundedYearFuture',
|
|
58
58
|
/** An upload key that is not one this product mints. */
|
|
59
59
|
uploadKeyInvalid: 'uploadKeyInvalid',
|
|
60
|
+
/** The same uploaded image attached twice to one post. */
|
|
61
|
+
mediaDuplicate: 'mediaDuplicate',
|
|
60
62
|
/** A post with neither text nor media. */
|
|
61
63
|
postEmpty: 'postEmpty',
|
|
62
64
|
/** Two poll options with the same label. */
|
|
@@ -77,6 +79,20 @@ export const VALIDATION_MESSAGE = {
|
|
|
77
79
|
mpinRequired: 'mpinRequired',
|
|
78
80
|
/** The same role twice in one list. */
|
|
79
81
|
rolesDuplicate: 'rolesDuplicate',
|
|
82
|
+
/** An ask ("testers wanted") on a post or poll that names no product. */
|
|
83
|
+
askWithoutProduct: 'askWithoutProduct',
|
|
84
|
+
/** An App Store link that is not on apps.apple.com. */
|
|
85
|
+
appStoreLinkHost: 'appStoreLinkHost',
|
|
86
|
+
/** A Play Store link that is not on play.google.com. */
|
|
87
|
+
playStoreLinkHost: 'playStoreLinkHost',
|
|
88
|
+
/** A GitHub link that is not on github.com. */
|
|
89
|
+
githubLinkHost: 'githubLinkHost',
|
|
90
|
+
/** A demo video that is not a YouTube link. Video is shared, never uploaded. */
|
|
91
|
+
videoLinkHost: 'videoLinkHost',
|
|
92
|
+
/** The same screenshot twice in one gallery. */
|
|
93
|
+
screenshotsDuplicate: 'screenshotsDuplicate',
|
|
94
|
+
/** The same platform twice in one list. */
|
|
95
|
+
platformsDuplicate: 'platformsDuplicate',
|
|
80
96
|
};
|
|
81
97
|
/**
|
|
82
98
|
* Which part of the request an issue is about.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/core/validation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — How a validation failure is described, on both sides\n *\n * TWO RULES, and they are what let the web client show any error, its own or\n * the server's, on the right field in the member's language:\n *\n * 1. A CUSTOM MESSAGE IS A KEY, NEVER A SENTENCE. Where a schema needs its own\n * message (a refine, a regex), the message is one of `VALIDATION_MESSAGE`,\n * such as `phoneInvalid`. The client turns the key into text in English or\n * Hindi. Before 0.2.0 these were English sentences, so a Hindi screen showed\n * an English error, and a client that wanted to translate one had to match\n * on the wording.\n *\n * Plain bounds carry NO custom message. `.min(1)` and `.max(80)` produce\n * Zod's own `too_small` / `too_big` issues, which already say which bound\n * and what it was (`minimum`, `maximum`, `origin`), and the client words\n * them from those. A hand-written \"at most 80 characters\" would only\n * restate the number in one language.\n *\n * 2. THE SERVER SENDS EVERY ISSUE, IN THIS SHAPE. A 400 answers with\n * `error.details.issues`, built by `toValidationIssues`, so the client can\n * put each one under its field. Until 0.2.0 the backend sent only the\n * first issue's message, with no path at all.\n *\n * ⚠️ `toValidationIssues` COPIES A WHITELIST OF FIELDS, it never spreads an\n * issue. Zod can carry the rejected `input` on an issue, and these bodies hold a\n * date of birth and a phone number. Echoing them into an error response sends\n * them back to the client, into logs and into any proxy in between. A test\n * proves `input` never survives.\n *\n * @module founder-schema/core/validation\n */\n\nimport { z } from 'zod';\n\n/**\n * Every custom message a schema in this package can produce. Each is a\n * translation key the clients define in every language they ship.\n *\n * ⚠️ Append only, and add the translation in the same change. Renaming one\n * shows the raw key to every member until each client ships the new name.\n */\nexport const VALIDATION_MESSAGE = {\n /** Not a 24-hex Mongo id. */\n invalidId: 'invalidId',\n /** Handle: wrong characters, or a dot in the wrong place. */\n usernameInvalid: 'usernameInvalid',\n /** Not a 10-digit Indian mobile number. */\n phoneInvalid: 'phoneInvalid',\n /** Not a `YYYY-MM-DD` date. */\n dateInvalid: 'dateInvalid',\n /** A date that does not exist, such as 30 February. */\n dobInvalid: 'dobInvalid',\n /** Under the minimum age. */\n dobUnderAge: 'dobUnderAge',\n /** Over the sanity ceiling: almost always a mistyped year. */\n dobTooOld: 'dobTooOld',\n /** A founding year after this year. */\n foundedYearFuture: 'foundedYearFuture',\n /** An upload key that is not one this product mints. */\n uploadKeyInvalid: 'uploadKeyInvalid',\n /** A post with neither text nor media. */\n postEmpty: 'postEmpty',\n /** Two poll options with the same label. */\n pollOptionsDuplicate: 'pollOptionsDuplicate',\n /** An empty string sent to clear a patch field; clearing is `null`. */\n clearWithNull: 'clearWithNull',\n /** Not a plain http(s) URL on a real domain (`javascript:`, `localhost`, an IP...). */\n urlInvalid: 'urlInvalid',\n /** A handle this product keeps for itself (`admin`, `support`...). */\n usernameReserved: 'usernameReserved',\n /** Agreed to a version of the guidelines that is not the current one. */\n guidelinesOutdated: 'guidelinesOutdated',\n /** An MPIN that is not exactly six digits. */\n mpinInvalid: 'mpinInvalid',\n /** The MPIN and its confirmation differ. */\n mpinMismatch: 'mpinMismatch',\n /** Switching to a staff role without the MPIN. */\n mpinRequired: 'mpinRequired',\n /** The same role twice in one list. */\n rolesDuplicate: 'rolesDuplicate',\n} as const;\nexport type ValidationMessageKey = (typeof VALIDATION_MESSAGE)[keyof typeof VALIDATION_MESSAGE];\n\n/**\n * Which part of the request an issue is about.\n *\n * ONE PATH CONVENTION: `path` is ALWAYS relative to this part, so a body field\n * reads `['venture', 'name']` with `location: 'body'`, never\n * `['body', 'venture', 'name']`. A form maps body issues straight onto its\n * fields without stripping a prefix, and a client that sees `query` or\n * `params` knows the problem is the URL, not anything the member typed.\n */\nexport const REQUEST_PART_VALUES = ['body', 'query', 'params'] as const;\nexport const requestPartSchema = z.enum(REQUEST_PART_VALUES);\nexport type RequestPart = z.infer<typeof requestPartSchema>;\n\n/**\n * One problem with one field, as it crosses the wire.\n *\n * `code` is Zod's issue code (`too_small`, `too_big`, `invalid_type`,\n * `invalid_format`, `custom`...). `message` is either one of\n * `VALIDATION_MESSAGE` or, for a plain bound, Zod's own English text, which the\n * client ignores in favour of `code` plus `minimum` / `maximum`.\n */\nexport const validationIssueSchema = z.object({\n /** Set by the server on every issue it sends; absent on a client's own parse. */\n location: requestPartSchema.optional(),\n /**\n * Where the problem is, RELATIVE TO `location`: `['venture', 'name']`,\n * `['options', 2, 'label']`, `['contentId']`.\n */\n path: z.array(z.union([z.string(), z.number()])),\n code: z.string(),\n message: z.string(),\n /** `too_small`: the bound that was missed. */\n minimum: z.number().optional(),\n /** `too_big`: the bound that was passed. */\n maximum: z.number().optional(),\n /** What was measured: `string`, `array`, `number`... */\n origin: z.string().optional(),\n /** `invalid_format`: which format, e.g. `url`, `regex`, `email`. */\n format: z.string().optional(),\n});\nexport type ValidationIssue = z.infer<typeof validationIssueSchema>;\n\n/** The loosest shape this needs from a Zod issue, so it works across 4.x. */\ninterface ZodIssueLike {\n path: readonly PropertyKey[];\n code: string;\n message: string;\n minimum?: unknown;\n maximum?: unknown;\n origin?: unknown;\n format?: unknown;\n}\n\nconst toNumber = (v: unknown): number | undefined =>\n typeof v === 'number' ? v : typeof v === 'bigint' ? Number(v) : undefined;\n\n/**\n * A Zod error as the wire shape. Use this on the server for every 400, and on\n * the client to treat its own errors and the server's the same way.\n *\n * `location` is stamped on every issue when given; the server always gives it.\n *\n * Copies a WHITELIST of fields (see the module note). A path segment that is a\n * symbol, which JSON cannot carry, becomes its description.\n */\nexport function toValidationIssues(\n error: { issues: readonly ZodIssueLike[] },\n location?: RequestPart,\n): ValidationIssue[] {\n return error.issues.map((issue) => {\n const out: ValidationIssue = {\n ...(location ? { location } : {}),\n path: issue.path.map((p) =>\n typeof p === 'number' ? p : typeof p === 'string' ? p : String(p.description ?? ''),\n ),\n code: issue.code,\n message: issue.message,\n };\n const minimum = toNumber(issue.minimum);\n const maximum = toNumber(issue.maximum);\n if (minimum !== undefined) out.minimum = minimum;\n if (maximum !== undefined) out.maximum = maximum;\n if (typeof issue.origin === 'string') out.origin = issue.origin;\n if (typeof issue.format === 'string') out.format = issue.format;\n return out;\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/core/validation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — How a validation failure is described, on both sides\n *\n * TWO RULES, and they are what let the web client show any error, its own or\n * the server's, on the right field in the member's language:\n *\n * 1. A CUSTOM MESSAGE IS A KEY, NEVER A SENTENCE. Where a schema needs its own\n * message (a refine, a regex), the message is one of `VALIDATION_MESSAGE`,\n * such as `phoneInvalid`. The client turns the key into text in English or\n * Hindi. Before 0.2.0 these were English sentences, so a Hindi screen showed\n * an English error, and a client that wanted to translate one had to match\n * on the wording.\n *\n * Plain bounds carry NO custom message. `.min(1)` and `.max(80)` produce\n * Zod's own `too_small` / `too_big` issues, which already say which bound\n * and what it was (`minimum`, `maximum`, `origin`), and the client words\n * them from those. A hand-written \"at most 80 characters\" would only\n * restate the number in one language.\n *\n * 2. THE SERVER SENDS EVERY ISSUE, IN THIS SHAPE. A 400 answers with\n * `error.details.issues`, built by `toValidationIssues`, so the client can\n * put each one under its field. Until 0.2.0 the backend sent only the\n * first issue's message, with no path at all.\n *\n * ⚠️ `toValidationIssues` COPIES A WHITELIST OF FIELDS, it never spreads an\n * issue. Zod can carry the rejected `input` on an issue, and these bodies hold a\n * date of birth and a phone number. Echoing them into an error response sends\n * them back to the client, into logs and into any proxy in between. A test\n * proves `input` never survives.\n *\n * @module founder-schema/core/validation\n */\n\nimport { z } from 'zod';\n\n/**\n * Every custom message a schema in this package can produce. Each is a\n * translation key the clients define in every language they ship.\n *\n * ⚠️ Append only, and add the translation in the same change. Renaming one\n * shows the raw key to every member until each client ships the new name.\n */\nexport const VALIDATION_MESSAGE = {\n /** Not a 24-hex Mongo id. */\n invalidId: 'invalidId',\n /** Handle: wrong characters, or a dot in the wrong place. */\n usernameInvalid: 'usernameInvalid',\n /** Not a 10-digit Indian mobile number. */\n phoneInvalid: 'phoneInvalid',\n /** Not a `YYYY-MM-DD` date. */\n dateInvalid: 'dateInvalid',\n /** A date that does not exist, such as 30 February. */\n dobInvalid: 'dobInvalid',\n /** Under the minimum age. */\n dobUnderAge: 'dobUnderAge',\n /** Over the sanity ceiling: almost always a mistyped year. */\n dobTooOld: 'dobTooOld',\n /** A founding year after this year. */\n foundedYearFuture: 'foundedYearFuture',\n /** An upload key that is not one this product mints. */\n uploadKeyInvalid: 'uploadKeyInvalid',\n /** The same uploaded image attached twice to one post. */\n mediaDuplicate: 'mediaDuplicate',\n /** A post with neither text nor media. */\n postEmpty: 'postEmpty',\n /** Two poll options with the same label. */\n pollOptionsDuplicate: 'pollOptionsDuplicate',\n /** An empty string sent to clear a patch field; clearing is `null`. */\n clearWithNull: 'clearWithNull',\n /** Not a plain http(s) URL on a real domain (`javascript:`, `localhost`, an IP...). */\n urlInvalid: 'urlInvalid',\n /** A handle this product keeps for itself (`admin`, `support`...). */\n usernameReserved: 'usernameReserved',\n /** Agreed to a version of the guidelines that is not the current one. */\n guidelinesOutdated: 'guidelinesOutdated',\n /** An MPIN that is not exactly six digits. */\n mpinInvalid: 'mpinInvalid',\n /** The MPIN and its confirmation differ. */\n mpinMismatch: 'mpinMismatch',\n /** Switching to a staff role without the MPIN. */\n mpinRequired: 'mpinRequired',\n /** The same role twice in one list. */\n rolesDuplicate: 'rolesDuplicate',\n /** An ask (\"testers wanted\") on a post or poll that names no product. */\n askWithoutProduct: 'askWithoutProduct',\n /** An App Store link that is not on apps.apple.com. */\n appStoreLinkHost: 'appStoreLinkHost',\n /** A Play Store link that is not on play.google.com. */\n playStoreLinkHost: 'playStoreLinkHost',\n /** A GitHub link that is not on github.com. */\n githubLinkHost: 'githubLinkHost',\n /** A demo video that is not a YouTube link. Video is shared, never uploaded. */\n videoLinkHost: 'videoLinkHost',\n /** The same screenshot twice in one gallery. */\n screenshotsDuplicate: 'screenshotsDuplicate',\n /** The same platform twice in one list. */\n platformsDuplicate: 'platformsDuplicate',\n} as const;\nexport type ValidationMessageKey = (typeof VALIDATION_MESSAGE)[keyof typeof VALIDATION_MESSAGE];\n\n/**\n * Which part of the request an issue is about.\n *\n * ONE PATH CONVENTION: `path` is ALWAYS relative to this part, so a body field\n * reads `['venture', 'name']` with `location: 'body'`, never\n * `['body', 'venture', 'name']`. A form maps body issues straight onto its\n * fields without stripping a prefix, and a client that sees `query` or\n * `params` knows the problem is the URL, not anything the member typed.\n */\nexport const REQUEST_PART_VALUES = ['body', 'query', 'params'] as const;\nexport const requestPartSchema = z.enum(REQUEST_PART_VALUES);\nexport type RequestPart = z.infer<typeof requestPartSchema>;\n\n/**\n * One problem with one field, as it crosses the wire.\n *\n * `code` is Zod's issue code (`too_small`, `too_big`, `invalid_type`,\n * `invalid_format`, `custom`...). `message` is either one of\n * `VALIDATION_MESSAGE` or, for a plain bound, Zod's own English text, which the\n * client ignores in favour of `code` plus `minimum` / `maximum`.\n */\nexport const validationIssueSchema = z.object({\n /** Set by the server on every issue it sends; absent on a client's own parse. */\n location: requestPartSchema.optional(),\n /**\n * Where the problem is, RELATIVE TO `location`: `['venture', 'name']`,\n * `['options', 2, 'label']`, `['contentId']`.\n */\n path: z.array(z.union([z.string(), z.number()])),\n code: z.string(),\n message: z.string(),\n /** `too_small`: the bound that was missed. */\n minimum: z.number().optional(),\n /** `too_big`: the bound that was passed. */\n maximum: z.number().optional(),\n /** What was measured: `string`, `array`, `number`... */\n origin: z.string().optional(),\n /** `invalid_format`: which format, e.g. `url`, `regex`, `email`. */\n format: z.string().optional(),\n});\nexport type ValidationIssue = z.infer<typeof validationIssueSchema>;\n\n/** The loosest shape this needs from a Zod issue, so it works across 4.x. */\ninterface ZodIssueLike {\n path: readonly PropertyKey[];\n code: string;\n message: string;\n minimum?: unknown;\n maximum?: unknown;\n origin?: unknown;\n format?: unknown;\n}\n\nconst toNumber = (v: unknown): number | undefined =>\n typeof v === 'number' ? v : typeof v === 'bigint' ? Number(v) : undefined;\n\n/**\n * A Zod error as the wire shape. Use this on the server for every 400, and on\n * the client to treat its own errors and the server's the same way.\n *\n * `location` is stamped on every issue when given; the server always gives it.\n *\n * Copies a WHITELIST of fields (see the module note). A path segment that is a\n * symbol, which JSON cannot carry, becomes its description.\n */\nexport function toValidationIssues(\n error: { issues: readonly ZodIssueLike[] },\n location?: RequestPart,\n): ValidationIssue[] {\n return error.issues.map((issue) => {\n const out: ValidationIssue = {\n ...(location ? { location } : {}),\n path: issue.path.map((p) =>\n typeof p === 'number' ? p : typeof p === 'string' ? p : String(p.description ?? ''),\n ),\n code: issue.code,\n message: issue.message,\n };\n const minimum = toNumber(issue.minimum);\n const maximum = toNumber(issue.maximum);\n if (minimum !== undefined) out.minimum = minimum;\n if (maximum !== undefined) out.maximum = maximum;\n if (typeof issue.origin === 'string') out.origin = issue.origin;\n if (typeof issue.format === 'string') out.format = issue.format;\n return out;\n });\n}\n"]}
|
|
@@ -20,6 +20,8 @@ import { z } from 'zod';
|
|
|
20
20
|
* - their reactions, bookmarks and their votes on polls still open are
|
|
21
21
|
* withdrawn (a closed poll's result is final and is not rewritten);
|
|
22
22
|
* - their messages are erased from chat and they leave every conversation;
|
|
23
|
+
* - every image they uploaded (their photo, their posts' images) is deleted
|
|
24
|
+
* from storage, so a copied image link stops working too;
|
|
23
25
|
* - the onboarding draft is discarded.
|
|
24
26
|
*
|
|
25
27
|
* Signing in to Founder Circle again later starts a NEW, empty account.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../../src/domains/account/account.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../../src/domains/account/account.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,2BAA2B;;iBAEtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC"}
|
|
@@ -20,6 +20,8 @@ import { z } from 'zod';
|
|
|
20
20
|
* - their reactions, bookmarks and their votes on polls still open are
|
|
21
21
|
* withdrawn (a closed poll's result is final and is not rewritten);
|
|
22
22
|
* - their messages are erased from chat and they leave every conversation;
|
|
23
|
+
* - every image they uploaded (their photo, their posts' images) is deleted
|
|
24
|
+
* from storage, so a copied image link stops working too;
|
|
23
25
|
* - the onboarding draft is discarded.
|
|
24
26
|
*
|
|
25
27
|
* Signing in to Founder Circle again later starts a NEW, empty account.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.js","sourceRoot":"","sources":["../../../src/domains/account/account.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../../../src/domains/account/account.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;CACzB,CAAC,CAAC","sourcesContent":["/**\n * The member's Founder Circle account\n * ===================================\n * Deleting it is PER PRODUCT. It removes the member from Founder Circle and\n * leaves their central-auth account, and every other product they use,\n * exactly as it was. Central auth deleting the whole account arrives here as\n * its `user.deleted` webhook and has the same outcome.\n *\n * @module founder-schema/domains/account\n */\n\nimport { z } from 'zod';\n\n/**\n * `DELETE /api/v1/users/me`. No body: the confirmation (typing the account's\n * email) is the client's, and the server's check is the signed-in session.\n *\n * What it does, all of it before it answers:\n * - the founder profile stops being listed or reachable;\n * - the member's posts, polls and comments are deleted, and the comment\n * counts on other people's posts go down with them;\n * - their reactions, bookmarks and their votes on polls still open are\n * withdrawn (a closed poll's result is final and is not rewritten);\n * - their messages are erased from chat and they leave every conversation;\n * - every image they uploaded (their photo, their posts' images) is deleted\n * from storage, so a copied image link stops working too;\n * - the onboarding draft is discarded.\n *\n * Signing in to Founder Circle again later starts a NEW, empty account.\n *\n * Refusals: `SERVICE_UNAVAILABLE` (503) when the chat server could not erase\n * the messages. NOTHING was deleted then, and retrying is safe.\n * `RATE_LIMITED` (429). `UNAUTHORIZED` / `TOKEN_EXPIRED` /\n * `SESSION_INVALIDATED` (401) and `FORBIDDEN` (403, an account that is not\n * active) as for every signed-in call.\n *\n * The client signs out after a success: the account is gone, the cookies on\n * the device are not.\n */\nexport const deleteAccountResponseSchema = z.object({\n deleted: z.literal(true),\n});\nexport type DeleteAccountResponse = z.infer<typeof deleteAccountResponseSchema>;\n"]}
|
|
@@ -102,6 +102,8 @@ export declare const auditEntrySchema: z.ZodObject<{
|
|
|
102
102
|
PlatformRole: "PlatformRole";
|
|
103
103
|
AuditEntry: "AuditEntry";
|
|
104
104
|
PlatformSettings: "PlatformSettings";
|
|
105
|
+
Product: "Product";
|
|
106
|
+
Review: "Review";
|
|
105
107
|
}>;
|
|
106
108
|
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
107
109
|
label: z.ZodNullable<z.ZodString>;
|
|
@@ -156,6 +158,8 @@ export declare const auditPageSchema: z.ZodObject<{
|
|
|
156
158
|
PlatformRole: "PlatformRole";
|
|
157
159
|
AuditEntry: "AuditEntry";
|
|
158
160
|
PlatformSettings: "PlatformSettings";
|
|
161
|
+
Product: "Product";
|
|
162
|
+
Review: "Review";
|
|
159
163
|
}>;
|
|
160
164
|
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
161
165
|
label: z.ZodNullable<z.ZodString>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/domains/audit/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/domains/audit/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,mBAAmB,uNA0BtB,CAAC;AACX,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAA8B,CAAC;AAC7D,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;2BAc3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAS3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAoC,CAAC;AACjE,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
|
@@ -33,11 +33,14 @@ export const AUDIT_ACTION_VALUES = [
|
|
|
33
33
|
'role.revoked',
|
|
34
34
|
/** Someone's forgotten MPIN was cleared, so they can set a new one. */
|
|
35
35
|
'mpin.cleared',
|
|
36
|
-
/** Staff removed someone else's post, poll or
|
|
36
|
+
/** Staff removed someone else's post, poll, comment or review (`target.subject` says which). */
|
|
37
37
|
'content.removed',
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Staff hid a post, poll, comment, product or review: out of every feed and
|
|
40
|
+
* list, restorable. `detail.note`.
|
|
41
|
+
*/
|
|
39
42
|
'content.hidden',
|
|
40
|
-
/** Staff put hidden content back. */
|
|
43
|
+
/** Staff put hidden content (a product or review too) back. */
|
|
41
44
|
'content.restored',
|
|
42
45
|
/** Staff looked at what was reported and left it up. `detail.reports`: how many closed. */
|
|
43
46
|
'report.dismissed',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/domains/audit/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,kDAAkD;IAClD,cAAc;IACd,qDAAqD;IACrD,cAAc;IACd,uEAAuE;IACvE,cAAc;IACd,
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/domains/audit/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,kDAAkD;IAClD,cAAc;IACd,qDAAqD;IACrD,cAAc;IACd,uEAAuE;IACvE,cAAc;IACd,gGAAgG;IAChG,iBAAiB;IACjB;;;OAGG;IACH,gBAAgB;IAChB,+DAA+D;IAC/D,kBAAkB;IAClB,2FAA2F;IAC3F,kBAAkB;IAClB,gEAAgE;IAChE,kBAAkB;IAClB,+BAA+B;IAC/B,oBAAoB;IACpB,qCAAqC;IACrC,gBAAgB;IAChB,mDAAmD;IACnD,iBAAiB;CACT,CAAC;AACX,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;AAG7D;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;IAC3D,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;QACxB,EAAE,EAAE,cAAc;QAClB,qFAAqF;QACrF,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,oCAAoC;QACpC,QAAQ,EAAE,UAAU;KACrB,CAAC;IACF,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;QACzB,6CAA6C;QAC7C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;KACjB,CAAC;CACH,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,cAAc;IAClB,KAAK,EAAE,gBAAgB;IACvB,MAAM,EAAE,iBAAiB;IACzB,6EAA6E;IAC7E,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC;IAC9F,mFAAmF;IACnF,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACtF,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CACrB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAC,gBAAgB,CAAC,CAAC","sourcesContent":["/**\n * founder-schema: the audit log\n *\n * GET /api/v1/audit staff actions, newest first\n *\n * Every action a staff member takes on a MEMBER or on what a member wrote\n * (roles, MPIN resets, suspensions, moderation decisions, taking and closing\n * tickets) is written here, at the moment it happens, by the server: who did\n * it, AS WHICH ROLE, what they did, to what, and anything that explains it.\n * The support team's own library (saved replies) is not a member's, so its\n * edits are not entries. Entries are never edited or\n * deleted. It is how a dispute (\"who removed my post, and why\") gets an\n * answer, and how a misused staff account is noticed.\n *\n * WHO READS WHAT is the reader's permissions, applied to the query: a\n * superadmin reads every entry; any other staff role reads the entries it\n * wrote itself (its History).\n *\n * The action list grows with the staff features; it never shrinks, because an\n * old entry must still parse.\n *\n * @module founder-schema/domains/audit\n */\n\nimport { z } from 'zod';\nimport { subjectSchema } from '../../access/permissions.js';\nimport { roleSchema } from '../../access/roles.js';\nimport { objectIdSchema } from '../../core/common.js';\nimport { paginatedSchema } from '../../core/responses.js';\n\nexport const AUDIT_ACTION_VALUES = [\n /** A platform role was granted. `detail.role`. */\n 'role.granted',\n /** A platform role was taken away. `detail.role`. */\n 'role.revoked',\n /** Someone's forgotten MPIN was cleared, so they can set a new one. */\n 'mpin.cleared',\n /** Staff removed someone else's post, poll, comment or review (`target.subject` says which). */\n 'content.removed',\n /**\n * Staff hid a post, poll, comment, product or review: out of every feed and\n * list, restorable. `detail.note`.\n */\n 'content.hidden',\n /** Staff put hidden content (a product or review too) back. */\n 'content.restored',\n /** Staff looked at what was reported and left it up. `detail.reports`: how many closed. */\n 'report.dismissed',\n /** A member was suspended from the product. `detail.reason`. */\n 'member.suspended',\n /** A suspension was lifted. */\n 'member.unsuspended',\n /** A support agent took a ticket. */\n 'ticket.claimed',\n /** A support agent closed a ticket as resolved. */\n 'ticket.resolved',\n] as const;\nexport const auditActionSchema = z.enum(AUDIT_ACTION_VALUES);\nexport type AuditAction = z.infer<typeof auditActionSchema>;\n\n/**\n * Who did it. A person, acting as one of their roles; or a server script, which\n * is the only way a superadmin is ever made.\n */\nexport const auditActorSchema = z.discriminatedUnion('kind', [\n z.object({\n kind: z.literal('staff'),\n id: objectIdSchema,\n /** Their name when they did it. An entry is a record, so it is not renamed later. */\n displayName: z.string(),\n /** The role they were acting as. */\n actingAs: roleSchema,\n }),\n z.object({\n kind: z.literal('script'),\n /** Which script, e.g. `grant-superadmin`. */\n name: z.string(),\n }),\n]);\nexport type AuditActor = z.infer<typeof auditActorSchema>;\n\nexport const auditEntrySchema = z.object({\n id: objectIdSchema,\n actor: auditActorSchema,\n action: auditActionSchema,\n /** What it was done to. `label` is its name at the time, when it has one. */\n target: z.object({ subject: subjectSchema, id: objectIdSchema, label: z.string().nullable() }),\n /** The specifics, by action (the role granted, a reason given). Never a secret. */\n detail: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])),\n at: z.iso.datetime(),\n});\nexport type AuditEntry = z.infer<typeof auditEntrySchema>;\n\nexport const auditPageSchema = paginatedSchema(auditEntrySchema);\nexport type AuditPage = z.infer<typeof auditPageSchema>;\n"]}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Contribution: the help a member gives, recognised
|
|
3
|
+
*
|
|
4
|
+
* The score counts HELP GIVEN, and only what OTHER members recognise:
|
|
5
|
+
*
|
|
6
|
+
* feedback_helpful a product's team marks your comment "this helped" +10
|
|
7
|
+
* tester_confirmed a product's team confirms you tested their product +15
|
|
8
|
+
* review_helpful another member finds your review helpful +2
|
|
9
|
+
* (at most `REVIEW_HELPFUL_POINTS_MAX` per review)
|
|
10
|
+
* review_written you review a product (at most `REVIEWS_REWARDED_PER_DAY`
|
|
11
|
+
* a day earn points) +3
|
|
12
|
+
*
|
|
13
|
+
* Nothing a member does for their OWN product counts, and nothing their own
|
|
14
|
+
* product receives, so the score cannot be bought with a popular product.
|
|
15
|
+
*
|
|
16
|
+
* Against farming: one member can earn at most `CONTRIBUTION_PAIR_POINTS_MAX`
|
|
17
|
+
* points from any ONE other member over 30 days, so two founders cannot trade
|
|
18
|
+
* Helpful marks into a high score.
|
|
19
|
+
*
|
|
20
|
+
* EVERY POINT IS A ROW (the ledger), so a score can be explained ("how did I
|
|
21
|
+
* earn this?"), checked and re-tuned. A point stands while what earned it
|
|
22
|
+
* stands: unmarking, withdrawing a vote, deleting the review or comment, or
|
|
23
|
+
* staff removing it takes the point back. An hourly job recounts every score
|
|
24
|
+
* from the ledger, so a total can never drift for long.
|
|
25
|
+
*
|
|
26
|
+
* Routes (all signed in):
|
|
27
|
+
* GET /api/v1/contributions/me paginationQuerySchema -> contributionPageSchema
|
|
28
|
+
* GET /api/v1/contributions/leaderboard -> contributionLeaderboardSchema
|
|
29
|
+
*
|
|
30
|
+
* The score itself is on the profile and the card (`contributionSummarySchema`).
|
|
31
|
+
*
|
|
32
|
+
* @module founder-schema/domains/contributions
|
|
33
|
+
*/
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
/** ⚠ Stored per ledger row: append only. */
|
|
36
|
+
export declare const CONTRIBUTION_KIND_VALUES: readonly ["feedback_helpful", "tester_confirmed", "review_helpful", "review_written"];
|
|
37
|
+
export declare const contributionKindSchema: z.ZodEnum<{
|
|
38
|
+
feedback_helpful: "feedback_helpful";
|
|
39
|
+
tester_confirmed: "tester_confirmed";
|
|
40
|
+
review_helpful: "review_helpful";
|
|
41
|
+
review_written: "review_written";
|
|
42
|
+
}>;
|
|
43
|
+
export type ContributionKind = z.infer<typeof contributionKindSchema>;
|
|
44
|
+
/**
|
|
45
|
+
* Points per kind. Exhaustive by type. Changing a number changes FUTURE rows;
|
|
46
|
+
* the hourly recount sums what each row was actually awarded.
|
|
47
|
+
*/
|
|
48
|
+
export declare const CONTRIBUTION_POINTS: {
|
|
49
|
+
readonly feedback_helpful: 10;
|
|
50
|
+
readonly tester_confirmed: 15;
|
|
51
|
+
readonly review_helpful: 2;
|
|
52
|
+
readonly review_written: 3;
|
|
53
|
+
};
|
|
54
|
+
/** One line of "how I earned my points". */
|
|
55
|
+
export declare const contributionEntrySchema: z.ZodObject<{
|
|
56
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
57
|
+
kind: z.ZodEnum<{
|
|
58
|
+
feedback_helpful: "feedback_helpful";
|
|
59
|
+
tester_confirmed: "tester_confirmed";
|
|
60
|
+
review_helpful: "review_helpful";
|
|
61
|
+
review_written: "review_written";
|
|
62
|
+
}>;
|
|
63
|
+
points: z.ZodNumber;
|
|
64
|
+
actor: z.ZodNullable<z.ZodObject<{
|
|
65
|
+
userId: z.ZodString;
|
|
66
|
+
username: z.ZodString;
|
|
67
|
+
displayName: z.ZodString;
|
|
68
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
69
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strip>>;
|
|
71
|
+
product: z.ZodOptional<z.ZodObject<{
|
|
72
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
73
|
+
slug: z.ZodString;
|
|
74
|
+
name: z.ZodString;
|
|
75
|
+
logoUrl: z.ZodOptional<z.ZodURL>;
|
|
76
|
+
}, z.core.$strip>>;
|
|
77
|
+
content: z.ZodOptional<z.ZodObject<{
|
|
78
|
+
kind: z.ZodEnum<{
|
|
79
|
+
post: "post";
|
|
80
|
+
poll: "poll";
|
|
81
|
+
}>;
|
|
82
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
83
|
+
}, z.core.$strip>>;
|
|
84
|
+
commentId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
85
|
+
reviewId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
86
|
+
createdAt: z.ZodISODateTime;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
export type ContributionEntry = z.infer<typeof contributionEntrySchema>;
|
|
89
|
+
export declare const contributionPageSchema: z.ZodObject<{
|
|
90
|
+
items: z.ZodArray<z.ZodObject<{
|
|
91
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
92
|
+
kind: z.ZodEnum<{
|
|
93
|
+
feedback_helpful: "feedback_helpful";
|
|
94
|
+
tester_confirmed: "tester_confirmed";
|
|
95
|
+
review_helpful: "review_helpful";
|
|
96
|
+
review_written: "review_written";
|
|
97
|
+
}>;
|
|
98
|
+
points: z.ZodNumber;
|
|
99
|
+
actor: z.ZodNullable<z.ZodObject<{
|
|
100
|
+
userId: z.ZodString;
|
|
101
|
+
username: z.ZodString;
|
|
102
|
+
displayName: z.ZodString;
|
|
103
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
104
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
105
|
+
}, z.core.$strip>>;
|
|
106
|
+
product: z.ZodOptional<z.ZodObject<{
|
|
107
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
108
|
+
slug: z.ZodString;
|
|
109
|
+
name: z.ZodString;
|
|
110
|
+
logoUrl: z.ZodOptional<z.ZodURL>;
|
|
111
|
+
}, z.core.$strip>>;
|
|
112
|
+
content: z.ZodOptional<z.ZodObject<{
|
|
113
|
+
kind: z.ZodEnum<{
|
|
114
|
+
post: "post";
|
|
115
|
+
poll: "poll";
|
|
116
|
+
}>;
|
|
117
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
118
|
+
}, z.core.$strip>>;
|
|
119
|
+
commentId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
120
|
+
reviewId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
121
|
+
createdAt: z.ZodISODateTime;
|
|
122
|
+
}, z.core.$strip>>;
|
|
123
|
+
nextCursor: z.ZodNullable<z.ZodString>;
|
|
124
|
+
}, z.core.$strip>;
|
|
125
|
+
export type ContributionPage = z.infer<typeof contributionPageSchema>;
|
|
126
|
+
/** One row of the top helpers of the last `CONTRIBUTION_RECENT_DAYS` days. */
|
|
127
|
+
export declare const contributionLeaderEntrySchema: z.ZodObject<{
|
|
128
|
+
rank: z.ZodNumber;
|
|
129
|
+
member: z.ZodObject<{
|
|
130
|
+
userId: z.ZodString;
|
|
131
|
+
username: z.ZodString;
|
|
132
|
+
displayName: z.ZodString;
|
|
133
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
134
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
135
|
+
}, z.core.$strip>;
|
|
136
|
+
points: z.ZodNumber;
|
|
137
|
+
}, z.core.$strip>;
|
|
138
|
+
export type ContributionLeaderEntry = z.infer<typeof contributionLeaderEntrySchema>;
|
|
139
|
+
/**
|
|
140
|
+
* Top helpers, best first, at most `CONTRIBUTION_LEADERBOARD_SIZE`. Recomputed
|
|
141
|
+
* hourly; `computedAt` says when (null before the first run).
|
|
142
|
+
*/
|
|
143
|
+
export declare const contributionLeaderboardSchema: z.ZodObject<{
|
|
144
|
+
items: z.ZodArray<z.ZodObject<{
|
|
145
|
+
rank: z.ZodNumber;
|
|
146
|
+
member: z.ZodObject<{
|
|
147
|
+
userId: z.ZodString;
|
|
148
|
+
username: z.ZodString;
|
|
149
|
+
displayName: z.ZodString;
|
|
150
|
+
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
151
|
+
headline: z.ZodOptional<z.ZodString>;
|
|
152
|
+
}, z.core.$strip>;
|
|
153
|
+
points: z.ZodNumber;
|
|
154
|
+
}, z.core.$strip>>;
|
|
155
|
+
computedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
156
|
+
}, z.core.$strip>;
|
|
157
|
+
export type ContributionLeaderboard = z.infer<typeof contributionLeaderboardSchema>;
|
|
158
|
+
//# sourceMappingURL=contributions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../../src/domains/contributions/contributions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,4CAA4C;AAC5C,eAAO,MAAM,wBAAwB,uFAK3B,CAAC;AACX,eAAO,MAAM,sBAAsB;;;;;EAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;CAKqB,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiBlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA2C,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B;;;;;;;;;;iBAKxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;iBAGxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Contribution: the help a member gives, recognised
|
|
3
|
+
*
|
|
4
|
+
* The score counts HELP GIVEN, and only what OTHER members recognise:
|
|
5
|
+
*
|
|
6
|
+
* feedback_helpful a product's team marks your comment "this helped" +10
|
|
7
|
+
* tester_confirmed a product's team confirms you tested their product +15
|
|
8
|
+
* review_helpful another member finds your review helpful +2
|
|
9
|
+
* (at most `REVIEW_HELPFUL_POINTS_MAX` per review)
|
|
10
|
+
* review_written you review a product (at most `REVIEWS_REWARDED_PER_DAY`
|
|
11
|
+
* a day earn points) +3
|
|
12
|
+
*
|
|
13
|
+
* Nothing a member does for their OWN product counts, and nothing their own
|
|
14
|
+
* product receives, so the score cannot be bought with a popular product.
|
|
15
|
+
*
|
|
16
|
+
* Against farming: one member can earn at most `CONTRIBUTION_PAIR_POINTS_MAX`
|
|
17
|
+
* points from any ONE other member over 30 days, so two founders cannot trade
|
|
18
|
+
* Helpful marks into a high score.
|
|
19
|
+
*
|
|
20
|
+
* EVERY POINT IS A ROW (the ledger), so a score can be explained ("how did I
|
|
21
|
+
* earn this?"), checked and re-tuned. A point stands while what earned it
|
|
22
|
+
* stands: unmarking, withdrawing a vote, deleting the review or comment, or
|
|
23
|
+
* staff removing it takes the point back. An hourly job recounts every score
|
|
24
|
+
* from the ledger, so a total can never drift for long.
|
|
25
|
+
*
|
|
26
|
+
* Routes (all signed in):
|
|
27
|
+
* GET /api/v1/contributions/me paginationQuerySchema -> contributionPageSchema
|
|
28
|
+
* GET /api/v1/contributions/leaderboard -> contributionLeaderboardSchema
|
|
29
|
+
*
|
|
30
|
+
* The score itself is on the profile and the card (`contributionSummarySchema`).
|
|
31
|
+
*
|
|
32
|
+
* @module founder-schema/domains/contributions
|
|
33
|
+
*/
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
import { objectIdSchema } from '../../core/common.js';
|
|
36
|
+
import { paginatedSchema } from '../../core/responses.js';
|
|
37
|
+
import { contentKindSchema } from '../feed/feed.js';
|
|
38
|
+
import { productChipSchema } from '../products/products.js';
|
|
39
|
+
import { memberBylineSchema } from '../profile/founder-profile.js';
|
|
40
|
+
/** ⚠ Stored per ledger row: append only. */
|
|
41
|
+
export const CONTRIBUTION_KIND_VALUES = [
|
|
42
|
+
'feedback_helpful',
|
|
43
|
+
'tester_confirmed',
|
|
44
|
+
'review_helpful',
|
|
45
|
+
'review_written',
|
|
46
|
+
];
|
|
47
|
+
export const contributionKindSchema = z.enum(CONTRIBUTION_KIND_VALUES);
|
|
48
|
+
/**
|
|
49
|
+
* Points per kind. Exhaustive by type. Changing a number changes FUTURE rows;
|
|
50
|
+
* the hourly recount sums what each row was actually awarded.
|
|
51
|
+
*/
|
|
52
|
+
export const CONTRIBUTION_POINTS = {
|
|
53
|
+
feedback_helpful: 10,
|
|
54
|
+
tester_confirmed: 15,
|
|
55
|
+
review_helpful: 2,
|
|
56
|
+
review_written: 3,
|
|
57
|
+
};
|
|
58
|
+
/** One line of "how I earned my points". */
|
|
59
|
+
export const contributionEntrySchema = z.object({
|
|
60
|
+
id: objectIdSchema,
|
|
61
|
+
kind: contributionKindSchema,
|
|
62
|
+
/** What it was actually awarded: below the kind's points when a cap applied. */
|
|
63
|
+
points: z.number().int().positive(),
|
|
64
|
+
/** Who recognised it; null for a deleted account, and for `review_written`. */
|
|
65
|
+
actor: memberBylineSchema.nullable(),
|
|
66
|
+
/** The product it was about, while it exists. */
|
|
67
|
+
product: productChipSchema.optional(),
|
|
68
|
+
/**
|
|
69
|
+
* What earned it, so the row opens it: the post or poll and the comment for
|
|
70
|
+
* `feedback_helpful`, the review for `review_helpful` and `review_written`.
|
|
71
|
+
*/
|
|
72
|
+
content: z.object({ kind: contentKindSchema, id: objectIdSchema }).optional(),
|
|
73
|
+
commentId: objectIdSchema.optional(),
|
|
74
|
+
reviewId: objectIdSchema.optional(),
|
|
75
|
+
createdAt: z.iso.datetime(),
|
|
76
|
+
});
|
|
77
|
+
export const contributionPageSchema = paginatedSchema(contributionEntrySchema);
|
|
78
|
+
/** One row of the top helpers of the last `CONTRIBUTION_RECENT_DAYS` days. */
|
|
79
|
+
export const contributionLeaderEntrySchema = z.object({
|
|
80
|
+
rank: z.number().int().positive(),
|
|
81
|
+
member: memberBylineSchema,
|
|
82
|
+
/** Points earned in the window. */
|
|
83
|
+
points: z.number().int().positive(),
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Top helpers, best first, at most `CONTRIBUTION_LEADERBOARD_SIZE`. Recomputed
|
|
87
|
+
* hourly; `computedAt` says when (null before the first run).
|
|
88
|
+
*/
|
|
89
|
+
export const contributionLeaderboardSchema = z.object({
|
|
90
|
+
items: z.array(contributionLeaderEntrySchema),
|
|
91
|
+
computedAt: z.iso.datetime().nullable(),
|
|
92
|
+
});
|
|
93
|
+
//# sourceMappingURL=contributions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contributions.js","sourceRoot":"","sources":["../../../src/domains/contributions/contributions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAEnE,4CAA4C;AAC5C,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,kBAAkB;IAClB,kBAAkB;IAClB,gBAAgB;IAChB,gBAAgB;CACR,CAAC;AACX,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;AAGvE;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,gBAAgB,EAAE,EAAE;IACpB,gBAAgB,EAAE,EAAE;IACpB,cAAc,EAAE,CAAC;IACjB,cAAc,EAAE,CAAC;CACkC,CAAC;AAEtD,4CAA4C;AAC5C,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,sBAAsB;IAC5B,gFAAgF;IAChF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACnC,+EAA+E;IAC/E,KAAK,EAAE,kBAAkB,CAAC,QAAQ,EAAE;IACpC,iDAAiD;IACjD,OAAO,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACrC;;;OAGG;IACH,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC7E,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;IACpC,QAAQ,EAAE,cAAc,CAAC,QAAQ,EAAE;IACnC,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,sBAAsB,GAAG,eAAe,CAAC,uBAAuB,CAAC,CAAC;AAG/E,8EAA8E;AAC9E,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACjC,MAAM,EAAE,kBAAkB;IAC1B,mCAAmC;IACnC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CACpC,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,6BAA6B,CAAC;IAC7C,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema — Contribution: the help a member gives, recognised\n *\n * The score counts HELP GIVEN, and only what OTHER members recognise:\n *\n * feedback_helpful a product's team marks your comment \"this helped\" +10\n * tester_confirmed a product's team confirms you tested their product +15\n * review_helpful another member finds your review helpful +2\n * (at most `REVIEW_HELPFUL_POINTS_MAX` per review)\n * review_written you review a product (at most `REVIEWS_REWARDED_PER_DAY`\n * a day earn points) +3\n *\n * Nothing a member does for their OWN product counts, and nothing their own\n * product receives, so the score cannot be bought with a popular product.\n *\n * Against farming: one member can earn at most `CONTRIBUTION_PAIR_POINTS_MAX`\n * points from any ONE other member over 30 days, so two founders cannot trade\n * Helpful marks into a high score.\n *\n * EVERY POINT IS A ROW (the ledger), so a score can be explained (\"how did I\n * earn this?\"), checked and re-tuned. A point stands while what earned it\n * stands: unmarking, withdrawing a vote, deleting the review or comment, or\n * staff removing it takes the point back. An hourly job recounts every score\n * from the ledger, so a total can never drift for long.\n *\n * Routes (all signed in):\n * GET /api/v1/contributions/me paginationQuerySchema -> contributionPageSchema\n * GET /api/v1/contributions/leaderboard -> contributionLeaderboardSchema\n *\n * The score itself is on the profile and the card (`contributionSummarySchema`).\n *\n * @module founder-schema/domains/contributions\n */\n\nimport { z } from 'zod';\nimport { objectIdSchema } from '../../core/common.js';\nimport { paginatedSchema } from '../../core/responses.js';\nimport { contentKindSchema } from '../feed/feed.js';\nimport { productChipSchema } from '../products/products.js';\nimport { memberBylineSchema } from '../profile/founder-profile.js';\n\n/** ⚠ Stored per ledger row: append only. */\nexport const CONTRIBUTION_KIND_VALUES = [\n 'feedback_helpful',\n 'tester_confirmed',\n 'review_helpful',\n 'review_written',\n] as const;\nexport const contributionKindSchema = z.enum(CONTRIBUTION_KIND_VALUES);\nexport type ContributionKind = z.infer<typeof contributionKindSchema>;\n\n/**\n * Points per kind. Exhaustive by type. Changing a number changes FUTURE rows;\n * the hourly recount sums what each row was actually awarded.\n */\nexport const CONTRIBUTION_POINTS = {\n feedback_helpful: 10,\n tester_confirmed: 15,\n review_helpful: 2,\n review_written: 3,\n} as const satisfies Record<ContributionKind, number>;\n\n/** One line of \"how I earned my points\". */\nexport const contributionEntrySchema = z.object({\n id: objectIdSchema,\n kind: contributionKindSchema,\n /** What it was actually awarded: below the kind's points when a cap applied. */\n points: z.number().int().positive(),\n /** Who recognised it; null for a deleted account, and for `review_written`. */\n actor: memberBylineSchema.nullable(),\n /** The product it was about, while it exists. */\n product: productChipSchema.optional(),\n /**\n * What earned it, so the row opens it: the post or poll and the comment for\n * `feedback_helpful`, the review for `review_helpful` and `review_written`.\n */\n content: z.object({ kind: contentKindSchema, id: objectIdSchema }).optional(),\n commentId: objectIdSchema.optional(),\n reviewId: objectIdSchema.optional(),\n createdAt: z.iso.datetime(),\n});\nexport type ContributionEntry = z.infer<typeof contributionEntrySchema>;\n\nexport const contributionPageSchema = paginatedSchema(contributionEntrySchema);\nexport type ContributionPage = z.infer<typeof contributionPageSchema>;\n\n/** One row of the top helpers of the last `CONTRIBUTION_RECENT_DAYS` days. */\nexport const contributionLeaderEntrySchema = z.object({\n rank: z.number().int().positive(),\n member: memberBylineSchema,\n /** Points earned in the window. */\n points: z.number().int().positive(),\n});\nexport type ContributionLeaderEntry = z.infer<typeof contributionLeaderEntrySchema>;\n\n/**\n * Top helpers, best first, at most `CONTRIBUTION_LEADERBOARD_SIZE`. Recomputed\n * hourly; `computedAt` says when (null before the first run).\n */\nexport const contributionLeaderboardSchema = z.object({\n items: z.array(contributionLeaderEntrySchema),\n computedAt: z.iso.datetime().nullable(),\n});\nexport type ContributionLeaderboard = z.infer<typeof contributionLeaderboardSchema>;\n"]}
|
|
@@ -87,9 +87,14 @@ export declare const discoverPageSchema: z.ZodObject<{
|
|
|
87
87
|
country: z.ZodString;
|
|
88
88
|
}, z.core.$strip>>;
|
|
89
89
|
displayName: z.ZodString;
|
|
90
|
+
userId: z.ZodString;
|
|
90
91
|
username: z.ZodString;
|
|
91
92
|
avatarUrl: z.ZodOptional<z.ZodURL>;
|
|
92
93
|
headline: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
94
|
+
contribution: z.ZodObject<{
|
|
95
|
+
score: z.ZodNumber;
|
|
96
|
+
helpedCount: z.ZodNumber;
|
|
97
|
+
}, z.core.$strip>;
|
|
93
98
|
venture: z.ZodOptional<z.ZodObject<{
|
|
94
99
|
name: z.ZodString;
|
|
95
100
|
tagline: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../src/domains/discover/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../src/domains/discover/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBxB;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAErE,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAqC,CAAC;AACrE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAM9D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
|
@@ -8,22 +8,12 @@
|
|
|
8
8
|
* @module founder-schema/domains/discover
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
-
import { industrySchema, optionalTrimmedString, paginationQuerySchema, skillSchema, startupStageSchema, } from '../../core/common.js';
|
|
11
|
+
import { industrySchema, multiValueQuery, optionalTrimmedString, paginationQuerySchema, skillSchema, startupStageSchema, } from '../../core/common.js';
|
|
12
12
|
import { DISCOVER_FILTER_VALUES_MAX, DISCOVER_TEXT_MAX } from '../../core/limits.js';
|
|
13
13
|
import { paginatedSchema } from '../../core/responses.js';
|
|
14
14
|
import { founderCardSchema } from '../profile/founder-profile.js';
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
* value, and always produces an array.
|
|
18
|
-
*
|
|
19
|
-
* Express gives `string` for one occurrence and `string[]` for several, so
|
|
20
|
-
* without this the same URL shape parses two different ways.
|
|
21
|
-
*/
|
|
22
|
-
const multiValue = (inner) => z.preprocess((value) => {
|
|
23
|
-
if (value === undefined || value === null)
|
|
24
|
-
return undefined;
|
|
25
|
-
return Array.isArray(value) ? value : [value];
|
|
26
|
-
}, z.array(inner).max(DISCOVER_FILTER_VALUES_MAX).optional());
|
|
15
|
+
/** A Discover multi-select (`?skill=a&skill=b`): see `multiValueQuery`. */
|
|
16
|
+
const multiValue = (inner) => multiValueQuery(inner, DISCOVER_FILTER_VALUES_MAX);
|
|
27
17
|
/**
|
|
28
18
|
* Every free-text filter here is `optionalTrimmedString`, so an EMPTY one is
|
|
29
19
|
* treated as "not provided" rather than as a validation failure.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../../../src/domains/discover/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../../../src/domains/discover/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAElE,2EAA2E;AAC3E,MAAM,UAAU,GAAG,CAAsB,KAAQ,EAAE,EAAE,CACnD,eAAe,CAAC,KAAK,EAAE,0BAA0B,CAAC,CAAC;AAErD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,MAAM,CAAC;IAC9D,wEAAwE;IACxE,CAAC,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;IAC3C,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC;IACpC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC;IAC9B,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC;IACrC,IAAI,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;IAC9C,OAAO,EAAE,qBAAqB,CAAC,iBAAiB,CAAC;CAClD,CAAC,CAAC;AAIH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAAC;AAGrE,+DAA+D;AAC/D,MAAM,WAAW,GAAG,CAAsB,KAAQ,EAAE,EAAE,CACpD,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAChD,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema — Discover search and filters\n *\n * Search is by name; the filters are industry, skill, startup stage and\n * location. The community surface the Discover tab will grow later is not\n * modelled yet.\n *\n * @module founder-schema/domains/discover\n */\n\nimport { z } from 'zod';\nimport {\n industrySchema,\n multiValueQuery,\n optionalTrimmedString,\n paginationQuerySchema,\n skillSchema,\n startupStageSchema,\n} from '../../core/common.js';\nimport { DISCOVER_FILTER_VALUES_MAX, DISCOVER_TEXT_MAX } from '../../core/limits.js';\nimport { paginatedSchema } from '../../core/responses.js';\nimport { founderCardSchema } from '../profile/founder-profile.js';\n\n/** A Discover multi-select (`?skill=a&skill=b`): see `multiValueQuery`. */\nconst multiValue = <T extends z.ZodType>(inner: T) =>\n multiValueQuery(inner, DISCOVER_FILTER_VALUES_MAX);\n\n/**\n * Every free-text filter here is `optionalTrimmedString`, so an EMPTY one is\n * treated as \"not provided\" rather than as a validation failure.\n *\n * A search box the member has just cleared sends `?q=`, and a select they reset\n * sends `?city=`. Refusing those turns an ordinary interaction into a 400 with\n * nothing useful to show for it: the member asked for fewer filters, not for an\n * error. This is the one place the create-body rule and the patch-body rule\n * agree, because a query has nothing to erase.\n */\nexport const discoverQuerySchema = paginationQuerySchema.extend({\n /** Free-text name search. Matched against display name and username. */\n q: optionalTrimmedString(DISCOVER_TEXT_MAX),\n industry: multiValue(industrySchema),\n skill: multiValue(skillSchema),\n stage: multiValue(startupStageSchema),\n city: optionalTrimmedString(DISCOVER_TEXT_MAX),\n country: optionalTrimmedString(DISCOVER_TEXT_MAX),\n});\nexport type DiscoverQuery = z.infer<typeof discoverQuerySchema>;\nexport type DiscoverQueryInput = z.input<typeof discoverQuerySchema>;\n\n/** A page of founder cards, most recently updated first. */\nexport const discoverPageSchema = paginatedSchema(founderCardSchema);\nexport type DiscoverPage = z.infer<typeof discoverPageSchema>;\n\n/** One filter option and how many listed founders match it. */\nconst facetOption = <T extends z.ZodType>(value: T) =>\n z.object({ value, count: z.number().int().nonnegative() });\n\n/**\n * The filter options the Discover drawer offers, with counts. The VALUES come\n * from the enums here, so the options cannot disagree with what the query\n * accepts.\n *\n * ⚠ NO LABELS. The value IS the translation key (`industries.<value>`,\n * `stages.<value>`, `skills.<value>`), and a client words it in the member's\n * language. Until 0.2.0 the server also sent an English `label` per option,\n * built by capitalising the value (\"Ai ml\"), which no client read and which\n * would have been wrong in Hindi if one had. Cities are member-typed place\n * names, so their value is shown as it is.\n */\nexport const discoverFacetsSchema = z.object({\n industries: z.array(facetOption(industrySchema)),\n skills: z.array(facetOption(skillSchema)),\n stages: z.array(facetOption(startupStageSchema)),\n cities: z.array(facetOption(z.string())),\n});\nexport type DiscoverFacets = z.infer<typeof discoverFacetsSchema>;\n"]}
|