@slango/mangusta 1.1.8 → 2.0.1

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,9 +1,10 @@
1
1
  import { setupMongoTestEnvironment } from '@slango.configs/vitest/helpers/mongooseTestEnvironment';
2
- import mongoose, { Document, model, Schema, Types } from 'mongoose';
2
+ import { Document, Schema, Types } from 'mongoose';
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
5
  import type { PluginFunction } from '../types.js';
6
6
 
7
+ import { createModelWithPlugin } from '../test-utils/model.js';
7
8
  import reactionsMiddleware, {
8
9
  defaultReactionTypes,
9
10
  ReactionCountSummary,
@@ -32,32 +33,22 @@ const createTestModel = <
32
33
  TimestampField extends string = 'createdAt',
33
34
  >(
34
35
  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
- };
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
+ });
57
48
 
58
49
  describe('reactionsMiddleware', () => {
59
50
  it('should add reactions field and persist entries with defaults', async () => {
60
- const TestModel = createTestModel();
51
+ const { model: TestModel } = createTestModel();
61
52
  const userId = new Types.ObjectId();
62
53
 
63
54
  const doc = new TestModel();
@@ -74,7 +65,7 @@ describe('reactionsMiddleware', () => {
74
65
  });
75
66
 
76
67
  it('should deduplicate reactions for the same user when multiple reactions are disabled', async () => {
77
- const TestModel = createTestModel();
68
+ const { model: TestModel } = createTestModel();
78
69
  const userId = new Types.ObjectId();
79
70
 
80
71
  const doc = new TestModel({
@@ -93,7 +84,7 @@ describe('reactionsMiddleware', () => {
93
84
  });
94
85
 
95
86
  it('should allow multiple reactions for the same user when enabled', async () => {
96
- const TestModel = createTestModel({ allowMultiplePerUser: true });
87
+ const { model: TestModel } = createTestModel({ allowMultiplePerUser: true });
97
88
  const userId = new Types.ObjectId();
98
89
 
99
90
  const doc = new TestModel();
@@ -109,7 +100,7 @@ describe('reactionsMiddleware', () => {
109
100
  });
110
101
 
111
102
  it('should expose helper methods for managing reactions', async () => {
112
- const TestModel = createTestModel();
103
+ const { model: TestModel } = createTestModel();
113
104
  const userId = new Types.ObjectId();
114
105
 
115
106
  const doc = new TestModel();
@@ -135,7 +126,7 @@ describe('reactionsMiddleware', () => {
135
126
  });
136
127
 
137
128
  it('should reject unsupported reaction types', async () => {
138
- const TestModel = createTestModel();
129
+ const { model: TestModel } = createTestModel();
139
130
  const userId = new Types.ObjectId();
140
131
  const doc = new TestModel();
141
132
 
@@ -150,7 +141,7 @@ describe('reactionsMiddleware', () => {
150
141
  });
151
142
 
152
143
  it('should allow custom configuration for field and helper schema options', async () => {
153
- const TestModel = createTestModel<'feedback', 'member', 'category', 'time'>({
144
+ const { model: TestModel, schema } = createTestModel<'feedback', 'member', 'category', 'time'>({
154
145
  field: 'feedback',
155
146
  userField: 'member',
156
147
  typeField: 'category',
@@ -176,7 +167,7 @@ describe('reactionsMiddleware', () => {
176
167
 
177
168
  await expect(doc.save()).resolves.not.toThrow();
178
169
 
179
- const indexes = TestModel.schema.indexes();
170
+ const indexes = schema.indexes();
180
171
  const categoryIndex = indexes.find(([fields]) =>
181
172
  Object.prototype.hasOwnProperty.call(fields, 'feedback.category'),
182
173
  );
@@ -194,7 +185,7 @@ describe('reactionsMiddleware', () => {
194
185
  });
195
186
 
196
187
  it('should support disabling timestamps', async () => {
197
- const TestModel = createTestModel({ timestamp: false });
188
+ const { model: TestModel } = createTestModel({ timestamp: false });
198
189
  const userId = new Types.ObjectId();
199
190
 
200
191
  const doc = new TestModel();
@@ -185,55 +185,43 @@ const reactionsMiddleware: PluginFunction<ReactionsMiddlewareOptions> = <
185
185
  return normalized;
186
186
  };
187
187
 
188
- schema.pre<DocType>('save', function reactionsPreSave(next) {
189
- try {
190
- const existing =
191
- (this.get(field) as ReactionEntry<UserField, TypeField, TimestampField>[] | undefined) ??
192
- [];
193
-
194
- if (!existing.length) {
195
- return next();
188
+ schema.pre<DocType>('save', function reactionsPreSave(this: DocType) {
189
+ const existing =
190
+ (this.get(field) as ReactionEntry<UserField, TypeField, TimestampField>[] | undefined) ?? [];
191
+
192
+ if (!existing.length) {
193
+ return;
194
+ }
195
+
196
+ const normalizedEntries: ReactionEntry<UserField, TypeField, TimestampField>[] = [];
197
+ const seenUsers = new Map<string, number>();
198
+ let modified = false;
199
+
200
+ for (const entry of existing) {
201
+ const normalized = normalizeEntry(entry);
202
+ if (!normalized) {
203
+ modified = true;
204
+ continue;
196
205
  }
197
206
 
198
- const normalizedEntries: ReactionEntry<UserField, TypeField, TimestampField>[] = [];
199
- const seenUsers = new Map<string, number>();
200
- let modified = false;
207
+ if (!allowMultiplePerUser) {
208
+ const userKey = normalized[userField].toString();
209
+ const idx = seenUsers.get(userKey);
201
210
 
202
- for (const entry of existing) {
203
- const normalized = normalizeEntry(entry);
204
- if (!normalized) {
211
+ if (idx !== undefined) {
212
+ normalizedEntries[idx] = normalized;
205
213
  modified = true;
206
- continue;
207
- }
208
-
209
- if (!allowMultiplePerUser) {
210
- const userKey = normalized[userField].toString();
211
- const idx = seenUsers.get(userKey);
212
-
213
- if (idx !== undefined) {
214
- normalizedEntries[idx] = normalized;
215
- modified = true;
216
- } else {
217
- seenUsers.set(userKey, normalizedEntries.length);
218
- normalizedEntries.push(normalized);
219
- }
220
214
  } else {
215
+ seenUsers.set(userKey, normalizedEntries.length);
221
216
  normalizedEntries.push(normalized);
222
217
  }
218
+ } else {
219
+ normalizedEntries.push(normalized);
223
220
  }
221
+ }
224
222
 
225
- if (normalizedEntries.length !== existing.length || modified) {
226
- this.set(field, normalizedEntries);
227
- }
228
-
229
- next();
230
- } catch (err) {
231
- if (err instanceof Error) {
232
- next(err);
233
- return;
234
- }
235
-
236
- next(new Error('Unknown error while normalizing reactions.'));
223
+ if (normalizedEntries.length !== existing.length || modified) {
224
+ this.set(field, normalizedEntries);
237
225
  }
238
226
  });
239
227
 
@@ -1,33 +1,25 @@
1
1
  import { setupMongoTestEnvironment } from '@slango.configs/vitest/helpers/mongooseTestEnvironment';
2
- import mongoose, { Document, model, Schema } from 'mongoose';
2
+ import mongoose, { Document } from 'mongoose';
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
+ import { createModelWithPlugin } from '../test-utils/model.js';
6
+ import { PluginFunction } from '../types.js';
5
7
  import tagsMiddleware, { TagsMiddlewareOptions, WithTags } from './tags.js';
6
8
 
7
9
  setupMongoTestEnvironment();
8
10
 
9
11
  type TestDoc<Field extends string = 'tags'> = Document & WithTags<Field>;
10
12
 
11
- const createTestModel = <Field extends string = 'tags'>(options?: TagsMiddlewareOptions<Field>) => {
12
- const modelName = 'TestDoc';
13
-
14
- if (mongoose.models[modelName]) {
15
- delete mongoose.models[modelName];
16
- }
17
-
18
- const TestSchema = new Schema<TestDoc<Field>>({});
19
- if (options) {
20
- TestSchema.plugin(tagsMiddleware, options as TagsMiddlewareOptions);
21
- } else {
22
- TestSchema.plugin(tagsMiddleware);
23
- }
24
-
25
- return model<TestDoc<Field>>(modelName, TestSchema);
26
- };
13
+ const createTestModel = <Field extends string = 'tags'>(options?: TagsMiddlewareOptions<Field>) =>
14
+ createModelWithPlugin<TestDoc<Field>, TagsMiddlewareOptions<Field>>({
15
+ modelName: 'TestDoc',
16
+ plugin: tagsMiddleware as PluginFunction<TagsMiddlewareOptions<Field>>,
17
+ pluginOptions: options,
18
+ });
27
19
 
28
20
  describe('tagsMiddleware', () => {
29
21
  it('should add a "tags" field with correct content if tags are provided', async () => {
30
- const TestModel = createTestModel();
22
+ const { model: TestModel } = createTestModel();
31
23
  const doc = new TestModel({ tags: ['tag1', 'tag2'] });
32
24
 
33
25
  await expect(doc.save()).resolves.not.toThrow();
@@ -38,7 +30,7 @@ describe('tagsMiddleware', () => {
38
30
  });
39
31
 
40
32
  it('should add a "tags" field with empty array if no tag is provided', async () => {
41
- const TestModel = createTestModel();
33
+ const { model: TestModel } = createTestModel();
42
34
  const doc = new TestModel({});
43
35
 
44
36
  await expect(doc.save()).resolves.not.toThrow();
@@ -49,7 +41,7 @@ describe('tagsMiddleware', () => {
49
41
  });
50
42
 
51
43
  it('should ensure uniqueness of tags if "unique" is set to true', async () => {
52
- const TestModel = createTestModel({ unique: true });
44
+ const { model: TestModel } = createTestModel({ unique: true });
53
45
  const doc = new TestModel({ tags: ['tag1', 'tag2', 'tag1'] });
54
46
 
55
47
  await expect(doc.save()).resolves.not.toThrow();
@@ -60,7 +52,7 @@ describe('tagsMiddleware', () => {
60
52
  });
61
53
 
62
54
  it('should not enforce uniqueness if "unique" is set to false', async () => {
63
- const TestModel = createTestModel({ unique: false });
55
+ const { model: TestModel } = createTestModel({ unique: false });
64
56
  const doc = new TestModel({ tags: ['tag1', 'tag2', 'tag1'] });
65
57
 
66
58
  await expect(doc.save()).resolves.not.toThrow();
@@ -71,7 +63,7 @@ describe('tagsMiddleware', () => {
71
63
  });
72
64
 
73
65
  it('should validate tags with custom validation function', async () => {
74
- const TestModel = createTestModel({
66
+ const { model: TestModel } = createTestModel({
75
67
  validate: (tags) => tags.every((tag) => tag.startsWith('valid')),
76
68
  });
77
69
 
@@ -83,7 +75,7 @@ describe('tagsMiddleware', () => {
83
75
  });
84
76
 
85
77
  it('should allow configuring the field name', async () => {
86
- const TestModel = createTestModel<'customTags'>({ field: 'customTags' });
78
+ const { model: TestModel } = createTestModel<'customTags'>({ field: 'customTags' });
87
79
  const doc = new TestModel({ customTags: ['tag1', 'tag2'] });
88
80
 
89
81
  await expect(doc.save()).resolves.not.toThrow();
@@ -94,16 +86,16 @@ describe('tagsMiddleware', () => {
94
86
  });
95
87
 
96
88
  it('should add a text index when index is set to "text"', () => {
97
- const TestModel = createTestModel({ index: 'text' });
98
- const indexes = TestModel.schema.indexes();
89
+ const { schema } = createTestModel({ index: 'text' });
90
+ const indexes = schema.indexes();
99
91
 
100
92
  const textIndex = indexes.find(([fields]) => fields.tags === 'text');
101
93
  expect(textIndex).toBeDefined();
102
94
  });
103
95
 
104
96
  it('should add a hashed index when index is set to "hashed"', () => {
105
- const TestModel = createTestModel({ index: 'hashed' });
106
- const indexes = TestModel.schema.indexes();
97
+ const { schema } = createTestModel({ index: 'hashed' });
98
+ const indexes = schema.indexes();
107
99
 
108
100
  const textIndex = indexes.find(([fields]) => fields.tags === 'hashed');
109
101
  expect(textIndex).toBeDefined();
@@ -43,13 +43,11 @@ const tagsMiddleware: PluginFunction<TagsMiddlewareOptions> = <
43
43
  schema.index({ [field]: index });
44
44
  }
45
45
 
46
- schema.pre<DocType>('save', function schemaWithTagsPreSave(next) {
46
+ schema.pre<DocType>('save', function schemaWithTagsPreSave(this: DocType) {
47
47
  const tags = this.get(field) as DocType[Field];
48
48
  if (unique && tags) {
49
49
  this.set(field, Array.from(new Set(tags)));
50
50
  }
51
-
52
- next();
53
51
  });
54
52
  };
55
53
 
@@ -1,37 +1,25 @@
1
1
  import { setupMongoTestEnvironment } from '@slango.configs/vitest/helpers/mongooseTestEnvironment';
2
- import mongoose, { Document, model, Schema } from 'mongoose';
2
+ import { Document } from 'mongoose';
3
3
  import { setTimeout as delay } from 'node:timers/promises';
4
4
  import { describe, expect, it } from 'vitest';
5
5
 
6
+ import { createModelWithPlugin } from '../test-utils/model.js';
6
7
  import timestampsMiddleware, { TimestampsMiddlewareOptions, WithTimestamps } from './timestamps.js';
7
8
 
8
9
  setupMongoTestEnvironment();
9
10
 
10
11
  type TestDoc = Document & WithTimestamps & { name: string };
11
12
 
12
- const createTestModel = (options?: TimestampsMiddlewareOptions) => {
13
- const modelName = 'TestDoc';
14
-
15
- if (mongoose.models[modelName]) {
16
- delete mongoose.models[modelName];
17
- }
18
-
19
- const TestSchema = new Schema<TestDoc>({
20
- name: String,
13
+ const createTestModel = (options?: TimestampsMiddlewareOptions) =>
14
+ createModelWithPlugin<TestDoc, TimestampsMiddlewareOptions>({
15
+ modelName: 'TestDoc',
16
+ plugin: timestampsMiddleware,
17
+ pluginOptions: options,
21
18
  });
22
19
 
23
- if (options) {
24
- TestSchema.plugin(timestampsMiddleware, options);
25
- } else {
26
- TestSchema.plugin(timestampsMiddleware);
27
- }
28
-
29
- return model<TestDoc>(modelName, TestSchema);
30
- };
31
-
32
20
  describe('timestampsMiddleware', () => {
33
21
  it('should store creation and update timestamps correctly on save', async () => {
34
- const TestModel = createTestModel();
22
+ const { model: TestModel } = createTestModel();
35
23
  const doc = new TestModel({ name: 'initial' });
36
24
 
37
25
  await expect(doc.save()).resolves.not.toThrow();
@@ -56,7 +44,7 @@ describe('timestampsMiddleware', () => {
56
44
  });
57
45
 
58
46
  it('should update timestamp when using updateOne operations', async () => {
59
- const TestModel = createTestModel();
47
+ const { model: TestModel } = createTestModel();
60
48
  const doc = new TestModel({ name: 'initial' });
61
49
  await doc.save();
62
50
 
@@ -72,8 +60,8 @@ describe('timestampsMiddleware', () => {
72
60
  });
73
61
 
74
62
  it('should add an index on the update field when indexUpdate is true', () => {
75
- const TestModel = createTestModel({ indexUpdate: true });
76
- const indexes = TestModel.schema.indexes();
63
+ const { schema } = createTestModel({ indexUpdate: true });
64
+ const indexes = schema.indexes();
77
65
 
78
66
  const index = indexes.find(([fields]) => fields.updated === 1);
79
67
  expect(index).toBeDefined();
@@ -54,29 +54,21 @@ const timestampsMiddleware: PluginFunction<TimestampsMiddlewareOptions> = <
54
54
  );
55
55
  }
56
56
 
57
- schema.pre('save', function schemaWithTimestampsPreSave(next) {
58
- try {
59
- const now = new Date();
57
+ schema.pre<DocType>('save', function schemaWithTimestampsPreSave(this: DocType) {
58
+ const now = new Date();
60
59
 
61
- if (creation && this.isNew && !this.get(creationField)) {
62
- this.set(creationField, now);
63
- }
60
+ if (creation && this.isNew && !this.get(creationField)) {
61
+ this.set(creationField, now);
62
+ }
64
63
 
65
- if (update) {
66
- if (this.isNew) {
67
- if (updateTimestampOnCreation) {
68
- this.set(updateField, now);
69
- }
70
- // else: do nothing so it's undefined
71
- } else {
64
+ if (update) {
65
+ if (this.isNew) {
66
+ if (updateTimestampOnCreation) {
72
67
  this.set(updateField, now);
73
68
  }
74
- }
75
-
76
- next();
77
- } catch (err) {
78
- if (err instanceof Error) {
79
- next(err);
69
+ // else: do nothing so it's undefined
70
+ } else {
71
+ this.set(updateField, now);
80
72
  }
81
73
  }
82
74
  });
@@ -0,0 +1,29 @@
1
+ import mongoose, { Document, Model, Schema } from 'mongoose';
2
+
3
+ import { PluginFunction } from '../types.js';
4
+
5
+ export interface CreateModelWithPluginParams<Doc extends Document, Options> {
6
+ buildSchema?: () => Schema<Doc>;
7
+ modelName: string;
8
+ plugin: PluginFunction<Options>;
9
+ pluginOptions?: Options;
10
+ }
11
+
12
+ export const createModelWithPlugin = <Doc extends Document, Options>({
13
+ buildSchema,
14
+ modelName,
15
+ plugin,
16
+ pluginOptions,
17
+ }: CreateModelWithPluginParams<Doc, Options>): { model: Model<Doc>; schema: Schema<Doc> } => {
18
+ if (mongoose.models[modelName]) {
19
+ delete mongoose.models[modelName];
20
+ }
21
+
22
+ const schema = buildSchema ? buildSchema() : new Schema<Doc>({});
23
+
24
+ schema.plugin(plugin, pluginOptions);
25
+
26
+ const modelInstance = mongoose.model<Doc>(modelName, schema);
27
+
28
+ return { model: modelInstance, schema };
29
+ };
@@ -6,5 +6,5 @@
6
6
  "outDir": "./dist",
7
7
  "rootDir": "./src"
8
8
  },
9
- "exclude": ["node_modules", "dist", "src/**/*.spec.ts"]
9
+ "exclude": ["node_modules", "dist", "src/**/*.spec.ts", "src/test-utils/**"]
10
10
  }