@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +16 -15
- package/CHANGELOG.md +13 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +9 -0
- package/dist/helpers.js.map +1 -0
- package/dist/middleware/reactions.d.ts +43 -0
- package/dist/middleware/reactions.d.ts.map +1 -0
- package/dist/middleware/reactions.js +201 -0
- package/dist/middleware/reactions.js.map +1 -0
- package/package.json +6 -6
- package/src/helpers.ts +14 -0
- package/src/middleware/reactions.spec.ts +209 -0
- package/src/middleware/reactions.ts +385 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
package/.turbo/turbo-lint.log
CHANGED
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
|
|
2
|
-
> @slango/mangusta@1.0
|
|
2
|
+
> @slango/mangusta@1.1.0 test /home/runner/work/slango/slango/packages/mangusta
|
|
3
3
|
> vitest --run
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
[1m[46m RUN [49m[22m [36mv3.2.4 [39m[90m/home/runner/work/slango/slango/packages/mangusta[39m
|
|
7
7
|
|
|
8
|
-
[90mstdout[2m | src/middleware/
|
|
8
|
+
[90mstdout[2m | src/middleware/compactId.spec.ts
|
|
9
9
|
[22m[39mDownloading MongoDB "7.0.14": 0% (0mb / 80.8mb)
|
|
10
10
|
|
|
11
|
-
[90mstdout[2m | src/middleware/
|
|
11
|
+
[90mstdout[2m | src/middleware/compactId.spec.ts
|
|
12
12
|
[22m[39mDownloading MongoDB "7.0.14": 100% (80.8mb / 80.8mb)
|
|
13
13
|
|
|
14
|
-
[90mstdout[2m | src/middleware/
|
|
14
|
+
[90mstdout[2m | src/middleware/compactId.spec.ts
|
|
15
15
|
[22m[39mDownloading MongoDB "7.0.14": 100% (0mb / 0mb)
|
|
16
16
|
|
|
17
|
-
[32m✓[39m src/middleware/
|
|
18
|
-
[32m✓[39m src/middleware/
|
|
19
|
-
[32m✓[39m src/middleware/
|
|
20
|
-
[32m✓[39m src/middleware/
|
|
21
|
-
[32m✓[39m src/middleware/tags.spec.ts [2m([22m[2m8 tests[22m[2m)[22m[33m
|
|
22
|
-
[32m✓[39m src/middleware/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
[2m
|
|
26
|
-
[2m
|
|
27
|
-
[2m
|
|
17
|
+
[32m✓[39m src/middleware/compactId.spec.ts [2m([22m[2m6 tests[22m[2m)[22m[33m 4046[2mms[22m[39m
|
|
18
|
+
[32m✓[39m src/middleware/email.spec.ts [2m([22m[2m7 tests[22m[2m)[22m[33m 392[2mms[22m[39m
|
|
19
|
+
[32m✓[39m src/middleware/owner.spec.ts [2m([22m[2m6 tests[22m[2m)[22m[33m 374[2mms[22m[39m
|
|
20
|
+
[32m✓[39m src/middleware/reactions.spec.ts [2m([22m[2m7 tests[22m[2m)[22m[33m 6514[2mms[22m[39m
|
|
21
|
+
[32m✓[39m src/middleware/tags.spec.ts [2m([22m[2m8 tests[22m[2m)[22m[33m 6698[2mms[22m[39m
|
|
22
|
+
[32m✓[39m src/middleware/timestamps.spec.ts [2m([22m[2m3 tests[22m[2m)[22m[33m 673[2mms[22m[39m
|
|
23
|
+
[32m✓[39m src/middleware/password.spec.ts [2m([22m[2m4 tests[22m[2m)[22m[33m 654[2mms[22m[39m
|
|
24
|
+
|
|
25
|
+
[2m Test Files [22m [1m[32m7 passed[39m[22m[90m (7)[39m
|
|
26
|
+
[2m Tests [22m [1m[32m41 passed[39m[22m[90m (41)[39m
|
|
27
|
+
[2m Start at [22m 09:54:04
|
|
28
|
+
[2m Duration [22m 9.25s[2m (transform 294ms, setup 0ms, collect 3.82s, tests 19.35s, environment 2ms, prepare 840ms)[22m
|
|
28
29
|
|
package/CHANGELOG.md
CHANGED
|
@@ -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"}
|
package/dist/helpers.js
ADDED
|
@@ -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
|
|
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.
|
|
15
|
+
"nanoid": "5.1.6"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"mongoose": "^8.18.
|
|
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.
|
|
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
|
-
"@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
|
+
});
|