jansathi-community-schema 0.1.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.
Files changed (58) hide show
  1. package/README.md +41 -0
  2. package/dist/area.d.ts +37 -0
  3. package/dist/area.d.ts.map +1 -0
  4. package/dist/area.js +51 -0
  5. package/dist/area.js.map +1 -0
  6. package/dist/comment.d.ts +54 -0
  7. package/dist/comment.d.ts.map +1 -0
  8. package/dist/comment.js +63 -0
  9. package/dist/comment.js.map +1 -0
  10. package/dist/constants.d.ts +42 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +47 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/engagement.d.ts +69 -0
  15. package/dist/engagement.d.ts.map +1 -0
  16. package/dist/engagement.js +50 -0
  17. package/dist/engagement.js.map +1 -0
  18. package/dist/enums.d.ts +149 -0
  19. package/dist/enums.d.ts.map +1 -0
  20. package/dist/enums.js +143 -0
  21. package/dist/enums.js.map +1 -0
  22. package/dist/feed.d.ts +311 -0
  23. package/dist/feed.d.ts.map +1 -0
  24. package/dist/feed.js +102 -0
  25. package/dist/feed.js.map +1 -0
  26. package/dist/group.d.ts +48 -0
  27. package/dist/group.d.ts.map +1 -0
  28. package/dist/group.js +45 -0
  29. package/dist/group.js.map +1 -0
  30. package/dist/index.d.ts +30 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +43 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/media.d.ts +48 -0
  35. package/dist/media.d.ts.map +1 -0
  36. package/dist/media.js +50 -0
  37. package/dist/media.js.map +1 -0
  38. package/dist/post.d.ts +130 -0
  39. package/dist/post.d.ts.map +1 -0
  40. package/dist/post.js +103 -0
  41. package/dist/post.js.map +1 -0
  42. package/dist/profile.d.ts +116 -0
  43. package/dist/profile.d.ts.map +1 -0
  44. package/dist/profile.js +70 -0
  45. package/dist/profile.js.map +1 -0
  46. package/dist/report.d.ts +62 -0
  47. package/dist/report.d.ts.map +1 -0
  48. package/dist/report.js +30 -0
  49. package/dist/report.js.map +1 -0
  50. package/dist/settings.d.ts +66 -0
  51. package/dist/settings.d.ts.map +1 -0
  52. package/dist/settings.js +75 -0
  53. package/dist/settings.js.map +1 -0
  54. package/dist/social.d.ts +95 -0
  55. package/dist/social.d.ts.map +1 -0
  56. package/dist/social.js +85 -0
  57. package/dist/social.js.map +1 -0
  58. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # jansathi-community-schema
2
+
3
+ Shared Zod schemas + TypeScript types for the Jansathi hyperlocal community feature.
4
+
5
+ Consumed by `reform-backend` and every Jansathi product frontend. Single source of truth for:
6
+
7
+ - **Feed** — polymorphic union of posts, lost-and-found, and voice-box items
8
+ - **Posts** — text + media (10 images, 5 videos ≤10 min), with `visibilityLevel` and area-lineage snapshot
9
+ - **Engagement** — upvote toggle + 7-emoji reactions + comments with one-level reply nesting
10
+ - **Social graph** — Follow (one-way) + Connection (mutual handshake)
11
+ - **Profiles** — public / private toggle, viewer-relationship state
12
+ - **Groups** — user-created group chats wire-compatible with chat-server conversations
13
+ - **Settings** — profile privacy, default post visibility, notification preferences
14
+ - **Reports** — content moderation queue payloads
15
+
16
+ ## Visibility semantics
17
+
18
+ Posts carry a `visibilityLevel ∈ { local, district, state, national, global }` and an `areaLineage` snapshot at write time. The feed query uses **cascade-upward** semantics:
19
+
20
+ > A viewer whose feed level is **L** sees posts at **L** and every broader level matching their area lineage.
21
+
22
+ So a viewer with `level=local` sees `local + district + state + national + global` content tied to their locality / village / urbanWard / etc.; a viewer with `level=state` sees `state + national + global` content tied to their state, etc.
23
+
24
+ Accepted Connections override the cascade entirely — friend posts always show regardless of level.
25
+
26
+ ## Build
27
+
28
+ ```bash
29
+ npm install
30
+ npm run build
31
+ ```
32
+
33
+ ## Publish (manual, not auto)
34
+
35
+ Bump version in `package.json`, then:
36
+
37
+ ```bash
38
+ npm publish --access public
39
+ ```
40
+
41
+ After publish, run `npm install jansathi-community-schema@<version>` in both `reform-backend` and any frontend that consumes it. Delete any nested `node_modules` inside the installed package to avoid duplicate Zod instances.
package/dist/area.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Area-lineage snapshot stored on every piece of community content.
3
+ *
4
+ * Why a snapshot (not a live lookup): the post's geographic scope is a
5
+ * write-time property — it travels with the content and survives the
6
+ * author moving away, the author's primary area being recomputed, or
7
+ * the AreaEntity being renamed. Mirrors the same denormalization
8
+ * pattern used by `udp-schema`'s residence step.
9
+ *
10
+ * Every level the cascade-upward feed query may filter on is captured
11
+ * here as a string id (Mongo ObjectId serialized). Indexes on the
12
+ * backend match: one compound per geo level paired with `createdAt`.
13
+ *
14
+ * The two "below district" admin units (locality / village / urbanBody
15
+ * / urbanWard / ruralWard / gramPanchayat / block / hamlet) are
16
+ * grouped under `localId` — the deepest admin area below the district
17
+ * the author resolved to at post time. This is what powers the "local"
18
+ * filter without forcing the backend to compose nine separate indexes.
19
+ *
20
+ * @module community-schema/area
21
+ */
22
+ import { z } from 'zod';
23
+ /**
24
+ * Snapshot of the author's primary area lineage at content creation
25
+ * time. All ids are optional because the author may not have a full
26
+ * lineage resolved yet (e.g., a brand-new user whose only known field
27
+ * is country).
28
+ */
29
+ export declare const areaLineageSnapshotSchema: z.ZodObject<{
30
+ countryId: z.ZodOptional<z.ZodString>;
31
+ stateId: z.ZodOptional<z.ZodString>;
32
+ districtId: z.ZodOptional<z.ZodString>;
33
+ localId: z.ZodOptional<z.ZodString>;
34
+ localName: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strip>;
36
+ export type AreaLineageSnapshot = z.infer<typeof areaLineageSnapshotSchema>;
37
+ //# sourceMappingURL=area.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"area.d.ts","sourceRoot":"","sources":["../src/area.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;;;;;iBAqBpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
package/dist/area.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Area-lineage snapshot stored on every piece of community content.
3
+ *
4
+ * Why a snapshot (not a live lookup): the post's geographic scope is a
5
+ * write-time property — it travels with the content and survives the
6
+ * author moving away, the author's primary area being recomputed, or
7
+ * the AreaEntity being renamed. Mirrors the same denormalization
8
+ * pattern used by `udp-schema`'s residence step.
9
+ *
10
+ * Every level the cascade-upward feed query may filter on is captured
11
+ * here as a string id (Mongo ObjectId serialized). Indexes on the
12
+ * backend match: one compound per geo level paired with `createdAt`.
13
+ *
14
+ * The two "below district" admin units (locality / village / urbanBody
15
+ * / urbanWard / ruralWard / gramPanchayat / block / hamlet) are
16
+ * grouped under `localId` — the deepest admin area below the district
17
+ * the author resolved to at post time. This is what powers the "local"
18
+ * filter without forcing the backend to compose nine separate indexes.
19
+ *
20
+ * @module community-schema/area
21
+ */
22
+ import { z } from 'zod';
23
+ /**
24
+ * Snapshot of the author's primary area lineage at content creation
25
+ * time. All ids are optional because the author may not have a full
26
+ * lineage resolved yet (e.g., a brand-new user whose only known field
27
+ * is country).
28
+ */
29
+ export const areaLineageSnapshotSchema = z.object({
30
+ countryId: z.string().optional(),
31
+ stateId: z.string().optional(),
32
+ districtId: z.string().optional(),
33
+ /**
34
+ * The deepest area below the district that the author resolved to.
35
+ * "Local" in this app explicitly means "anywhere below district" —
36
+ * which level varies by urban / rural / hamlet structure, so the
37
+ * id is collapsed into a single field for indexing efficiency.
38
+ *
39
+ * Resolution order (server-side, at write time): hamlet → village →
40
+ * locality → ruralWard → urbanWard → gramPanchayat → urbanBody → block.
41
+ * The first match becomes `localId`.
42
+ */
43
+ localId: z.string().optional(),
44
+ /**
45
+ * Human-readable label of the deepest matched area (e.g. "Rohini
46
+ * Sector 15"). Surfaced on the post card so viewers see where it
47
+ * came from without an extra Area lookup.
48
+ */
49
+ localName: z.string().optional(),
50
+ });
51
+ //# sourceMappingURL=area.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"area.js","sourceRoot":"","sources":["../src/area.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC;;;;;;;;;OASG;IACH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B;;;;OAIG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAC","sourcesContent":["/**\n * Area-lineage snapshot stored on every piece of community content.\n *\n * Why a snapshot (not a live lookup): the post's geographic scope is a\n * write-time property — it travels with the content and survives the\n * author moving away, the author's primary area being recomputed, or\n * the AreaEntity being renamed. Mirrors the same denormalization\n * pattern used by `udp-schema`'s residence step.\n *\n * Every level the cascade-upward feed query may filter on is captured\n * here as a string id (Mongo ObjectId serialized). Indexes on the\n * backend match: one compound per geo level paired with `createdAt`.\n *\n * The two \"below district\" admin units (locality / village / urbanBody\n * / urbanWard / ruralWard / gramPanchayat / block / hamlet) are\n * grouped under `localId` — the deepest admin area below the district\n * the author resolved to at post time. This is what powers the \"local\"\n * filter without forcing the backend to compose nine separate indexes.\n *\n * @module community-schema/area\n */\n\nimport { z } from 'zod';\n\n/**\n * Snapshot of the author's primary area lineage at content creation\n * time. All ids are optional because the author may not have a full\n * lineage resolved yet (e.g., a brand-new user whose only known field\n * is country).\n */\nexport const areaLineageSnapshotSchema = z.object({\n countryId: z.string().optional(),\n stateId: z.string().optional(),\n districtId: z.string().optional(),\n /**\n * The deepest area below the district that the author resolved to.\n * \"Local\" in this app explicitly means \"anywhere below district\" —\n * which level varies by urban / rural / hamlet structure, so the\n * id is collapsed into a single field for indexing efficiency.\n *\n * Resolution order (server-side, at write time): hamlet → village →\n * locality → ruralWard → urbanWard → gramPanchayat → urbanBody → block.\n * The first match becomes `localId`.\n */\n localId: z.string().optional(),\n /**\n * Human-readable label of the deepest matched area (e.g. \"Rohini\n * Sector 15\"). Surfaced on the post card so viewers see where it\n * came from without an extra Area lookup.\n */\n localName: z.string().optional(),\n});\nexport type AreaLineageSnapshot = z.infer<typeof areaLineageSnapshotSchema>;\n"]}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Comment wire shapes (with one-level reply nesting).
3
+ *
4
+ * Comments attach to any feed item via `(contentKind, contentId)` — the
5
+ * same polymorphic key used by reactions and upvotes. One level of
6
+ * nesting only: a reply to a comment is allowed; a reply to a reply
7
+ * is rejected at the server.
8
+ *
9
+ * The shape mirrors the post's `viewer` pattern so the client can
10
+ * render an edit button on own comments and a reaction picker on
11
+ * every comment.
12
+ *
13
+ * @module community-schema/comment
14
+ */
15
+ import { z } from 'zod';
16
+ export declare const communityCommentWireSchema: z.ZodObject<{
17
+ _id: z.ZodString;
18
+ contentKind: z.ZodEnum<{
19
+ post: "post";
20
+ lost_found: "lost_found";
21
+ voice_box: "voice_box";
22
+ }>;
23
+ contentId: z.ZodString;
24
+ parentCommentId: z.ZodNullable<z.ZodString>;
25
+ author: z.ZodObject<{
26
+ userId: z.ZodString;
27
+ displayName: z.ZodString;
28
+ avatarUrl: z.ZodOptional<z.ZodString>;
29
+ localName: z.ZodOptional<z.ZodString>;
30
+ }, z.core.$strip>;
31
+ text: z.ZodString;
32
+ upvoteCount: z.ZodNumber;
33
+ replyCount: z.ZodNumber;
34
+ reactionCounts: z.ZodRecord<z.ZodString, z.ZodNumber>;
35
+ createdAt: z.ZodString;
36
+ editedAt: z.ZodNullable<z.ZodString>;
37
+ deletedAt: z.ZodNullable<z.ZodString>;
38
+ viewer: z.ZodOptional<z.ZodObject<{
39
+ upvoted: z.ZodBoolean;
40
+ reaction: z.ZodNullable<z.ZodString>;
41
+ isAuthor: z.ZodBoolean;
42
+ }, z.core.$strip>>;
43
+ }, z.core.$strip>;
44
+ export type CommunityCommentWire = z.infer<typeof communityCommentWireSchema>;
45
+ export declare const createCommentBodySchema: z.ZodObject<{
46
+ text: z.ZodString;
47
+ parentCommentId: z.ZodNullable<z.ZodString>;
48
+ }, z.core.$strip>;
49
+ export type CreateCommentBody = z.infer<typeof createCommentBodySchema>;
50
+ export declare const updateCommentBodySchema: z.ZodObject<{
51
+ text: z.ZodString;
52
+ }, z.core.$strip>;
53
+ export type UpdateCommentBody = z.infer<typeof updateCommentBodySchema>;
54
+ //# sourceMappingURL=comment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment.d.ts","sourceRoot":"","sources":["../src/comment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkCrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E,eAAO,MAAM,uBAAuB;;;iBAMlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAIxE,eAAO,MAAM,uBAAuB;;iBAElC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Comment wire shapes (with one-level reply nesting).
3
+ *
4
+ * Comments attach to any feed item via `(contentKind, contentId)` — the
5
+ * same polymorphic key used by reactions and upvotes. One level of
6
+ * nesting only: a reply to a comment is allowed; a reply to a reply
7
+ * is rejected at the server.
8
+ *
9
+ * The shape mirrors the post's `viewer` pattern so the client can
10
+ * render an edit button on own comments and a reaction picker on
11
+ * every comment.
12
+ *
13
+ * @module community-schema/comment
14
+ */
15
+ import { z } from 'zod';
16
+ import { COMMENT_MAX_BODY_CHARS } from './constants.js';
17
+ import { contentKindSchema } from './enums.js';
18
+ import { postAuthorSnapshotSchema } from './post.js';
19
+ // ─── Wire shape ─────────────────────────────────────────────────────────────
20
+ export const communityCommentWireSchema = z.object({
21
+ _id: z.string(),
22
+ /** The feed item this comment belongs to. */
23
+ contentKind: contentKindSchema,
24
+ contentId: z.string(),
25
+ /** The parent comment id, or null if this is a top-level comment.
26
+ * A non-null value MAY only point at a top-level comment — the
27
+ * server enforces this so the tree never grows to depth 3. */
28
+ parentCommentId: z.string().nullable(),
29
+ author: postAuthorSnapshotSchema,
30
+ // Body
31
+ text: z.string().max(COMMENT_MAX_BODY_CHARS),
32
+ // Denormalised engagement
33
+ upvoteCount: z.number().int().nonnegative(),
34
+ /** Number of replies. Only meaningful on top-level comments; always
35
+ * 0 on replies (the server enforces no-nesting). */
36
+ replyCount: z.number().int().nonnegative(),
37
+ reactionCounts: z.record(z.string(), z.number().int().nonnegative()),
38
+ // Lifecycle
39
+ createdAt: z.string().datetime(),
40
+ editedAt: z.string().datetime().nullable(),
41
+ deletedAt: z.string().datetime().nullable(),
42
+ // Per-viewer state
43
+ viewer: z
44
+ .object({
45
+ upvoted: z.boolean(),
46
+ reaction: z.string().nullable(),
47
+ isAuthor: z.boolean(),
48
+ })
49
+ .optional(),
50
+ });
51
+ // ─── Create body ────────────────────────────────────────────────────────────
52
+ export const createCommentBodySchema = z.object({
53
+ text: z.string().min(1).max(COMMENT_MAX_BODY_CHARS),
54
+ /** Null = top-level comment on the content; a string id = reply to
55
+ * a specific top-level comment. Server rejects ids pointing at a
56
+ * reply (depth-3 attempt). */
57
+ parentCommentId: z.string().nullable(),
58
+ });
59
+ // ─── Update body ────────────────────────────────────────────────────────────
60
+ export const updateCommentBodySchema = z.object({
61
+ text: z.string().min(1).max(COMMENT_MAX_BODY_CHARS),
62
+ });
63
+ //# sourceMappingURL=comment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment.js","sourceRoot":"","sources":["../src/comment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAErD,+EAA+E;AAE/E,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,6CAA6C;IAC7C,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB;;mEAE+D;IAC/D,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACtC,MAAM,EAAE,wBAAwB;IAEhC,OAAO;IACP,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,sBAAsB,CAAC;IAE5C,0BAA0B;IAC1B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC3C;yDACqD;IACrD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC1C,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAEpE,YAAY;IACZ,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC1C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAE3C,mBAAmB;IACnB,MAAM,EAAE,CAAC;SACN,MAAM,CAAC;QACN,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;QACpB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;KACtB,CAAC;SACD,QAAQ,EAAE;CACd,CAAC,CAAC;AAGH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;IACnD;;mCAE+B;IAC/B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC;AAGH,+EAA+E;AAE/E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;CACpD,CAAC,CAAC","sourcesContent":["/**\n * Comment wire shapes (with one-level reply nesting).\n *\n * Comments attach to any feed item via `(contentKind, contentId)` — the\n * same polymorphic key used by reactions and upvotes. One level of\n * nesting only: a reply to a comment is allowed; a reply to a reply\n * is rejected at the server.\n *\n * The shape mirrors the post's `viewer` pattern so the client can\n * render an edit button on own comments and a reaction picker on\n * every comment.\n *\n * @module community-schema/comment\n */\n\nimport { z } from 'zod';\nimport { COMMENT_MAX_BODY_CHARS } from './constants.js';\nimport { contentKindSchema } from './enums.js';\nimport { postAuthorSnapshotSchema } from './post.js';\n\n// ─── Wire shape ─────────────────────────────────────────────────────────────\n\nexport const communityCommentWireSchema = z.object({\n _id: z.string(),\n /** The feed item this comment belongs to. */\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** The parent comment id, or null if this is a top-level comment.\n * A non-null value MAY only point at a top-level comment — the\n * server enforces this so the tree never grows to depth 3. */\n parentCommentId: z.string().nullable(),\n author: postAuthorSnapshotSchema,\n\n // Body\n text: z.string().max(COMMENT_MAX_BODY_CHARS),\n\n // Denormalised engagement\n upvoteCount: z.number().int().nonnegative(),\n /** Number of replies. Only meaningful on top-level comments; always\n * 0 on replies (the server enforces no-nesting). */\n replyCount: z.number().int().nonnegative(),\n reactionCounts: z.record(z.string(), z.number().int().nonnegative()),\n\n // Lifecycle\n createdAt: z.string().datetime(),\n editedAt: z.string().datetime().nullable(),\n deletedAt: z.string().datetime().nullable(),\n\n // Per-viewer state\n viewer: z\n .object({\n upvoted: z.boolean(),\n reaction: z.string().nullable(),\n isAuthor: z.boolean(),\n })\n .optional(),\n});\nexport type CommunityCommentWire = z.infer<typeof communityCommentWireSchema>;\n\n// ─── Create body ────────────────────────────────────────────────────────────\n\nexport const createCommentBodySchema = z.object({\n text: z.string().min(1).max(COMMENT_MAX_BODY_CHARS),\n /** Null = top-level comment on the content; a string id = reply to\n * a specific top-level comment. Server rejects ids pointing at a\n * reply (depth-3 attempt). */\n parentCommentId: z.string().nullable(),\n});\nexport type CreateCommentBody = z.infer<typeof createCommentBodySchema>;\n\n// ─── Update body ────────────────────────────────────────────────────────────\n\nexport const updateCommentBodySchema = z.object({\n text: z.string().min(1).max(COMMENT_MAX_BODY_CHARS),\n});\nexport type UpdateCommentBody = z.infer<typeof updateCommentBodySchema>;\n"]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tunable constants shared across the community feature.
3
+ *
4
+ * Live here (not inline in feature code) so backend and every frontend
5
+ * agree byte-for-byte on the same limits, paginations, and timeouts.
6
+ * Bump versions of this package when these change.
7
+ *
8
+ * @module community-schema/constants
9
+ */
10
+ /** Feed list default page size. */
11
+ export declare const FEED_PAGE_SIZE = 20;
12
+ /** People list default page size. */
13
+ export declare const PEOPLE_PAGE_SIZE = 30;
14
+ /** Comments per page on a single post. */
15
+ export declare const COMMENTS_PAGE_SIZE = 20;
16
+ /** Replies per top-level comment loaded with the first comments page. */
17
+ export declare const REPLIES_PRELOAD_COUNT = 3;
18
+ /** Max images per post. */
19
+ export declare const POST_MAX_IMAGES = 10;
20
+ /** Max videos per post. */
21
+ export declare const POST_MAX_VIDEOS = 5;
22
+ /** Max duration of a single video in seconds (10 min). */
23
+ export declare const POST_MAX_VIDEO_SECONDS: number;
24
+ /** Per-image hard cap (15 MB). The file service compresses to fit. */
25
+ export declare const POST_MAX_IMAGE_BYTES: number;
26
+ /** Per-video hard cap (200 MB). */
27
+ export declare const POST_MAX_VIDEO_BYTES: number;
28
+ /** Post body character cap. Long enough for long-form, short enough to
29
+ * keep feed rendering snappy. */
30
+ export declare const POST_MAX_BODY_CHARS = 5000;
31
+ /** Comment + reply body character cap. */
32
+ export declare const COMMENT_MAX_BODY_CHARS = 2000;
33
+ /** Free-text bio on a public profile. */
34
+ export declare const PROFILE_MAX_BIO_CHARS = 280;
35
+ /** Window during which an author can edit their own post / comment, in ms. */
36
+ export declare const EDIT_GRACE_WINDOW_MS: number;
37
+ /** Max members in a single user-created group chat. */
38
+ export declare const GROUP_MAX_MEMBERS = 256;
39
+ /** Group name length. */
40
+ export declare const GROUP_NAME_MAX_CHARS = 60;
41
+ export declare const GROUP_NAME_MIN_CHARS = 2;
42
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,mCAAmC;AACnC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,qCAAqC;AACrC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,0CAA0C;AAC1C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAIvC,2BAA2B;AAC3B,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,2BAA2B;AAC3B,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,QAAU,CAAC;AAE9C,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,QAAmB,CAAC;AAErD,mCAAmC;AACnC,eAAO,MAAM,oBAAoB,QAAoB,CAAC;AAItD;kCACkC;AAClC,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,0CAA0C;AAC1C,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,yCAAyC;AACzC,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAIzC,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,QAAgB,CAAC;AAIlD,uDAAuD;AACvD,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,yBAAyB;AACzB,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,oBAAoB,IAAI,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Tunable constants shared across the community feature.
3
+ *
4
+ * Live here (not inline in feature code) so backend and every frontend
5
+ * agree byte-for-byte on the same limits, paginations, and timeouts.
6
+ * Bump versions of this package when these change.
7
+ *
8
+ * @module community-schema/constants
9
+ */
10
+ // ─── Pagination ─────────────────────────────────────────────────────────────
11
+ /** Feed list default page size. */
12
+ export const FEED_PAGE_SIZE = 20;
13
+ /** People list default page size. */
14
+ export const PEOPLE_PAGE_SIZE = 30;
15
+ /** Comments per page on a single post. */
16
+ export const COMMENTS_PAGE_SIZE = 20;
17
+ /** Replies per top-level comment loaded with the first comments page. */
18
+ export const REPLIES_PRELOAD_COUNT = 3;
19
+ // ─── Post media limits ─────────────────────────────────────────────────────
20
+ /** Max images per post. */
21
+ export const POST_MAX_IMAGES = 10;
22
+ /** Max videos per post. */
23
+ export const POST_MAX_VIDEOS = 5;
24
+ /** Max duration of a single video in seconds (10 min). */
25
+ export const POST_MAX_VIDEO_SECONDS = 10 * 60;
26
+ /** Per-image hard cap (15 MB). The file service compresses to fit. */
27
+ export const POST_MAX_IMAGE_BYTES = 15 * 1024 * 1024;
28
+ /** Per-video hard cap (200 MB). */
29
+ export const POST_MAX_VIDEO_BYTES = 200 * 1024 * 1024;
30
+ // ─── Text limits ────────────────────────────────────────────────────────────
31
+ /** Post body character cap. Long enough for long-form, short enough to
32
+ * keep feed rendering snappy. */
33
+ export const POST_MAX_BODY_CHARS = 5000;
34
+ /** Comment + reply body character cap. */
35
+ export const COMMENT_MAX_BODY_CHARS = 2000;
36
+ /** Free-text bio on a public profile. */
37
+ export const PROFILE_MAX_BIO_CHARS = 280;
38
+ // ─── Engagement ─────────────────────────────────────────────────────────────
39
+ /** Window during which an author can edit their own post / comment, in ms. */
40
+ export const EDIT_GRACE_WINDOW_MS = 5 * 60 * 1000;
41
+ // ─── Groups ─────────────────────────────────────────────────────────────────
42
+ /** Max members in a single user-created group chat. */
43
+ export const GROUP_MAX_MEMBERS = 256;
44
+ /** Group name length. */
45
+ export const GROUP_NAME_MAX_CHARS = 60;
46
+ export const GROUP_NAME_MIN_CHARS = 2;
47
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,+EAA+E;AAE/E,mCAAmC;AACnC,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,qCAAqC;AACrC,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAEnC,0CAA0C;AAC1C,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,yEAAyE;AACzE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,8EAA8E;AAE9E,2BAA2B;AAC3B,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAElC,2BAA2B;AAC3B,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AAEjC,0DAA0D;AAC1D,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,CAAC;AAE9C,sEAAsE;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAErD,mCAAmC;AACnC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtD,+EAA+E;AAE/E;kCACkC;AAClC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAExC,0CAA0C;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAE3C,yCAAyC;AACzC,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC,+EAA+E;AAE/E,8EAA8E;AAC9E,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAElD,+EAA+E;AAE/E,uDAAuD;AACvD,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,yBAAyB;AACzB,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AACvC,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC","sourcesContent":["/**\n * Tunable constants shared across the community feature.\n *\n * Live here (not inline in feature code) so backend and every frontend\n * agree byte-for-byte on the same limits, paginations, and timeouts.\n * Bump versions of this package when these change.\n *\n * @module community-schema/constants\n */\n\n// ─── Pagination ─────────────────────────────────────────────────────────────\n\n/** Feed list default page size. */\nexport const FEED_PAGE_SIZE = 20;\n\n/** People list default page size. */\nexport const PEOPLE_PAGE_SIZE = 30;\n\n/** Comments per page on a single post. */\nexport const COMMENTS_PAGE_SIZE = 20;\n\n/** Replies per top-level comment loaded with the first comments page. */\nexport const REPLIES_PRELOAD_COUNT = 3;\n\n// ─── Post media limits ─────────────────────────────────────────────────────\n\n/** Max images per post. */\nexport const POST_MAX_IMAGES = 10;\n\n/** Max videos per post. */\nexport const POST_MAX_VIDEOS = 5;\n\n/** Max duration of a single video in seconds (10 min). */\nexport const POST_MAX_VIDEO_SECONDS = 10 * 60;\n\n/** Per-image hard cap (15 MB). The file service compresses to fit. */\nexport const POST_MAX_IMAGE_BYTES = 15 * 1024 * 1024;\n\n/** Per-video hard cap (200 MB). */\nexport const POST_MAX_VIDEO_BYTES = 200 * 1024 * 1024;\n\n// ─── Text limits ────────────────────────────────────────────────────────────\n\n/** Post body character cap. Long enough for long-form, short enough to\n * keep feed rendering snappy. */\nexport const POST_MAX_BODY_CHARS = 5000;\n\n/** Comment + reply body character cap. */\nexport const COMMENT_MAX_BODY_CHARS = 2000;\n\n/** Free-text bio on a public profile. */\nexport const PROFILE_MAX_BIO_CHARS = 280;\n\n// ─── Engagement ─────────────────────────────────────────────────────────────\n\n/** Window during which an author can edit their own post / comment, in ms. */\nexport const EDIT_GRACE_WINDOW_MS = 5 * 60 * 1000;\n\n// ─── Groups ─────────────────────────────────────────────────────────────────\n\n/** Max members in a single user-created group chat. */\nexport const GROUP_MAX_MEMBERS = 256;\n\n/** Group name length. */\nexport const GROUP_NAME_MAX_CHARS = 60;\nexport const GROUP_NAME_MIN_CHARS = 2;\n"]}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Engagement wire shapes — upvotes and reactions.
3
+ *
4
+ * Both are keyed by `(contentKind, contentId)` so the same engagement
5
+ * bar applies to every kind of feed item (post, lost-and-found,
6
+ * voice-box) and to comments.
7
+ *
8
+ * Per the product decision, upvote and reaction are TWO INDEPENDENT
9
+ * axes — a viewer can upvote AND react. Upvote drives feed sort;
10
+ * reaction is the expressive layer.
11
+ *
12
+ * @module community-schema/engagement
13
+ */
14
+ import { z } from 'zod';
15
+ /**
16
+ * What the client sends to `POST /api/v1/community/:contentKind/:id/upvote`.
17
+ * The endpoint is a toggle — calling it twice in a row removes the
18
+ * upvote. Idempotent: the server returns the post-toggle state.
19
+ */
20
+ export declare const toggleUpvoteResponseSchema: z.ZodObject<{
21
+ contentKind: z.ZodEnum<{
22
+ post: "post";
23
+ lost_found: "lost_found";
24
+ voice_box: "voice_box";
25
+ }>;
26
+ contentId: z.ZodString;
27
+ upvoted: z.ZodBoolean;
28
+ upvoteCount: z.ZodNumber;
29
+ }, z.core.$strip>;
30
+ export type ToggleUpvoteResponse = z.infer<typeof toggleUpvoteResponseSchema>;
31
+ /**
32
+ * What the client sends to `POST /api/v1/community/:contentKind/:id/reactions`.
33
+ *
34
+ * Setting `reaction` to a value replaces the user's current reaction
35
+ * (a user may have at most ONE active reaction per target).
36
+ * Setting `reaction` to null clears it.
37
+ */
38
+ export declare const setReactionBodySchema: z.ZodObject<{
39
+ reaction: z.ZodNullable<z.ZodEnum<{
40
+ like: "like";
41
+ love: "love";
42
+ haha: "haha";
43
+ wow: "wow";
44
+ sad: "sad";
45
+ angry: "angry";
46
+ support: "support";
47
+ }>>;
48
+ }, z.core.$strip>;
49
+ export type SetReactionBody = z.infer<typeof setReactionBodySchema>;
50
+ export declare const setReactionResponseSchema: z.ZodObject<{
51
+ contentKind: z.ZodEnum<{
52
+ post: "post";
53
+ lost_found: "lost_found";
54
+ voice_box: "voice_box";
55
+ }>;
56
+ contentId: z.ZodString;
57
+ reaction: z.ZodNullable<z.ZodEnum<{
58
+ like: "like";
59
+ love: "love";
60
+ haha: "haha";
61
+ wow: "wow";
62
+ sad: "sad";
63
+ angry: "angry";
64
+ support: "support";
65
+ }>>;
66
+ reactionCounts: z.ZodRecord<z.ZodString, z.ZodNumber>;
67
+ }, z.core.$strip>;
68
+ export type SetReactionResponse = z.infer<typeof setReactionResponseSchema>;
69
+ //# sourceMappingURL=engagement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engagement.d.ts","sourceRoot":"","sources":["../src/engagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;GAIG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;iBAQrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;iBAEhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;iBAOpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Engagement wire shapes — upvotes and reactions.
3
+ *
4
+ * Both are keyed by `(contentKind, contentId)` so the same engagement
5
+ * bar applies to every kind of feed item (post, lost-and-found,
6
+ * voice-box) and to comments.
7
+ *
8
+ * Per the product decision, upvote and reaction are TWO INDEPENDENT
9
+ * axes — a viewer can upvote AND react. Upvote drives feed sort;
10
+ * reaction is the expressive layer.
11
+ *
12
+ * @module community-schema/engagement
13
+ */
14
+ import { z } from 'zod';
15
+ import { contentKindSchema, reactionTypeSchema } from './enums.js';
16
+ // ─── Upvote ─────────────────────────────────────────────────────────────────
17
+ /**
18
+ * What the client sends to `POST /api/v1/community/:contentKind/:id/upvote`.
19
+ * The endpoint is a toggle — calling it twice in a row removes the
20
+ * upvote. Idempotent: the server returns the post-toggle state.
21
+ */
22
+ export const toggleUpvoteResponseSchema = z.object({
23
+ contentKind: contentKindSchema,
24
+ contentId: z.string(),
25
+ /** New upvote state for the calling user. */
26
+ upvoted: z.boolean(),
27
+ /** Post-toggle aggregate count, returned so the client can render
28
+ * without a follow-up read. */
29
+ upvoteCount: z.number().int().nonnegative(),
30
+ });
31
+ // ─── Reaction ───────────────────────────────────────────────────────────────
32
+ /**
33
+ * What the client sends to `POST /api/v1/community/:contentKind/:id/reactions`.
34
+ *
35
+ * Setting `reaction` to a value replaces the user's current reaction
36
+ * (a user may have at most ONE active reaction per target).
37
+ * Setting `reaction` to null clears it.
38
+ */
39
+ export const setReactionBodySchema = z.object({
40
+ reaction: reactionTypeSchema.nullable(),
41
+ });
42
+ export const setReactionResponseSchema = z.object({
43
+ contentKind: contentKindSchema,
44
+ contentId: z.string(),
45
+ /** The viewer's new reaction, or null when cleared. */
46
+ reaction: reactionTypeSchema.nullable(),
47
+ /** Post-set aggregate counts, keyed by reaction type. */
48
+ reactionCounts: z.record(z.string(), z.number().int().nonnegative()),
49
+ });
50
+ //# sourceMappingURL=engagement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engagement.js","sourceRoot":"","sources":["../src/engagement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,+EAA+E;AAE/E;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,6CAA6C;IAC7C,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB;oCACgC;IAChC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CAC5C,CAAC,CAAC;AAGH,+EAA+E;AAE/E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,QAAQ,EAAE,kBAAkB,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,WAAW,EAAE,iBAAiB;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,uDAAuD;IACvD,QAAQ,EAAE,kBAAkB,CAAC,QAAQ,EAAE;IACvC,yDAAyD;IACzD,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;CACrE,CAAC,CAAC","sourcesContent":["/**\n * Engagement wire shapes — upvotes and reactions.\n *\n * Both are keyed by `(contentKind, contentId)` so the same engagement\n * bar applies to every kind of feed item (post, lost-and-found,\n * voice-box) and to comments.\n *\n * Per the product decision, upvote and reaction are TWO INDEPENDENT\n * axes — a viewer can upvote AND react. Upvote drives feed sort;\n * reaction is the expressive layer.\n *\n * @module community-schema/engagement\n */\n\nimport { z } from 'zod';\nimport { contentKindSchema, reactionTypeSchema } from './enums.js';\n\n// ─── Upvote ─────────────────────────────────────────────────────────────────\n\n/**\n * What the client sends to `POST /api/v1/community/:contentKind/:id/upvote`.\n * The endpoint is a toggle — calling it twice in a row removes the\n * upvote. Idempotent: the server returns the post-toggle state.\n */\nexport const toggleUpvoteResponseSchema = z.object({\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** New upvote state for the calling user. */\n upvoted: z.boolean(),\n /** Post-toggle aggregate count, returned so the client can render\n * without a follow-up read. */\n upvoteCount: z.number().int().nonnegative(),\n});\nexport type ToggleUpvoteResponse = z.infer<typeof toggleUpvoteResponseSchema>;\n\n// ─── Reaction ───────────────────────────────────────────────────────────────\n\n/**\n * What the client sends to `POST /api/v1/community/:contentKind/:id/reactions`.\n *\n * Setting `reaction` to a value replaces the user's current reaction\n * (a user may have at most ONE active reaction per target).\n * Setting `reaction` to null clears it.\n */\nexport const setReactionBodySchema = z.object({\n reaction: reactionTypeSchema.nullable(),\n});\nexport type SetReactionBody = z.infer<typeof setReactionBodySchema>;\n\nexport const setReactionResponseSchema = z.object({\n contentKind: contentKindSchema,\n contentId: z.string(),\n /** The viewer's new reaction, or null when cleared. */\n reaction: reactionTypeSchema.nullable(),\n /** Post-set aggregate counts, keyed by reaction type. */\n reactionCounts: z.record(z.string(), z.number().int().nonnegative()),\n});\nexport type SetReactionResponse = z.infer<typeof setReactionResponseSchema>;\n"]}