convex-feedback 0.1.3 → 0.1.4

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 (39) hide show
  1. package/README.md +16 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/client/api.d.ts +3 -1
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/index.d.ts +1 -1
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/index.js +4 -1
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/component/_generated/component.d.ts +25 -0
  10. package/dist/component/_generated/component.d.ts.map +1 -1
  11. package/dist/component/comments.d.ts +10 -10
  12. package/dist/component/entries.d.ts +53 -28
  13. package/dist/component/entries.d.ts.map +1 -1
  14. package/dist/component/entries.js +7 -3
  15. package/dist/component/entries.js.map +1 -1
  16. package/dist/component/helpers.d.ts +2 -1
  17. package/dist/component/helpers.d.ts.map +1 -1
  18. package/dist/component/helpers.js +46 -1
  19. package/dist/component/helpers.js.map +1 -1
  20. package/dist/component/model.d.ts +70 -3
  21. package/dist/component/model.d.ts.map +1 -1
  22. package/dist/component/model.js +7 -0
  23. package/dist/component/model.js.map +1 -1
  24. package/dist/component/schema.d.ts +12 -1
  25. package/dist/component/schema.d.ts.map +1 -1
  26. package/dist/component/schema.js +2 -1
  27. package/dist/component/schema.js.map +1 -1
  28. package/dist/react/index.d.ts +22 -2
  29. package/dist/react/index.d.ts.map +1 -1
  30. package/dist/test.d.ts +12 -1
  31. package/dist/test.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/client/api.ts +4 -0
  34. package/src/client/index.ts +6 -0
  35. package/src/component/_generated/component.ts +29 -1
  36. package/src/component/entries.ts +12 -1
  37. package/src/component/helpers.ts +70 -0
  38. package/src/component/model.ts +30 -0
  39. package/src/component/schema.ts +6 -1
@@ -4,6 +4,72 @@ import type { DataModel } from "./_generated/dataModel.js";
4
4
  import type { QueryCtx } from "./types.js";
5
5
  import type { FeedbackComment, FeedbackEntry } from "./model.js";
6
6
 
7
+ const metadataMaximumKeysPerSection = 32;
8
+ const metadataMaximumKeyLength = 64;
9
+ const metadataMaximumStringLength = 1_024;
10
+ const metadataMaximumEncodedBytes = 16 * 1_024;
11
+ const forbiddenMetadataKeys = new Set([
12
+ "__proto__",
13
+ "constructor",
14
+ "prototype",
15
+ ]);
16
+
17
+ export function validateFeedbackMetadata(
18
+ metadata: DataModel["entries"]["document"]["metadata"],
19
+ ): void {
20
+ if (metadata === undefined) return;
21
+
22
+ for (const [sectionName, section] of Object.entries(metadata)) {
23
+ if (section === undefined) continue;
24
+ const entries = Object.entries(section);
25
+
26
+ if (entries.length > metadataMaximumKeysPerSection) {
27
+ throw new ConvexError(
28
+ `Metadata section '${sectionName}' must contain ${metadataMaximumKeysPerSection} keys or fewer.`,
29
+ );
30
+ }
31
+
32
+ for (const [key, value] of entries) {
33
+ if (key.length === 0) {
34
+ throw new ConvexError(
35
+ `Metadata section '${sectionName}' contains an empty key.`,
36
+ );
37
+ }
38
+ if (key.length > metadataMaximumKeyLength) {
39
+ throw new ConvexError(
40
+ `Metadata key '${key}' in section '${sectionName}' must be ${metadataMaximumKeyLength} characters or fewer.`,
41
+ );
42
+ }
43
+ if (
44
+ key.startsWith("_") ||
45
+ key.startsWith("$") ||
46
+ forbiddenMetadataKeys.has(key)
47
+ ) {
48
+ throw new ConvexError(
49
+ `Metadata key '${key}' in section '${sectionName}' is reserved and cannot be used.`,
50
+ );
51
+ }
52
+ if (
53
+ typeof value === "string" &&
54
+ value.length > metadataMaximumStringLength
55
+ ) {
56
+ throw new ConvexError(
57
+ `Metadata value for '${sectionName}.${key}' must be ${metadataMaximumStringLength} characters or fewer.`,
58
+ );
59
+ }
60
+ }
61
+ }
62
+
63
+ const encodedBytes = new TextEncoder().encode(
64
+ JSON.stringify(metadata),
65
+ ).length;
66
+ if (encodedBytes > metadataMaximumEncodedBytes) {
67
+ throw new ConvexError(
68
+ `Metadata must be ${metadataMaximumEncodedBytes} UTF-8 bytes or fewer; received ${encodedBytes} bytes.`,
69
+ );
70
+ }
71
+ }
72
+
7
73
  export function normalizeTitle(value: string): string {
8
74
  return value.trim().replace(/\s+/g, " ").toLocaleLowerCase("en-US");
9
75
  }
@@ -41,6 +107,7 @@ export async function serializeEntry(
41
107
  ctx: QueryCtx,
42
108
  entry: DataModel["entries"]["document"],
43
109
  viewerActorId: string | undefined,
110
+ includeMetadata = false,
44
111
  ): Promise<FeedbackEntry> {
45
112
  const reaction =
46
113
  viewerActorId !== undefined
@@ -64,6 +131,9 @@ export async function serializeEntry(
64
131
  commentCount: entry.commentCount,
65
132
  ...(entry.updatedAt === undefined ? {} : { updatedAt: entry.updatedAt }),
66
133
  viewerHasUpvoted: reaction !== null,
134
+ ...(includeMetadata && entry.metadata !== undefined
135
+ ? { metadata: entry.metadata }
136
+ : {}),
67
137
  };
68
138
  }
69
139
 
@@ -32,6 +32,22 @@ export const actorValidator = v.object({
32
32
  isModerator: v.boolean(),
33
33
  });
34
34
 
35
+ export const feedbackMetadataValueValidator = v.union(
36
+ v.string(),
37
+ v.number(),
38
+ v.boolean(),
39
+ );
40
+
41
+ export const feedbackMetadataRecordValidator = v.record(
42
+ v.string(),
43
+ feedbackMetadataValueValidator,
44
+ );
45
+
46
+ export const feedbackMetadataValidator = v.object({
47
+ standard: v.optional(feedbackMetadataRecordValidator),
48
+ additional: v.optional(feedbackMetadataRecordValidator),
49
+ });
50
+
35
51
  export const publicEntryValidator = v.object({
36
52
  id: v.string(),
37
53
  creationTime: v.number(),
@@ -44,6 +60,7 @@ export const publicEntryValidator = v.object({
44
60
  commentCount: v.number(),
45
61
  updatedAt: v.optional(v.number()),
46
62
  viewerHasUpvoted: v.boolean(),
63
+ metadata: v.optional(feedbackMetadataValidator),
47
64
  });
48
65
 
49
66
  export const publicCommentValidator = v.object({
@@ -119,6 +136,14 @@ export type CommentSort = Infer<typeof commentSortValidator>;
119
136
  */
120
137
  export type FeedbackActor = Infer<typeof actorValidator>;
121
138
 
139
+ /** Scalar value accepted in entry diagnostic metadata. */
140
+ export type FeedbackMetadataValue = Infer<
141
+ typeof feedbackMetadataValueValidator
142
+ >;
143
+
144
+ /** Flat metadata values grouped by their source. */
145
+ export type FeedbackMetadata = Infer<typeof feedbackMetadataValidator>;
146
+
122
147
  /**
123
148
  * Public representation of a feedback, feature-request, or bug-report entry.
124
149
  *
@@ -157,6 +182,11 @@ export type FeedbackActor = Infer<typeof actorValidator>;
157
182
  * @property viewerHasUpvoted
158
183
  * Whether the actor associated with the current query has upvoted the entry.
159
184
  * `false` when no viewer actor is available.
185
+ *
186
+ * @property metadata
187
+ * Creation-time diagnostic metadata. Present only when `getEntry` is queried
188
+ * by a moderator. Ordinary entry lists, searches, and non-moderator reads omit
189
+ * this property.
160
190
  */
161
191
  export type FeedbackEntry = Infer<typeof publicEntryValidator>;
162
192
 
@@ -1,7 +1,11 @@
1
1
  import { defineSchema, defineTable } from "convex/server";
2
2
  import { v } from "convex/values";
3
3
 
4
- import { entryKindValidator, entryStatusValidator } from "./model.js";
4
+ import {
5
+ entryKindValidator,
6
+ entryStatusValidator,
7
+ feedbackMetadataValidator,
8
+ } from "./model.js";
5
9
 
6
10
  const schema = defineSchema({
7
11
  entries: defineTable({
@@ -15,6 +19,7 @@ const schema = defineSchema({
15
19
  upvoteCount: v.number(),
16
20
  commentCount: v.number(),
17
21
  updatedAt: v.optional(v.number()),
22
+ metadata: v.optional(feedbackMetadataValidator),
18
23
  })
19
24
  .index("by_kind", ["kind"])
20
25
  .index("by_status", ["status"])