@slango/mangusta 1.0.7 → 1.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @slango/mangusta@1.0.7 build /home/runner/work/slango/slango/packages/mangusta
2
+ > @slango/mangusta@1.1.0 build /home/runner/work/slango/slango/packages/mangusta
3
3
  > tsc -p tsconfig.build.json
4
4
 
@@ -1,4 +1,4 @@
1
1
 
2
- > @slango/mangusta@1.0.7 lint /home/runner/work/slango/slango/packages/mangusta
2
+ > @slango/mangusta@1.1.0 lint /home/runner/work/slango/slango/packages/mangusta
3
3
  > eslint . --max-warnings 0
4
4
 
@@ -1,28 +1,29 @@
1
1
 
2
- > @slango/mangusta@1.0.7 test /home/runner/work/slango/slango/packages/mangusta
2
+ > @slango/mangusta@1.1.0 test /home/runner/work/slango/slango/packages/mangusta
3
3
  > vitest --run
4
4
 
5
5
 
6
6
   RUN  v3.2.4 /home/runner/work/slango/slango/packages/mangusta
7
7
 
8
- stdout | src/middleware/email.spec.ts
8
+ stdout | src/middleware/compactId.spec.ts
9
9
  Downloading MongoDB "7.0.14": 0% (0mb / 80.8mb)
10
10
 
11
- stdout | src/middleware/email.spec.ts
11
+ stdout | src/middleware/compactId.spec.ts
12
12
  Downloading MongoDB "7.0.14": 100% (80.8mb / 80.8mb)
13
13
 
14
- stdout | src/middleware/email.spec.ts
14
+ stdout | src/middleware/compactId.spec.ts
15
15
  Downloading MongoDB "7.0.14": 100% (0mb / 0mb)
16
16
 
17
- ✓ src/middleware/email.spec.ts (7 tests) 4113ms
18
- ✓ src/middleware/owner.spec.ts (6 tests) 377ms
19
- ✓ src/middleware/timestamps.spec.ts (3 tests) 400ms
20
- ✓ src/middleware/compactId.spec.ts (6 tests) 6479ms
21
- ✓ src/middleware/tags.spec.ts (8 tests) 6599ms
22
- ✓ src/middleware/password.spec.ts (4 tests) 849ms
23
-
24
-  Test Files  6 passed (6)
25
-  Tests  34 passed (34)
26
-  Start at  04:54:35
27
-  Duration  8.53s (transform 278ms, setup 0ms, collect 3.20s, tests 18.82s, environment 4ms, prepare 920ms)
17
+ ✓ src/middleware/compactId.spec.ts (6 tests) 4046ms
18
+ ✓ src/middleware/email.spec.ts (7 tests) 392ms
19
+ ✓ src/middleware/owner.spec.ts (6 tests) 374ms
20
+ ✓ src/middleware/reactions.spec.ts (7 tests) 6514ms
21
+ ✓ src/middleware/tags.spec.ts (8 tests) 6698ms
22
+ ✓ src/middleware/timestamps.spec.ts (3 tests) 673ms
23
+ ✓ src/middleware/password.spec.ts (4 tests) 654ms
24
+
25
+  Test Files  7 passed (7)
26
+  Tests  41 passed (41)
27
+  Start at  09:54:04
28
+  Duration  9.25s (transform 294ms, setup 0ms, collect 3.82s, tests 19.35s, environment 2ms, prepare 840ms)
28
29
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @slango/mangusta
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9ef53b9: Reactions middleware
8
+
9
+ ## 1.0.8
10
+
11
+ ### Patch Changes
12
+
13
+ - 71b046e: Dependencies bump
14
+ - 71b046e: Dependencies bump
15
+
3
16
  ## 1.0.7
4
17
 
5
18
  ### Patch Changes
@@ -0,0 +1,5 @@
1
+ import { Types } from 'mongoose';
2
+ export type Id = string | Types.ObjectId;
3
+ export declare const toObjectId: (value: Id) => Types.ObjectId;
4
+ export declare const toStringId: (value: Id) => string;
5
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC,MAAM,MAAM,EAAE,GAAG,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC;AAEzC,eAAO,MAAM,UAAU,GAAI,OAAO,EAAE,KAAG,KAAK,CAAC,QAM5C,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,OAAO,EAAE,KAAG,MACqB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Types } from 'mongoose';
2
+ export const toObjectId = (value) => {
3
+ if (value instanceof Types.ObjectId) {
4
+ return value;
5
+ }
6
+ return new Types.ObjectId(value);
7
+ };
8
+ export const toStringId = (value) => value instanceof Types.ObjectId ? value.toString() : value;
9
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAIjC,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAS,EAAkB,EAAE;IACtD,IAAI,KAAK,YAAY,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACnC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAS,EAAU,EAAE,CAC9C,KAAK,YAAY,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { Types } from 'mongoose';
2
+ import { Id } from '../helpers.js';
3
+ import { PluginFunction } from '../types.js';
4
+ export declare const defaultReactionTypes: readonly ["👍", "❤️", "🤔", "😂", "😮"];
5
+ export type ReactionType = (typeof defaultReactionTypes)[number];
6
+ export interface ReactionCountSummary {
7
+ perType: Record<string, number>;
8
+ total: number;
9
+ }
10
+ export type ReactionUserField<UserField extends string = 'user'> = {
11
+ [key in UserField]: Types.ObjectId;
12
+ };
13
+ export type ReactionTypeField<TypeField extends string = 'type'> = {
14
+ [key in TypeField]: string;
15
+ };
16
+ export type ReactionTimestampField<TimestampField extends string = 'createdAt'> = {
17
+ [key in TimestampField]?: Date;
18
+ };
19
+ export type ReactionEntry<UserField extends string = 'user', TypeField extends string = 'type', TimestampField extends string = 'createdAt'> = ReactionTimestampField<TimestampField> & ReactionTypeField<TypeField> & ReactionUserField<UserField>;
20
+ export type WithReactions<Field extends string = 'reactions', UserField extends string = 'user', TypeField extends string = 'type', TimestampField extends string = 'createdAt'> = {
21
+ [key in Field]: ReactionEntry<UserField, TypeField, TimestampField>[];
22
+ };
23
+ export interface WithReactionsMethods {
24
+ addReaction(user: Id, type: string, timestampValue?: Date): this;
25
+ countReactions(type?: string): number | ReactionCountSummary;
26
+ hasReaction(user: Id, type?: string): boolean;
27
+ removeReaction(user: Id, type?: string): this;
28
+ }
29
+ export interface ReactionsMiddlewareOptions<Field extends string = 'reactions', UserField extends string = 'user', TypeField extends string = 'type', TimestampField extends string = 'createdAt'> {
30
+ allowedTypes?: readonly string[];
31
+ allowMultiplePerUser?: boolean;
32
+ field?: Field;
33
+ indexType?: -1 | 1 | boolean;
34
+ indexUser?: -1 | 1 | boolean;
35
+ timestamp?: boolean;
36
+ timestampField?: TimestampField;
37
+ typeField?: TypeField;
38
+ userField?: UserField;
39
+ userRef?: string;
40
+ }
41
+ declare const reactionsMiddleware: PluginFunction<ReactionsMiddlewareOptions>;
42
+ export default reactionsMiddleware;
43
+ //# sourceMappingURL=reactions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactions.d.ts","sourceRoot":"","sources":["../../src/middleware/reactions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,201 @@
1
+ import { Schema, Types } from 'mongoose';
2
+ import { toObjectId, toStringId } from '../helpers.js';
3
+ export const defaultReactionTypes = ['👍', '❤️', '🤔', '😂', '😮'];
4
+ const reactionsMiddleware = (schema, { allowedTypes = defaultReactionTypes, allowMultiplePerUser = false, field = 'reactions', indexType = false, indexUser = true, timestamp = true, timestampField = 'createdAt', typeField = 'type', userField = 'user', userRef = 'User', } = {}) => {
5
+ const allowedTypesSet = new Set(allowedTypes);
6
+ const shouldEnforceAllowedTypes = allowedTypesSet.size > 0;
7
+ const reactionSchemaDefinition = {
8
+ [userField]: {
9
+ ref: userRef,
10
+ required: true,
11
+ type: Types.ObjectId,
12
+ },
13
+ [typeField]: {
14
+ required: true,
15
+ type: String,
16
+ ...(shouldEnforceAllowedTypes && { enum: Array.from(allowedTypesSet) }),
17
+ },
18
+ };
19
+ const reactionsSubSchema = new Schema(reactionSchemaDefinition, {
20
+ _id: false,
21
+ timestamps: timestamp ? { createdAt: timestampField, updatedAt: false } : false,
22
+ });
23
+ schema.add(new Schema({
24
+ [field]: {
25
+ default: [],
26
+ type: [reactionsSubSchema],
27
+ },
28
+ }));
29
+ if (indexUser) {
30
+ schema.index({ [`${field}.${userField}`]: indexUser === true ? 1 : indexUser });
31
+ }
32
+ if (indexType) {
33
+ schema.index({ [`${field}.${typeField}`]: indexType === true ? 1 : indexType });
34
+ }
35
+ const ensureAllowedType = (reactionType) => {
36
+ if (shouldEnforceAllowedTypes && !allowedTypesSet.has(reactionType)) {
37
+ throw new Error(`Reaction type "${reactionType}" is not permitted.`);
38
+ }
39
+ };
40
+ const normalizeEntry = (entry) => {
41
+ const userValue = entry[userField];
42
+ const typeValue = entry[typeField];
43
+ if (!userValue || !typeValue) {
44
+ return null;
45
+ }
46
+ ensureAllowedType(typeValue);
47
+ const normalized = {
48
+ ...entry,
49
+ [userField]: toObjectId(userValue),
50
+ [typeField]: typeValue,
51
+ };
52
+ const rawTimestamp = entry[timestampField];
53
+ if (timestamp) {
54
+ if (rawTimestamp instanceof Date) {
55
+ normalized[timestampField] = rawTimestamp;
56
+ }
57
+ else if (rawTimestamp === undefined) {
58
+ normalized[timestampField] = new Date();
59
+ }
60
+ else if (typeof rawTimestamp === 'string' || typeof rawTimestamp === 'number') {
61
+ normalized[timestampField] = new Date(rawTimestamp);
62
+ }
63
+ }
64
+ else if (rawTimestamp !== undefined) {
65
+ delete normalized[timestampField];
66
+ }
67
+ return normalized;
68
+ };
69
+ schema.pre('save', function reactionsPreSave(next) {
70
+ try {
71
+ const existing = this.get(field) ??
72
+ [];
73
+ if (!existing.length) {
74
+ return next();
75
+ }
76
+ const normalizedEntries = [];
77
+ const seenUsers = new Map();
78
+ let modified = false;
79
+ for (const entry of existing) {
80
+ const normalized = normalizeEntry(entry);
81
+ if (!normalized) {
82
+ modified = true;
83
+ continue;
84
+ }
85
+ if (!allowMultiplePerUser) {
86
+ const userKey = normalized[userField].toString();
87
+ const idx = seenUsers.get(userKey);
88
+ if (idx !== undefined) {
89
+ normalizedEntries[idx] = normalized;
90
+ modified = true;
91
+ }
92
+ else {
93
+ seenUsers.set(userKey, normalizedEntries.length);
94
+ normalizedEntries.push(normalized);
95
+ }
96
+ }
97
+ else {
98
+ normalizedEntries.push(normalized);
99
+ }
100
+ }
101
+ if (normalizedEntries.length !== existing.length || modified) {
102
+ this.set(field, normalizedEntries);
103
+ }
104
+ next();
105
+ }
106
+ catch (err) {
107
+ if (err instanceof Error) {
108
+ next(err);
109
+ return;
110
+ }
111
+ next(new Error('Unknown error while normalizing reactions.'));
112
+ }
113
+ });
114
+ schema.methods.addReaction = function addReaction(user, reactionType, timestampValue) {
115
+ ensureAllowedType(reactionType);
116
+ const userId = toObjectId(user);
117
+ const userKey = userId.toString();
118
+ const reactions = (this.get(field) ?? [])
119
+ .map((entry) => normalizeEntry(entry))
120
+ .filter((entry) => entry !== null);
121
+ let updated = false;
122
+ if (!allowMultiplePerUser) {
123
+ const existingIndex = reactions.findIndex((entry) => entry[userField]?.toString() === userKey);
124
+ const newEntry = {
125
+ [userField]: userId,
126
+ [typeField]: reactionType,
127
+ };
128
+ if (timestamp) {
129
+ newEntry[timestampField] = (timestampValue ?? new Date());
130
+ }
131
+ if (existingIndex !== -1) {
132
+ reactions[existingIndex] = newEntry;
133
+ }
134
+ else {
135
+ reactions.push(newEntry);
136
+ }
137
+ updated = true;
138
+ }
139
+ else {
140
+ const newEntry = {
141
+ [userField]: userId,
142
+ [typeField]: reactionType,
143
+ };
144
+ if (timestamp) {
145
+ newEntry[timestampField] = (timestampValue ?? new Date());
146
+ }
147
+ reactions.push(newEntry);
148
+ updated = true;
149
+ }
150
+ if (updated) {
151
+ this.set(field, reactions);
152
+ }
153
+ return this;
154
+ };
155
+ schema.methods.removeReaction = function removeReaction(user, reactionType) {
156
+ const userKey = toStringId(user);
157
+ const reactions = this.get(field) ?? [];
158
+ const filtered = reactions.filter((entry) => {
159
+ const matchesUser = entry[userField]?.toString() === userKey;
160
+ if (!matchesUser) {
161
+ return true;
162
+ }
163
+ if (!reactionType) {
164
+ return false;
165
+ }
166
+ return entry[typeField] !== reactionType;
167
+ });
168
+ if (filtered.length !== reactions.length) {
169
+ this.set(field, filtered);
170
+ }
171
+ return this;
172
+ };
173
+ schema.methods.hasReaction = function hasReaction(user, reactionType) {
174
+ const userKey = toStringId(user);
175
+ const reactions = this.get(field) ?? [];
176
+ return reactions.some((entry) => {
177
+ if (entry[userField]?.toString() !== userKey) {
178
+ return false;
179
+ }
180
+ if (reactionType) {
181
+ return entry[typeField] === reactionType;
182
+ }
183
+ return true;
184
+ });
185
+ };
186
+ schema.methods.countReactions = function countReactions(reactionType) {
187
+ const reactions = this.get(field) ?? [];
188
+ if (reactionType) {
189
+ ensureAllowedType(reactionType);
190
+ return reactions.filter((entry) => entry[typeField] === reactionType).length;
191
+ }
192
+ return reactions.reduce((accumulator, entry) => {
193
+ const type = entry[typeField];
194
+ accumulator.perType[type] = (accumulator.perType[type] ?? 0) + 1;
195
+ accumulator.total += 1;
196
+ return accumulator;
197
+ }, { total: 0, perType: {} });
198
+ };
199
+ };
200
+ export default reactionsMiddleware;
201
+ //# sourceMappingURL=reactions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactions.js","sourceRoot":"","sources":["../../src/middleware/reactions.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slango/mangusta",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Mongoose middlewares and utilities",
6
6
  "type": "module",
@@ -12,22 +12,22 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "bcrypt": "6.0.0",
15
- "nanoid": "5.1.5"
15
+ "nanoid": "5.1.6"
16
16
  },
17
17
  "peerDependencies": {
18
- "mongoose": "^8.18.1"
18
+ "mongoose": "^8.18.2"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/bcrypt": "6.0.0",
22
22
  "@vitest/coverage-v8": "3.2.4",
23
23
  "@vitest/ui": "3.2.4",
24
24
  "eslint": "9.36.0",
25
- "mongoose": "8.18.1",
25
+ "mongoose": "8.18.2",
26
26
  "typescript": "5.9.2",
27
27
  "vitest": "3.2.4",
28
+ "@slango.configs/eslint": "1.1.12",
28
29
  "@slango.configs/typescript": "1.0.5",
29
- "@slango.configs/vitest": "1.0.30",
30
- "@slango.configs/eslint": "1.1.9"
30
+ "@slango.configs/vitest": "1.0.33"
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsc -p tsconfig.build.json",
package/src/helpers.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { Types } from 'mongoose';
2
+
3
+ export type Id = string | Types.ObjectId;
4
+
5
+ export const toObjectId = (value: Id): Types.ObjectId => {
6
+ if (value instanceof Types.ObjectId) {
7
+ return value;
8
+ }
9
+
10
+ return new Types.ObjectId(value);
11
+ };
12
+
13
+ export const toStringId = (value: Id): string =>
14
+ value instanceof Types.ObjectId ? value.toString() : value;
@@ -0,0 +1,209 @@
1
+ import { setupMongoTestEnvironment } from '@slango.configs/vitest/helpers/mongooseTestEnvironment';
2
+ import mongoose, { Document, model, Schema, Types } from 'mongoose';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import type { PluginFunction } from '../types.js';
6
+
7
+ import reactionsMiddleware, {
8
+ defaultReactionTypes,
9
+ ReactionCountSummary,
10
+ ReactionsMiddlewareOptions,
11
+ WithReactions,
12
+ WithReactionsMethods,
13
+ } from './reactions.js';
14
+
15
+ setupMongoTestEnvironment();
16
+
17
+ type TestDoc<
18
+ Field extends string = 'reactions',
19
+ UserField extends string = 'user',
20
+ TypeField extends string = 'type',
21
+ TimestampField extends string = 'createdAt',
22
+ > = Document &
23
+ WithReactions<Field, UserField, TypeField, TimestampField> &
24
+ WithReactionsMethods & {
25
+ [key: string]: unknown;
26
+ };
27
+
28
+ const createTestModel = <
29
+ Field extends string = 'reactions',
30
+ UserField extends string = 'user',
31
+ TypeField extends string = 'type',
32
+ TimestampField extends string = 'createdAt',
33
+ >(
34
+ options?: ReactionsMiddlewareOptions<Field, UserField, TypeField, TimestampField>,
35
+ ) => {
36
+ const modelName = 'ReactionsTestDoc';
37
+
38
+ if (mongoose.models[modelName]) {
39
+ delete mongoose.models[modelName];
40
+ }
41
+
42
+ const TestSchema = new Schema<TestDoc<Field, UserField, TypeField, TimestampField>>({});
43
+
44
+ if (options) {
45
+ TestSchema.plugin(
46
+ reactionsMiddleware as PluginFunction<
47
+ ReactionsMiddlewareOptions<Field, UserField, TypeField, TimestampField>
48
+ >,
49
+ options,
50
+ );
51
+ } else {
52
+ TestSchema.plugin(reactionsMiddleware);
53
+ }
54
+
55
+ return model<TestDoc<Field, UserField, TypeField, TimestampField>>(modelName, TestSchema);
56
+ };
57
+
58
+ describe('reactionsMiddleware', () => {
59
+ it('should add reactions field and persist entries with defaults', async () => {
60
+ const TestModel = createTestModel();
61
+ const userId = new Types.ObjectId();
62
+
63
+ const doc = new TestModel();
64
+ doc.reactions.push({ user: userId, type: defaultReactionTypes[0] });
65
+
66
+ await expect(doc.save()).resolves.not.toThrow();
67
+
68
+ const saved = await TestModel.findById(doc._id);
69
+ expect(saved).toBeDefined();
70
+ expect(saved!.reactions).toHaveLength(1);
71
+ expect(saved!.reactions[0]?.user?.toString()).toBe(userId.toString());
72
+ expect(saved!.reactions[0]?.type).toBe(defaultReactionTypes[0]);
73
+ expect(saved!.reactions[0]?.createdAt).toBeInstanceOf(Date);
74
+ });
75
+
76
+ it('should deduplicate reactions for the same user when multiple reactions are disabled', async () => {
77
+ const TestModel = createTestModel();
78
+ const userId = new Types.ObjectId();
79
+
80
+ const doc = new TestModel({
81
+ reactions: [
82
+ { user: userId, type: '👍' },
83
+ { user: userId, type: '❤️' },
84
+ ],
85
+ });
86
+
87
+ await expect(doc.save()).resolves.not.toThrow();
88
+
89
+ const saved = await TestModel.findById(doc._id).lean();
90
+ expect(saved).toBeDefined();
91
+ expect(saved!.reactions).toHaveLength(1);
92
+ expect(saved!.reactions[0]?.type).toBe('❤️');
93
+ });
94
+
95
+ it('should allow multiple reactions for the same user when enabled', async () => {
96
+ const TestModel = createTestModel({ allowMultiplePerUser: true });
97
+ const userId = new Types.ObjectId();
98
+
99
+ const doc = new TestModel();
100
+ doc.addReaction(userId, '👍');
101
+ doc.addReaction(userId, '❤️');
102
+
103
+ await expect(doc.save()).resolves.not.toThrow();
104
+
105
+ const saved = await TestModel.findById(doc._id);
106
+ expect(saved?.reactions).toHaveLength(2);
107
+ const types = saved?.reactions.map((reaction) => reaction.type).sort();
108
+ expect(types).toEqual(['❤️', '👍']);
109
+ });
110
+
111
+ it('should expose helper methods for managing reactions', async () => {
112
+ const TestModel = createTestModel();
113
+ const userId = new Types.ObjectId();
114
+
115
+ const doc = new TestModel();
116
+ doc.addReaction(userId, '👍');
117
+ doc.addReaction(userId, '❤️');
118
+
119
+ expect(doc.hasReaction(userId, '❤️')).toBe(true);
120
+ expect(doc.hasReaction(userId, '👍')).toBe(false);
121
+
122
+ const summary = doc.countReactions() as ReactionCountSummary;
123
+ expect(summary.total).toBe(1);
124
+ expect(summary.perType['❤️']).toBe(1);
125
+
126
+ const heartCount = doc.countReactions('❤️');
127
+ expect(heartCount).toBe(1);
128
+
129
+ doc.removeReaction(userId, '❤️');
130
+ expect(doc.hasReaction(userId)).toBe(false);
131
+
132
+ await expect(doc.save()).resolves.not.toThrow();
133
+ const saved = await TestModel.findById(doc._id);
134
+ expect(saved?.reactions).toHaveLength(0);
135
+ });
136
+
137
+ it('should reject unsupported reaction types', async () => {
138
+ const TestModel = createTestModel();
139
+ const userId = new Types.ObjectId();
140
+ const doc = new TestModel();
141
+
142
+ expect(() => doc.addReaction(userId, 'unsupported')).toThrowError(
143
+ /Reaction type "unsupported" is not permitted/,
144
+ );
145
+
146
+ doc.reactions.push({ user: userId, type: 'unsupported' });
147
+ await expect(doc.save()).rejects.toThrowError(
148
+ /`unsupported` is not a valid enum value for path `type`/,
149
+ );
150
+ });
151
+
152
+ it('should allow custom configuration for field and helper schema options', async () => {
153
+ const TestModel = createTestModel<'feedback', 'member', 'category', 'time'>({
154
+ field: 'feedback',
155
+ userField: 'member',
156
+ typeField: 'category',
157
+ timestampField: 'time',
158
+ allowMultiplePerUser: true,
159
+ allowedTypes: ['👏', '😲'],
160
+ indexUser: false,
161
+ indexType: true,
162
+ timestamp: true,
163
+ userRef: 'Account',
164
+ });
165
+
166
+ const doc = new TestModel();
167
+ const memberId = new Types.ObjectId();
168
+
169
+ doc.addReaction(memberId, '👏');
170
+ doc.addReaction(memberId, '😲');
171
+
172
+ expect(doc.hasReaction(memberId, '😲')).toBe(true);
173
+ const stats = doc.countReactions() as ReactionCountSummary;
174
+ expect(stats.total).toBe(2);
175
+ expect(Object.keys(stats.perType).sort()).toEqual(['👏', '😲']);
176
+
177
+ await expect(doc.save()).resolves.not.toThrow();
178
+
179
+ const indexes = TestModel.schema.indexes();
180
+ const categoryIndex = indexes.find(([fields]) =>
181
+ Object.prototype.hasOwnProperty.call(fields, 'feedback.category'),
182
+ );
183
+ const memberIndex = indexes.find(([fields]) =>
184
+ Object.prototype.hasOwnProperty.call(fields, 'feedback.member'),
185
+ );
186
+
187
+ expect(categoryIndex).toBeDefined();
188
+ expect(memberIndex).toBeUndefined();
189
+
190
+ const saved = await TestModel.findById(doc._id).lean();
191
+ expect(saved?.feedback).toHaveLength(2);
192
+ expect(saved?.feedback[0]).toHaveProperty('time');
193
+ expect(saved?.feedback[1]).toHaveProperty('time');
194
+ });
195
+
196
+ it('should support disabling timestamps', async () => {
197
+ const TestModel = createTestModel({ timestamp: false });
198
+ const userId = new Types.ObjectId();
199
+
200
+ const doc = new TestModel();
201
+ doc.addReaction(userId, '👍');
202
+
203
+ await expect(doc.save()).resolves.not.toThrow();
204
+
205
+ const saved = await TestModel.findById(doc._id).lean();
206
+ expect(saved?.reactions).toHaveLength(1);
207
+ expect(saved?.reactions[0]?.createdAt).toBeUndefined();
208
+ });
209
+ });