@slango/mangusta 2.1.40 → 2.1.41

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 (79) hide show
  1. package/dist/helpers.d.ts +5 -0
  2. package/dist/helpers.d.ts.map +1 -0
  3. package/dist/helpers.js +9 -0
  4. package/dist/helpers.js.map +1 -0
  5. package/dist/middleware/compactId.d.ts +13 -0
  6. package/dist/middleware/compactId.d.ts.map +1 -0
  7. package/dist/middleware/compactId.js +16 -0
  8. package/dist/middleware/compactId.js.map +1 -0
  9. package/dist/middleware/email.d.ts +18 -0
  10. package/dist/middleware/email.d.ts.map +1 -0
  11. package/dist/middleware/email.js +17 -0
  12. package/dist/middleware/email.js.map +1 -0
  13. package/dist/middleware/owner.d.ts +14 -0
  14. package/dist/middleware/owner.d.ts.map +1 -0
  15. package/dist/middleware/owner.js +14 -0
  16. package/dist/middleware/owner.js.map +1 -0
  17. package/dist/middleware/password.d.ts +18 -0
  18. package/dist/middleware/password.d.ts.map +1 -0
  19. package/dist/middleware/password.js +29 -0
  20. package/dist/middleware/password.js.map +1 -0
  21. package/dist/middleware/reactions.d.ts +43 -0
  22. package/dist/middleware/reactions.d.ts.map +1 -0
  23. package/dist/middleware/reactions.js +190 -0
  24. package/dist/middleware/reactions.js.map +1 -0
  25. package/dist/middleware/tags.d.ts +13 -0
  26. package/dist/middleware/tags.d.ts.map +1 -0
  27. package/dist/middleware/tags.js +26 -0
  28. package/dist/middleware/tags.js.map +1 -0
  29. package/dist/middleware/timestamps.d.ts +20 -0
  30. package/dist/middleware/timestamps.d.ts.map +1 -0
  31. package/dist/middleware/timestamps.js +42 -0
  32. package/dist/middleware/timestamps.js.map +1 -0
  33. package/dist/test-utils/globalSetup.d.ts +4 -0
  34. package/dist/test-utils/globalSetup.d.ts.map +1 -0
  35. package/dist/test-utils/globalSetup.js +15 -0
  36. package/dist/test-utils/globalSetup.js.map +1 -0
  37. package/dist/test-utils/model.d.ts +13 -0
  38. package/dist/test-utils/model.d.ts.map +1 -0
  39. package/dist/test-utils/model.js +11 -0
  40. package/dist/test-utils/model.js.map +1 -0
  41. package/dist/test-utils/mongooseTestEnvironment.d.ts +6 -0
  42. package/dist/test-utils/mongooseTestEnvironment.d.ts.map +1 -0
  43. package/dist/test-utils/mongooseTestEnvironment.js +40 -0
  44. package/dist/test-utils/mongooseTestEnvironment.js.map +1 -0
  45. package/dist/test-utils/testMongoUri.d.ts +4 -0
  46. package/dist/test-utils/testMongoUri.d.ts.map +1 -0
  47. package/dist/test-utils/testMongoUri.js +14 -0
  48. package/dist/test-utils/testMongoUri.js.map +1 -0
  49. package/{src/types.ts → dist/types.d.ts} +1 -2
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +2 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +7 -4
  54. package/CHANGELOG.md +0 -374
  55. package/eslint.config.js +0 -1
  56. package/lint-staged.config.js +0 -1
  57. package/src/helpers.ts +0 -14
  58. package/src/middleware/compactId.spec.ts +0 -88
  59. package/src/middleware/compactId.ts +0 -42
  60. package/src/middleware/email.spec.ts +0 -86
  61. package/src/middleware/email.ts +0 -49
  62. package/src/middleware/owner.spec.ts +0 -77
  63. package/src/middleware/owner.ts +0 -39
  64. package/src/middleware/password.spec.ts +0 -83
  65. package/src/middleware/password.ts +0 -77
  66. package/src/middleware/reactions.spec.ts +0 -200
  67. package/src/middleware/reactions.ts +0 -371
  68. package/src/middleware/tags.spec.ts +0 -103
  69. package/src/middleware/tags.ts +0 -54
  70. package/src/middleware/timestamps.spec.ts +0 -69
  71. package/src/middleware/timestamps.ts +0 -92
  72. package/src/test-utils/globalSetup.ts +0 -22
  73. package/src/test-utils/model.ts +0 -29
  74. package/src/test-utils/mongooseTestEnvironment.ts +0 -54
  75. package/src/test-utils/testMongoUri.ts +0 -52
  76. package/src/test-utils/vitest.d.ts +0 -7
  77. package/tsconfig.build.json +0 -10
  78. package/tsconfig.json +0 -9
  79. package/vitest.config.js +0 -1
@@ -1,86 +0,0 @@
1
- import mongoose, { Document, model, Schema } from 'mongoose';
2
- import { describe, expect, it } from 'vitest';
3
-
4
- import { setupMongoTestEnvironment } from '../test-utils/mongooseTestEnvironment.js';
5
- import emailMiddleware from './email.js';
6
-
7
- setupMongoTestEnvironment();
8
-
9
- interface TestUser extends Document {
10
- email: string;
11
- }
12
-
13
- const TestUserSchema = new Schema<TestUser>({});
14
- TestUserSchema.plugin(emailMiddleware);
15
-
16
- const TestUserModel = model<TestUser>('TestUser', TestUserSchema);
17
-
18
- const validEmail = 'valid.email@example.com';
19
- const invalidEmail = 'invalid-email';
20
- const customErrorMessage = 'Custom error message for invalid email';
21
-
22
- describe('emailMiddleware', () => {
23
- it('should allow saving a user with a valid email', async () => {
24
- const user = new TestUserModel({ email: validEmail });
25
-
26
- await expect(user.save()).resolves.not.toThrow();
27
- });
28
-
29
- it('should fail when saving a user with an invalid email', async () => {
30
- const user = new TestUserModel({ email: invalidEmail });
31
-
32
- await expect(user.save()).rejects.toThrowError(mongoose.Error.ValidationError);
33
- });
34
-
35
- it('should enforce uniqueness for email', async () => {
36
- await TestUserModel.init();
37
-
38
- const user1 = new TestUserModel({ email: validEmail });
39
- const user2 = new TestUserModel({ email: validEmail });
40
-
41
- await user1.save();
42
- await expect(user2.save()).rejects.toThrowError(Error);
43
- });
44
-
45
- it('should allow saving a user without an email if "required" is set to false', async () => {
46
- const OptionalEmailUserSchema = new Schema<TestUser>({});
47
- OptionalEmailUserSchema.plugin(emailMiddleware, { required: false });
48
- const OptionalEmailUserModel = model<TestUser>('OptionalEmailUser', OptionalEmailUserSchema);
49
-
50
- const user = new OptionalEmailUserModel({});
51
-
52
- await expect(user.save()).resolves.not.toThrow();
53
- });
54
-
55
- it('should display custom error message for invalid email', async () => {
56
- const CustomMessageSchema = new Schema<TestUser>({});
57
- CustomMessageSchema.plugin(emailMiddleware, { doesNotMatchMessage: customErrorMessage });
58
- const CustomMessageUserModel = model<TestUser>('CustomMessageUser', CustomMessageSchema);
59
-
60
- const user = new CustomMessageUserModel({ email: invalidEmail });
61
-
62
- await expect(user.save()).rejects.toThrowError(customErrorMessage);
63
- });
64
-
65
- it('should allow saving a user with a non-unique email if "unique" is set to false', async () => {
66
- const NonUniqueEmailSchema = new Schema<TestUser>({});
67
- NonUniqueEmailSchema.plugin(emailMiddleware, { unique: false });
68
- const NonUniqueEmailUserModel = model<TestUser>('NonUniqueEmailUser', NonUniqueEmailSchema);
69
-
70
- const user1 = new NonUniqueEmailUserModel({ email: validEmail });
71
- const user2 = new NonUniqueEmailUserModel({ email: validEmail });
72
-
73
- await expect(user1.save()).resolves.not.toThrow();
74
- await expect(user2.save()).resolves.not.toThrow(); // Should pass without unique constraint
75
- });
76
-
77
- it('should trim email addresses before saving', async () => {
78
- const user = new TestUserModel({ email: ` ${validEmail} ` });
79
-
80
- await user.save();
81
- const savedUser = await TestUserModel.findOne({ email: validEmail });
82
-
83
- expect(savedUser).toBeDefined();
84
- expect(savedUser?.email).toBe(validEmail);
85
- });
86
- });
@@ -1,49 +0,0 @@
1
- import { Document, Model, Schema } from 'mongoose';
2
-
3
- import { PluginFunction } from '../types.js';
4
-
5
- export const emailRegexp = /^[\w.%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
6
-
7
- export type WithEmail<
8
- Field extends string = 'email',
9
- Required extends boolean = true,
10
- > = Required extends true ? { [key in Field]: string } : { [key in Field]?: string };
11
-
12
- interface EmailMiddlewareOptions<Field extends string = 'email'> {
13
- doesNotMatchMessage?: string;
14
- field?: Field;
15
- index?: boolean;
16
- regExp?: RegExp;
17
- required?: boolean;
18
- unique?: boolean;
19
- }
20
-
21
- const emailMiddleware: PluginFunction<EmailMiddlewareOptions> = <
22
- Field extends string = 'email',
23
- DocType extends Document & WithEmail<Field> = Document & WithEmail<Field>,
24
- >(
25
- schema: Schema<DocType, Model<DocType>>,
26
- {
27
- doesNotMatchMessage,
28
- field = 'email' as Field,
29
- index = true,
30
- regExp = emailRegexp,
31
- required = true,
32
- unique = true,
33
- }: EmailMiddlewareOptions<Field> = {},
34
- ): void => {
35
- const emailSchema = new Schema<WithEmail<Field>>({
36
- [field]: {
37
- index,
38
- match: doesNotMatchMessage ? [regExp, doesNotMatchMessage] : regExp,
39
- required,
40
- trim: true,
41
- type: String,
42
- unique,
43
- },
44
- });
45
-
46
- schema.add(emailSchema);
47
- };
48
-
49
- export default emailMiddleware;
@@ -1,77 +0,0 @@
1
- import mongoose, { Document, Types } from 'mongoose';
2
- import { describe, expect, it } from 'vitest';
3
-
4
- import { createModelWithPlugin } from '../test-utils/model.js';
5
- import { setupMongoTestEnvironment } from '../test-utils/mongooseTestEnvironment.js';
6
- import { PluginFunction } from '../types.js';
7
- import ownerMiddleware, { OwnerMiddlewareOptions, WithOwner } from './owner.js';
8
-
9
- setupMongoTestEnvironment();
10
-
11
- type TestDoc<Field extends string = 'user'> = Document<Types.ObjectId> &
12
- WithOwner<Types.ObjectId, Field>;
13
-
14
- const createTestModel = <Field extends string = 'user'>(options?: OwnerMiddlewareOptions<Field>) =>
15
- createModelWithPlugin<TestDoc<Field>, OwnerMiddlewareOptions<Field>>({
16
- modelName: 'TestDoc',
17
- plugin: ownerMiddleware as PluginFunction<OwnerMiddlewareOptions<Field>>,
18
- pluginOptions: options,
19
- });
20
-
21
- describe('ownerMiddleware', () => {
22
- it('should add a "user" field by default with correct options', async () => {
23
- const { model: TestModel } = createTestModel();
24
- const doc = new TestModel({ user: new Types.ObjectId() });
25
-
26
- await expect(doc.save()).resolves.not.toThrow();
27
- const savedDoc = await TestModel.findOne({ _id: doc._id });
28
- expect(savedDoc).toBeDefined();
29
- expect(savedDoc?.user?.toString()).toBe(doc.user.toString());
30
- });
31
-
32
- it('should not allow saving without user field if required', async () => {
33
- const { model: TestModel } = createTestModel();
34
- const doc = new TestModel({});
35
-
36
- await expect(doc.save()).rejects.toThrowError(mongoose.Error.ValidationError);
37
- });
38
-
39
- it('should allow configuring the field name', async () => {
40
- const { model: TestModel } = createTestModel<'owner'>({ field: 'owner' });
41
- const doc = new TestModel({ owner: new Types.ObjectId() });
42
-
43
- await expect(doc.save()).resolves.not.toThrow();
44
- const savedDoc = await TestModel.findOne({ _id: doc._id });
45
-
46
- expect(savedDoc).toBeDefined();
47
- expect(savedDoc?.owner?.toString()).toBe(doc.owner.toString());
48
- });
49
-
50
- it('should allow optional "user" field if "required" is set to false', async () => {
51
- const { model: TestModel } = createTestModel({ required: false });
52
- const doc = new TestModel({});
53
-
54
- await expect(doc.save()).resolves.not.toThrow();
55
- const savedDoc = await TestModel.findOne({ _id: doc._id });
56
- expect(savedDoc).toBeDefined();
57
- expect(savedDoc?.user).toBeUndefined();
58
- });
59
-
60
- it('should apply "index" option to the field', () => {
61
- const { schema } = createTestModel({ index: true });
62
- const indexes = schema.indexes();
63
- const userIndex = indexes.find(([fields]) =>
64
- Object.prototype.hasOwnProperty.call(fields, 'user'),
65
- );
66
-
67
- expect(userIndex).toBeDefined();
68
- expect(userIndex![0].user).toBe(1);
69
- });
70
-
71
- it('should reference the specified model in "ref"', () => {
72
- const { schema } = createTestModel({ ref: 'CustomUser' });
73
- const refField = schema.path('user');
74
-
75
- expect(refField.options.ref).toBe('CustomUser');
76
- });
77
- });
@@ -1,39 +0,0 @@
1
- import { Document, Schema, Types } from 'mongoose';
2
-
3
- import { PluginFunction } from '../types.js';
4
-
5
- export interface OwnerMiddlewareOptions<Field extends string = 'user'> {
6
- field?: Field;
7
- index?: boolean;
8
- ref?: string;
9
- required?: boolean;
10
- }
11
-
12
- export type WithOwner<T = Types.ObjectId, Field extends string = 'user'> = {
13
- [key in Field]: T;
14
- };
15
-
16
- const ownerMiddleware: PluginFunction<OwnerMiddlewareOptions> = <
17
- Field extends string = 'user',
18
- DocType extends Document & WithOwner<Field> = Document & WithOwner<Field>,
19
- >(
20
- schema: Schema<DocType>,
21
- {
22
- field = 'user' as Field,
23
- index = true,
24
- ref = 'User',
25
- required = true,
26
- }: OwnerMiddlewareOptions<Field> = {},
27
- ): void => {
28
- const fieldDescription = new Schema<WithOwner<Field>>({
29
- [field]: {
30
- index,
31
- ref,
32
- required,
33
- type: Types.ObjectId,
34
- },
35
- });
36
- schema.add(fieldDescription);
37
- };
38
-
39
- export default ownerMiddleware;
@@ -1,83 +0,0 @@
1
- import mongoose, { Document, model, Schema } from 'mongoose';
2
- import { describe, expect, it } from 'vitest';
3
-
4
- import type { PluginFunction } from '../types.js';
5
-
6
- import { setupMongoTestEnvironment } from '../test-utils/mongooseTestEnvironment.js';
7
- import passwordMiddleware, { PasswordMiddlewareOptions, WithPassword } from './password.js';
8
-
9
- setupMongoTestEnvironment();
10
-
11
- type TestDoc<
12
- Field extends string = 'password',
13
- ComparisonFunction extends string = 'comparePassword',
14
- > = Document & WithPassword<Field, ComparisonFunction>;
15
-
16
- const createTestModel = <
17
- Field extends string = 'password',
18
- ComparisonFunction extends string = 'comparePassword',
19
- >(
20
- options?: PasswordMiddlewareOptions<Field, ComparisonFunction>,
21
- ) => {
22
- const modelName = 'TestDoc';
23
-
24
- if (mongoose.models[modelName]) {
25
- delete mongoose.models[modelName];
26
- }
27
-
28
- const TestSchema = new Schema<TestDoc<Field, ComparisonFunction>>({});
29
- if (options) {
30
- TestSchema.plugin(
31
- passwordMiddleware as PluginFunction<PasswordMiddlewareOptions<Field, ComparisonFunction>>,
32
- options,
33
- );
34
- } else {
35
- TestSchema.plugin(passwordMiddleware);
36
- }
37
-
38
- return model<TestDoc<Field, ComparisonFunction>>(modelName, TestSchema);
39
- };
40
-
41
- describe('passwordMiddleware', () => {
42
- it('should hash password and provide comparison method', async () => {
43
- const TestModel = createTestModel();
44
- const plain = 'Valid@123';
45
- const doc = new TestModel({ password: plain });
46
-
47
- await expect(doc.save()).resolves.not.toThrow();
48
- const saved = await TestModel.findById(doc._id);
49
-
50
- expect(saved).toBeDefined();
51
- expect(saved!.password).not.toBe(plain);
52
- await expect(saved!.comparePassword(plain)).resolves.toBe(true);
53
- await expect(saved!.comparePassword('Wrong@123')).resolves.toBe(false);
54
- });
55
-
56
- it('should reject invalid passwords', async () => {
57
- const TestModel = createTestModel();
58
- const doc = new TestModel({ password: 'invalid' });
59
-
60
- await expect(doc.save()).rejects.toThrowError(mongoose.Error.ValidationError);
61
- });
62
-
63
- it('should allow configuring field and comparison function names', async () => {
64
- const TestModel = createTestModel<'pass', 'checkPassword'>({
65
- field: 'pass',
66
- comparisonFunction: 'checkPassword',
67
- });
68
- const doc = new TestModel({ pass: 'Valid@123' });
69
-
70
- await expect(doc.save()).resolves.not.toThrow();
71
- const saved = await TestModel.findById(doc._id);
72
-
73
- expect(saved).toBeDefined();
74
- await expect(saved!.checkPassword('Valid@123')).resolves.toBe(true);
75
- });
76
-
77
- it('should allow optional password field when required is false', async () => {
78
- const TestModel = createTestModel({ required: false });
79
- const doc = new TestModel({});
80
-
81
- await expect(doc.save()).resolves.not.toThrow();
82
- });
83
- });
@@ -1,77 +0,0 @@
1
- import { compare, hash } from 'bcrypt';
2
- import { Document, Model, Schema } from 'mongoose';
3
-
4
- import { PluginFunction } from '../types.js';
5
-
6
- export type WithPassword<
7
- Field extends string = 'password',
8
- ComparisonFunction extends string = 'comparePassword',
9
- > = {
10
- [key in ComparisonFunction]: (password: string) => Promise<boolean>;
11
- } & {
12
- [key in Field]: string;
13
- };
14
-
15
- export const passwordRegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&*)(\][+=.,_-]).{8,}$/;
16
-
17
- export interface PasswordMiddlewareOptions<
18
- Field extends string = 'password',
19
- ComparisonFunction extends string = 'comparePassword',
20
- > {
21
- comparisonFunction?: ComparisonFunction;
22
- doesNotMatchMessage?: string;
23
- field?: Field;
24
- regExp?: RegExp;
25
- required?: boolean;
26
- saltingRounds?: number;
27
- }
28
-
29
- const passwordMiddleware: PluginFunction<PasswordMiddlewareOptions> = <
30
- Field extends string = 'password',
31
- ComparisonFunction extends string = 'comparePassword',
32
- DocType extends Document & WithPassword<Field, ComparisonFunction> = Document &
33
- WithPassword<Field, ComparisonFunction>,
34
- >(
35
- schema: Schema<DocType, Model<DocType>>,
36
- {
37
- comparisonFunction = 'comparePassword' as ComparisonFunction,
38
- doesNotMatchMessage,
39
- field = 'password' as Field,
40
- regExp = passwordRegExp,
41
- required = true,
42
- saltingRounds = 10,
43
- }: PasswordMiddlewareOptions<Field, ComparisonFunction> = {},
44
- ): void => {
45
- const fieldDescription = new Schema<WithPassword<Field, ComparisonFunction>>({
46
- [field]: {
47
- match: doesNotMatchMessage ? [regExp, doesNotMatchMessage] : regExp,
48
- required,
49
- type: String,
50
- },
51
- });
52
-
53
- schema.add(fieldDescription);
54
-
55
- schema.pre<DocType>('save', async function schemaWithPasswordPreSave(this: DocType) {
56
- // only hash the password if it has been modified (or is new)
57
- if (!this.isModified(field)) {
58
- return;
59
- }
60
-
61
- // generate a hash and override the clear text password with the hashed one
62
- this.set(field, await hash(this.get(field) as DocType[Field], saltingRounds));
63
- });
64
-
65
- schema.methods[comparisonFunction] = async function comparePassword(
66
- this: DocType & Document,
67
- candidate: string,
68
- ): Promise<boolean> {
69
- try {
70
- return await compare(candidate, this.get(field) as DocType[Field]);
71
- } catch {
72
- return false;
73
- }
74
- };
75
- };
76
-
77
- export default passwordMiddleware;
@@ -1,200 +0,0 @@
1
- import { Document, Schema, Types } from 'mongoose';
2
- import { describe, expect, it } from 'vitest';
3
-
4
- import type { PluginFunction } from '../types.js';
5
-
6
- import { createModelWithPlugin } from '../test-utils/model.js';
7
- import { setupMongoTestEnvironment } from '../test-utils/mongooseTestEnvironment.js';
8
- import reactionsMiddleware, {
9
- defaultReactionTypes,
10
- ReactionCountSummary,
11
- ReactionsMiddlewareOptions,
12
- WithReactions,
13
- WithReactionsMethods,
14
- } from './reactions.js';
15
-
16
- setupMongoTestEnvironment();
17
-
18
- type TestDoc<
19
- Field extends string = 'reactions',
20
- UserField extends string = 'user',
21
- TypeField extends string = 'type',
22
- TimestampField extends string = 'createdAt',
23
- > = Document &
24
- WithReactions<Field, UserField, TypeField, TimestampField> &
25
- WithReactionsMethods & {
26
- [key: string]: unknown;
27
- };
28
-
29
- const createTestModel = <
30
- Field extends string = 'reactions',
31
- UserField extends string = 'user',
32
- TypeField extends string = 'type',
33
- TimestampField extends string = 'createdAt',
34
- >(
35
- options?: ReactionsMiddlewareOptions<Field, UserField, TypeField, TimestampField>,
36
- ) =>
37
- createModelWithPlugin<
38
- TestDoc<Field, UserField, TypeField, TimestampField>,
39
- ReactionsMiddlewareOptions<Field, UserField, TypeField, TimestampField>
40
- >({
41
- modelName: 'ReactionsTestDoc',
42
- plugin: reactionsMiddleware as PluginFunction<
43
- ReactionsMiddlewareOptions<Field, UserField, TypeField, TimestampField>
44
- >,
45
- pluginOptions: options,
46
- buildSchema: () => new Schema<TestDoc<Field, UserField, TypeField, TimestampField>>({}),
47
- });
48
-
49
- describe('reactionsMiddleware', () => {
50
- it('should add reactions field and persist entries with defaults', async () => {
51
- const { model: TestModel } = createTestModel();
52
- const userId = new Types.ObjectId();
53
-
54
- const doc = new TestModel();
55
- doc.reactions.push({ user: userId, type: defaultReactionTypes[0] });
56
-
57
- await expect(doc.save()).resolves.not.toThrow();
58
-
59
- const saved = await TestModel.findById(doc._id);
60
- expect(saved).toBeDefined();
61
- expect(saved!.reactions).toHaveLength(1);
62
- expect(saved!.reactions[0]?.user?.toString()).toBe(userId.toString());
63
- expect(saved!.reactions[0]?.type).toBe(defaultReactionTypes[0]);
64
- expect(saved!.reactions[0]?.createdAt).toBeInstanceOf(Date);
65
- });
66
-
67
- it('should deduplicate reactions for the same user when multiple reactions are disabled', async () => {
68
- const { model: TestModel } = createTestModel();
69
- const userId = new Types.ObjectId();
70
-
71
- const doc = new TestModel({
72
- reactions: [
73
- { user: userId, type: '👍' },
74
- { user: userId, type: '❤️' },
75
- ],
76
- });
77
-
78
- await expect(doc.save()).resolves.not.toThrow();
79
-
80
- const saved = await TestModel.findById(doc._id).lean();
81
- expect(saved).toBeDefined();
82
- expect(saved!.reactions).toHaveLength(1);
83
- expect(saved!.reactions[0]?.type).toBe('❤️');
84
- });
85
-
86
- it('should allow multiple reactions for the same user when enabled', async () => {
87
- const { model: TestModel } = createTestModel({ allowMultiplePerUser: true });
88
- const userId = new Types.ObjectId();
89
-
90
- const doc = new TestModel();
91
- doc.addReaction(userId, '👍');
92
- doc.addReaction(userId, '❤️');
93
-
94
- await expect(doc.save()).resolves.not.toThrow();
95
-
96
- const saved = await TestModel.findById(doc._id);
97
- expect(saved?.reactions).toHaveLength(2);
98
- const types = saved?.reactions.map((reaction) => reaction.type).sort();
99
- expect(types).toEqual(['❤️', '👍']);
100
- });
101
-
102
- it('should expose helper methods for managing reactions', async () => {
103
- const { model: TestModel } = createTestModel();
104
- const userId = new Types.ObjectId();
105
-
106
- const doc = new TestModel();
107
- doc.addReaction(userId, '👍');
108
- doc.addReaction(userId, '❤️');
109
-
110
- expect(doc.hasReaction(userId, '❤️')).toBe(true);
111
- expect(doc.hasReaction(userId, '👍')).toBe(false);
112
-
113
- const summary = doc.countReactions() as ReactionCountSummary;
114
- expect(summary.total).toBe(1);
115
- expect(summary.perType['❤️']).toBe(1);
116
-
117
- const heartCount = doc.countReactions('❤️');
118
- expect(heartCount).toBe(1);
119
-
120
- doc.removeReaction(userId, '❤️');
121
- expect(doc.hasReaction(userId)).toBe(false);
122
-
123
- await expect(doc.save()).resolves.not.toThrow();
124
- const saved = await TestModel.findById(doc._id);
125
- expect(saved?.reactions).toHaveLength(0);
126
- });
127
-
128
- it('should reject unsupported reaction types', async () => {
129
- const { model: TestModel } = createTestModel();
130
- const userId = new Types.ObjectId();
131
- const doc = new TestModel();
132
-
133
- expect(() => doc.addReaction(userId, 'unsupported')).toThrowError(
134
- /Reaction type "unsupported" is not permitted/,
135
- );
136
-
137
- doc.reactions.push({ user: userId, type: 'unsupported' });
138
- await expect(doc.save()).rejects.toThrowError(
139
- /`unsupported` is not a valid enum value for path `type`/,
140
- );
141
- });
142
-
143
- it('should allow custom configuration for field and helper schema options', async () => {
144
- const { model: TestModel, schema } = createTestModel<'feedback', 'member', 'category', 'time'>({
145
- field: 'feedback',
146
- userField: 'member',
147
- typeField: 'category',
148
- timestampField: 'time',
149
- allowMultiplePerUser: true,
150
- allowedTypes: ['👏', '😲'],
151
- indexUser: false,
152
- indexType: true,
153
- timestamp: true,
154
- userRef: 'Account',
155
- });
156
-
157
- const doc = new TestModel();
158
- const memberId = new Types.ObjectId();
159
-
160
- doc.addReaction(memberId, '👏');
161
- doc.addReaction(memberId, '😲');
162
-
163
- expect(doc.hasReaction(memberId, '😲')).toBe(true);
164
- const stats = doc.countReactions() as ReactionCountSummary;
165
- expect(stats.total).toBe(2);
166
- expect(Object.keys(stats.perType).sort()).toEqual(['👏', '😲']);
167
-
168
- await expect(doc.save()).resolves.not.toThrow();
169
-
170
- const indexes = schema.indexes();
171
- const categoryIndex = indexes.find(([fields]) =>
172
- Object.prototype.hasOwnProperty.call(fields, 'feedback.category'),
173
- );
174
- const memberIndex = indexes.find(([fields]) =>
175
- Object.prototype.hasOwnProperty.call(fields, 'feedback.member'),
176
- );
177
-
178
- expect(categoryIndex).toBeDefined();
179
- expect(memberIndex).toBeUndefined();
180
-
181
- const saved = await TestModel.findById(doc._id).lean();
182
- expect(saved?.feedback).toHaveLength(2);
183
- expect(saved?.feedback[0]).toHaveProperty('time');
184
- expect(saved?.feedback[1]).toHaveProperty('time');
185
- });
186
-
187
- it('should support disabling timestamps', async () => {
188
- const { model: TestModel } = createTestModel({ timestamp: false });
189
- const userId = new Types.ObjectId();
190
-
191
- const doc = new TestModel();
192
- doc.addReaction(userId, '👍');
193
-
194
- await expect(doc.save()).resolves.not.toThrow();
195
-
196
- const saved = await TestModel.findById(doc._id).lean();
197
- expect(saved?.reactions).toHaveLength(1);
198
- expect(saved?.reactions[0]?.createdAt).toBeUndefined();
199
- });
200
- });