@slango/mangusta 2.1.39 → 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.
@@ -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
- });