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.
- package/README.md +16 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/client/api.d.ts +3 -1
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -1
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +25 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/comments.d.ts +10 -10
- package/dist/component/entries.d.ts +53 -28
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +7 -3
- package/dist/component/entries.js.map +1 -1
- package/dist/component/helpers.d.ts +2 -1
- package/dist/component/helpers.d.ts.map +1 -1
- package/dist/component/helpers.js +46 -1
- package/dist/component/helpers.js.map +1 -1
- package/dist/component/model.d.ts +70 -3
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/model.js +7 -0
- package/dist/component/model.js.map +1 -1
- package/dist/component/schema.d.ts +12 -1
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +2 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/react/index.d.ts +22 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/test.d.ts +12 -1
- package/dist/test.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +4 -0
- package/src/client/index.ts +6 -0
- package/src/component/_generated/component.ts +29 -1
- package/src/component/entries.ts +12 -1
- package/src/component/helpers.ts +70 -0
- package/src/component/model.ts +30 -0
- package/src/component/schema.ts +6 -1
package/src/component/helpers.ts
CHANGED
|
@@ -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
|
|
package/src/component/model.ts
CHANGED
|
@@ -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
|
|
package/src/component/schema.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
|
|
4
|
-
import {
|
|
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"])
|