@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,371 +0,0 @@
1
- import { Document, Model, Schema, Types } from 'mongoose';
2
-
3
- import { Id, toObjectId, toStringId } from '../helpers.js';
4
- import { PluginFunction } from '../types.js';
5
-
6
- export const defaultReactionTypes = ['👍', '❤️', '🤔', '😂', '😮'] as const;
7
-
8
- export type ReactionType = (typeof defaultReactionTypes)[number];
9
-
10
- export interface ReactionCountSummary {
11
- perType: Record<string, number>;
12
- total: number;
13
- }
14
-
15
- export type ReactionUserField<UserField extends string = 'user'> = {
16
- [key in UserField]: Types.ObjectId;
17
- };
18
-
19
- export type ReactionTypeField<TypeField extends string = 'type'> = {
20
- [key in TypeField]: string;
21
- };
22
-
23
- export type ReactionTimestampField<TimestampField extends string = 'createdAt'> = {
24
- [key in TimestampField]?: Date;
25
- };
26
-
27
- export type ReactionEntry<
28
- UserField extends string = 'user',
29
- TypeField extends string = 'type',
30
- TimestampField extends string = 'createdAt',
31
- > = ReactionTimestampField<TimestampField> &
32
- ReactionTypeField<TypeField> &
33
- ReactionUserField<UserField>;
34
-
35
- export type WithReactions<
36
- Field extends string = 'reactions',
37
- UserField extends string = 'user',
38
- TypeField extends string = 'type',
39
- TimestampField extends string = 'createdAt',
40
- > = {
41
- [key in Field]: ReactionEntry<UserField, TypeField, TimestampField>[];
42
- };
43
-
44
- export interface WithReactionsMethods {
45
- addReaction(user: Id, type: string, timestampValue?: Date): this;
46
- countReactions(type?: string): number | ReactionCountSummary;
47
- hasReaction(user: Id, type?: string): boolean;
48
- removeReaction(user: Id, type?: string): this;
49
- }
50
-
51
- export interface ReactionsMiddlewareOptions<
52
- Field extends string = 'reactions',
53
- UserField extends string = 'user',
54
- TypeField extends string = 'type',
55
- TimestampField extends string = 'createdAt',
56
- > {
57
- allowedTypes?: readonly string[];
58
- allowMultiplePerUser?: boolean;
59
- field?: Field;
60
- indexType?: -1 | 1 | boolean;
61
- indexUser?: -1 | 1 | boolean;
62
- timestamp?: boolean;
63
- timestampField?: TimestampField;
64
- typeField?: TypeField;
65
- userField?: UserField;
66
- userRef?: string;
67
- }
68
-
69
- const reactionsMiddleware: PluginFunction<ReactionsMiddlewareOptions> = <
70
- Field extends string = 'reactions',
71
- UserField extends string = 'user',
72
- TypeField extends string = 'type',
73
- TimestampField extends string = 'createdAt',
74
- DocType extends Document &
75
- WithReactions<Field, UserField, TypeField, TimestampField> &
76
- WithReactionsMethods = Document &
77
- WithReactions<Field, UserField, TypeField, TimestampField> &
78
- WithReactionsMethods,
79
- >(
80
- schema: Schema<DocType, Model<DocType>>,
81
- {
82
- allowedTypes = defaultReactionTypes,
83
- allowMultiplePerUser = false,
84
- field = 'reactions' as Field,
85
- indexType = false,
86
- indexUser = true,
87
- timestamp = true,
88
- timestampField = 'createdAt' as TimestampField,
89
- typeField = 'type' as TypeField,
90
- userField = 'user' as UserField,
91
- userRef = 'User',
92
- }: ReactionsMiddlewareOptions<Field, UserField, TypeField, TimestampField> = {},
93
- ): void => {
94
- const allowedTypesSet = new Set(allowedTypes);
95
- const shouldEnforceAllowedTypes = allowedTypesSet.size > 0;
96
-
97
- const reactionSchemaDefinition: Record<string, unknown> = {
98
- [userField]: {
99
- ref: userRef,
100
- required: true,
101
- type: Types.ObjectId,
102
- },
103
- [typeField]: {
104
- required: true,
105
- type: String,
106
- ...(shouldEnforceAllowedTypes && { enum: Array.from(allowedTypesSet) }),
107
- },
108
- ...(timestamp && {
109
- [timestampField]: {
110
- default: Date.now,
111
- type: Date,
112
- },
113
- }),
114
- };
115
-
116
- const reactionsSubSchema = new Schema<ReactionEntry<UserField, TypeField, TimestampField>>(
117
- reactionSchemaDefinition,
118
- {
119
- _id: false,
120
- },
121
- );
122
-
123
- schema.add(
124
- new Schema<WithReactions<Field, UserField, TypeField, TimestampField>>({
125
- [field]: {
126
- default: [],
127
- type: [reactionsSubSchema],
128
- },
129
- }),
130
- );
131
-
132
- if (indexUser) {
133
- schema.index({ [`${field}.${userField}`]: indexUser === true ? 1 : indexUser });
134
- }
135
-
136
- if (indexType) {
137
- schema.index({ [`${field}.${typeField}`]: indexType === true ? 1 : indexType });
138
- }
139
-
140
- const ensureAllowedType = (reactionType: string): void => {
141
- if (shouldEnforceAllowedTypes && !allowedTypesSet.has(reactionType)) {
142
- throw new Error(`Reaction type "${reactionType}" is not permitted.`);
143
- }
144
- };
145
-
146
- const normalizeEntry = (
147
- entry: ReactionEntry<UserField, TypeField, TimestampField>,
148
- ): null | ReactionEntry<UserField, TypeField, TimestampField> => {
149
- const userValue = entry[userField];
150
- const typeValue = entry[typeField];
151
-
152
- if (!userValue || !typeValue) {
153
- return null;
154
- }
155
-
156
- ensureAllowedType(typeValue);
157
-
158
- const normalized: ReactionEntry<UserField, TypeField, TimestampField> = {
159
- ...entry,
160
- [userField]: toObjectId(userValue),
161
- [typeField]: typeValue,
162
- };
163
-
164
- const rawTimestamp = entry[timestampField] as unknown;
165
-
166
- if (timestamp) {
167
- if (rawTimestamp instanceof Date) {
168
- normalized[timestampField] = rawTimestamp as ReactionEntry<
169
- UserField,
170
- TypeField,
171
- TimestampField
172
- >[TimestampField];
173
- } else if (rawTimestamp === undefined) {
174
- normalized[timestampField] = new Date() as ReactionEntry<
175
- UserField,
176
- TypeField,
177
- TimestampField
178
- >[TimestampField];
179
- } else if (typeof rawTimestamp === 'string' || typeof rawTimestamp === 'number') {
180
- normalized[timestampField] = new Date(rawTimestamp) as ReactionEntry<
181
- UserField,
182
- TypeField,
183
- TimestampField
184
- >[TimestampField];
185
- }
186
- } else if (rawTimestamp !== undefined) {
187
- delete (normalized as Record<string, unknown>)[timestampField];
188
- }
189
-
190
- return normalized;
191
- };
192
-
193
- schema.pre<DocType>('save', function reactionsPreSave(this: DocType) {
194
- const existing =
195
- (this.get(field) as ReactionEntry<UserField, TypeField, TimestampField>[] | undefined) ?? [];
196
-
197
- if (!existing.length) {
198
- return;
199
- }
200
-
201
- const normalizedEntries: ReactionEntry<UserField, TypeField, TimestampField>[] = [];
202
- const seenUsers = new Map<string, number>();
203
- let modified = false;
204
-
205
- for (const entry of existing) {
206
- const normalized = normalizeEntry(entry);
207
- if (!normalized) {
208
- modified = true;
209
- continue;
210
- }
211
-
212
- if (!allowMultiplePerUser) {
213
- const userKey = normalized[userField].toString();
214
- const idx = seenUsers.get(userKey);
215
-
216
- if (idx !== undefined) {
217
- normalizedEntries[idx] = normalized;
218
- modified = true;
219
- } else {
220
- seenUsers.set(userKey, normalizedEntries.length);
221
- normalizedEntries.push(normalized);
222
- }
223
- } else {
224
- normalizedEntries.push(normalized);
225
- }
226
- }
227
-
228
- if (normalizedEntries.length !== existing.length || modified) {
229
- this.set(field, normalizedEntries);
230
- }
231
- });
232
-
233
- schema.methods.addReaction = function addReaction(
234
- this: DocType,
235
- user: Id,
236
- reactionType: string,
237
- timestampValue?: Date,
238
- ): DocType {
239
- ensureAllowedType(reactionType);
240
-
241
- const userId = toObjectId(user);
242
- const userKey = userId.toString();
243
- const reactions = (
244
- (this.get(field) as ReactionEntry<UserField, TypeField, TimestampField>[]) ?? []
245
- )
246
- .map((entry) => normalizeEntry(entry))
247
- .filter(
248
- (entry): entry is ReactionEntry<UserField, TypeField, TimestampField> => entry !== null,
249
- );
250
-
251
- if (!allowMultiplePerUser) {
252
- const existingIndex = reactions.findIndex(
253
- (entry) => entry[userField]?.toString() === userKey,
254
- );
255
-
256
- const newEntry: ReactionEntry<UserField, TypeField, TimestampField> = {
257
- [userField]: userId,
258
- [typeField]: reactionType,
259
- } as ReactionEntry<UserField, TypeField, TimestampField>;
260
-
261
- if (timestamp) {
262
- newEntry[timestampField] = (timestampValue ?? new Date()) as ReactionEntry<
263
- UserField,
264
- TypeField,
265
- TimestampField
266
- >[TimestampField];
267
- }
268
-
269
- if (existingIndex !== -1) {
270
- reactions[existingIndex] = newEntry;
271
- } else {
272
- reactions.push(newEntry);
273
- }
274
- } else {
275
- const newEntry: ReactionEntry<UserField, TypeField, TimestampField> = {
276
- [userField]: userId,
277
- [typeField]: reactionType,
278
- } as ReactionEntry<UserField, TypeField, TimestampField>;
279
-
280
- if (timestamp) {
281
- newEntry[timestampField] = (timestampValue ?? new Date()) as ReactionEntry<
282
- UserField,
283
- TypeField,
284
- TimestampField
285
- >[TimestampField];
286
- }
287
-
288
- reactions.push(newEntry);
289
- }
290
-
291
- this.set(field, reactions);
292
-
293
- return this;
294
- };
295
-
296
- schema.methods.removeReaction = function removeReaction(
297
- this: DocType,
298
- user: Id,
299
- reactionType?: string,
300
- ): DocType {
301
- const userKey = toStringId(user);
302
- const reactions =
303
- (this.get(field) as ReactionEntry<UserField, TypeField, TimestampField>[] | undefined) ?? [];
304
-
305
- const filtered = reactions.filter((entry) => {
306
- const matchesUser = entry[userField]?.toString() === userKey;
307
- if (!matchesUser) {
308
- return true;
309
- }
310
-
311
- if (!reactionType) {
312
- return false;
313
- }
314
-
315
- return entry[typeField] !== reactionType;
316
- });
317
-
318
- if (filtered.length !== reactions.length) {
319
- this.set(field, filtered);
320
- }
321
-
322
- return this;
323
- };
324
-
325
- schema.methods.hasReaction = function hasReaction(
326
- this: DocType,
327
- user: Id,
328
- reactionType?: string,
329
- ): boolean {
330
- const userKey = toStringId(user);
331
- const reactions =
332
- (this.get(field) as ReactionEntry<UserField, TypeField, TimestampField>[] | undefined) ?? [];
333
-
334
- return reactions.some((entry) => {
335
- if (entry[userField]?.toString() !== userKey) {
336
- return false;
337
- }
338
-
339
- if (reactionType) {
340
- return entry[typeField] === reactionType;
341
- }
342
-
343
- return true;
344
- });
345
- };
346
-
347
- schema.methods.countReactions = function countReactions(
348
- this: DocType,
349
- reactionType?: string,
350
- ): number | ReactionCountSummary {
351
- const reactions =
352
- (this.get(field) as ReactionEntry<UserField, TypeField, TimestampField>[] | undefined) ?? [];
353
-
354
- if (reactionType) {
355
- ensureAllowedType(reactionType);
356
- return reactions.filter((entry) => entry[typeField] === reactionType).length;
357
- }
358
-
359
- return reactions.reduce<ReactionCountSummary>(
360
- (accumulator, entry) => {
361
- const type = entry[typeField];
362
- accumulator.perType[type] = (accumulator.perType[type] ?? 0) + 1;
363
- accumulator.total += 1;
364
- return accumulator;
365
- },
366
- { total: 0, perType: {} },
367
- );
368
- };
369
- };
370
-
371
- export default reactionsMiddleware;
@@ -1,103 +0,0 @@
1
- import mongoose, { Document } 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 tagsMiddleware, { TagsMiddlewareOptions, WithTags } from './tags.js';
8
-
9
- setupMongoTestEnvironment();
10
-
11
- type TestDoc<Field extends string = 'tags'> = Document & WithTags<Field>;
12
-
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
- });
19
-
20
- describe('tagsMiddleware', () => {
21
- it('should add a "tags" field with correct content if tags are provided', async () => {
22
- const { model: TestModel } = createTestModel();
23
- const doc = new TestModel({ tags: ['tag1', 'tag2'] });
24
-
25
- await expect(doc.save()).resolves.not.toThrow();
26
- const savedDoc = await TestModel.findOne({ _id: doc._id });
27
-
28
- expect(savedDoc).toBeDefined();
29
- expect(savedDoc?.tags).toEqual(['tag1', 'tag2']);
30
- });
31
-
32
- it('should add a "tags" field with empty array if no tag is provided', async () => {
33
- const { model: TestModel } = createTestModel();
34
- const doc = new TestModel({});
35
-
36
- await expect(doc.save()).resolves.not.toThrow();
37
- const savedDoc = await TestModel.findOne({ _id: doc._id });
38
-
39
- expect(savedDoc).toBeDefined();
40
- expect(savedDoc?.tags).toEqual([]);
41
- });
42
-
43
- it('should ensure uniqueness of tags if "unique" is set to true', async () => {
44
- const { model: TestModel } = createTestModel({ unique: true });
45
- const doc = new TestModel({ tags: ['tag1', 'tag2', 'tag1'] });
46
-
47
- await expect(doc.save()).resolves.not.toThrow();
48
- const savedDoc = await TestModel.findOne({ _id: doc._id });
49
-
50
- expect(savedDoc).toBeDefined();
51
- expect(savedDoc?.tags).toEqual(['tag1', 'tag2']); // Duplicates removed
52
- });
53
-
54
- it('should not enforce uniqueness if "unique" is set to false', async () => {
55
- const { model: TestModel } = createTestModel({ unique: false });
56
- const doc = new TestModel({ tags: ['tag1', 'tag2', 'tag1'] });
57
-
58
- await expect(doc.save()).resolves.not.toThrow();
59
- const savedDoc = await TestModel.findOne({ _id: doc._id });
60
-
61
- expect(savedDoc).toBeDefined();
62
- expect(savedDoc?.tags).toEqual(['tag1', 'tag2', 'tag1']);
63
- });
64
-
65
- it('should validate tags with custom validation function', async () => {
66
- const { model: TestModel } = createTestModel({
67
- validate: (tags) => tags.every((tag) => tag.startsWith('valid')),
68
- });
69
-
70
- const validDoc = new TestModel({ tags: ['validTag1', 'validTag2'] });
71
- await expect(validDoc.save()).resolves.not.toThrow();
72
-
73
- const invalidDoc = new TestModel({ tags: ['validTag', 'invalidTag'] });
74
- await expect(invalidDoc.save()).rejects.toThrowError(mongoose.Error.ValidationError);
75
- });
76
-
77
- it('should allow configuring the field name', async () => {
78
- const { model: TestModel } = createTestModel<'customTags'>({ field: 'customTags' });
79
- const doc = new TestModel({ customTags: ['tag1', 'tag2'] });
80
-
81
- await expect(doc.save()).resolves.not.toThrow();
82
- const savedDoc = await TestModel.findOne({ _id: doc._id });
83
-
84
- expect(savedDoc).toBeDefined();
85
- expect(savedDoc?.customTags).toEqual(['tag1', 'tag2']);
86
- });
87
-
88
- it('should add a text index when index is set to "text"', () => {
89
- const { schema } = createTestModel({ index: 'text' });
90
- const indexes = schema.indexes();
91
-
92
- const textIndex = indexes.find(([fields]) => fields.tags === 'text');
93
- expect(textIndex).toBeDefined();
94
- });
95
-
96
- it('should add a hashed index when index is set to "hashed"', () => {
97
- const { schema } = createTestModel({ index: 'hashed' });
98
- const indexes = schema.indexes();
99
-
100
- const textIndex = indexes.find(([fields]) => fields.tags === 'hashed');
101
- expect(textIndex).toBeDefined();
102
- });
103
- });
@@ -1,54 +0,0 @@
1
- import { Document, Model, Schema } from 'mongoose';
2
-
3
- import { PluginFunction } from '../types.js';
4
-
5
- export interface TagsMiddlewareOptions<Field extends string = 'tags'> {
6
- field?: Field;
7
- index?: 'hashed' | 'text' | false;
8
- unique?: boolean;
9
- validate?: (tags: string[]) => boolean;
10
- }
11
-
12
- export type WithTags<Field extends string = 'tags'> = {
13
- [key in Field]: string[];
14
- };
15
-
16
- const tagsMiddleware: PluginFunction<TagsMiddlewareOptions> = <
17
- Field extends string = 'tags',
18
- DocType extends Document & WithTags<Field> = Document & WithTags<Field>,
19
- >(
20
- schema: Schema<DocType, Model<DocType>>,
21
- {
22
- field = 'tags' as Field,
23
- index = false,
24
- unique = true,
25
- validate,
26
- }: TagsMiddlewareOptions<Field> = {},
27
- ): void => {
28
- const tagsSchema = new Schema<WithTags<Field>>({
29
- [field]: {
30
- type: [String],
31
- ...(validate && {
32
- validate: {
33
- message: 'Tags failed custom validation.',
34
- validator: (tags: string[]) => validate(tags),
35
- },
36
- }),
37
- },
38
- });
39
-
40
- schema.add(tagsSchema);
41
-
42
- if (index) {
43
- schema.index({ [field]: index });
44
- }
45
-
46
- schema.pre<DocType>('save', function schemaWithTagsPreSave(this: DocType) {
47
- const tags = this.get(field) as DocType[Field];
48
- if (unique && tags) {
49
- this.set(field, Array.from(new Set(tags)));
50
- }
51
- });
52
- };
53
-
54
- export default tagsMiddleware;
@@ -1,69 +0,0 @@
1
- import { Document } from 'mongoose';
2
- import { setTimeout as delay } from 'node:timers/promises';
3
- import { describe, expect, it } from 'vitest';
4
-
5
- import { createModelWithPlugin } from '../test-utils/model.js';
6
- import { setupMongoTestEnvironment } from '../test-utils/mongooseTestEnvironment.js';
7
- import timestampsMiddleware, { TimestampsMiddlewareOptions, WithTimestamps } from './timestamps.js';
8
-
9
- setupMongoTestEnvironment();
10
-
11
- type TestDoc = Document & WithTimestamps & { name: string };
12
-
13
- const createTestModel = (options?: TimestampsMiddlewareOptions) =>
14
- createModelWithPlugin<TestDoc, TimestampsMiddlewareOptions>({
15
- modelName: 'TestDoc',
16
- plugin: timestampsMiddleware,
17
- pluginOptions: options,
18
- });
19
-
20
- describe('timestampsMiddleware', () => {
21
- it('should store creation and update timestamps correctly on save', async () => {
22
- const { model: TestModel } = createTestModel();
23
- const doc = new TestModel({ name: 'initial' });
24
-
25
- await expect(doc.save()).resolves.not.toThrow();
26
-
27
- const savedDoc = await TestModel.findById(doc._id);
28
- expect(savedDoc).not.toBeNull();
29
- if (!savedDoc) return;
30
- expect(savedDoc.created).toBeInstanceOf(Date);
31
- expect(savedDoc.updated).not.toBeDefined();
32
-
33
- await delay(10);
34
- savedDoc.name = 'changed';
35
- await expect(savedDoc.save()).resolves.not.toThrow();
36
-
37
- const updatedDoc = await TestModel.findById(doc._id);
38
- expect(updatedDoc).not.toBeNull();
39
- if (!updatedDoc) return;
40
- expect(updatedDoc.created).toEqual(savedDoc.created);
41
- expect(updatedDoc.updated).toBeInstanceOf(Date);
42
- if (!updatedDoc.created || !updatedDoc.updated) return;
43
- expect(updatedDoc.updated.getTime()).toBeGreaterThan(updatedDoc.created.getTime());
44
- });
45
-
46
- it('should update timestamp when using updateOne operations', async () => {
47
- const { model: TestModel } = createTestModel();
48
- const doc = new TestModel({ name: 'initial' });
49
- await doc.save();
50
-
51
- await delay(10);
52
- await TestModel.updateOne({ _id: doc._id }, { name: 'updated' });
53
-
54
- const updatedDoc = await TestModel.findById(doc._id);
55
- expect(updatedDoc).not.toBeNull();
56
- if (!updatedDoc) return;
57
- expect(updatedDoc.updated).not.toBeDefined();
58
- if (!updatedDoc.created || !updatedDoc.updated) return;
59
- expect(updatedDoc.updated.getTime()).toBeGreaterThan(updatedDoc.created.getTime());
60
- });
61
-
62
- it('should add an index on the update field when indexUpdate is true', () => {
63
- const { schema } = createTestModel({ indexUpdate: true });
64
- const indexes = schema.indexes();
65
-
66
- const index = indexes.find(([fields]) => fields.updated === 1);
67
- expect(index).toBeDefined();
68
- });
69
- });
@@ -1,92 +0,0 @@
1
- import { Document, Model, Schema } from 'mongoose';
2
-
3
- import { PluginFunction } from '../types.js';
4
-
5
- export interface TimestampsMiddlewareOptions {
6
- creation?: boolean;
7
- creationField?: string;
8
- indexCreation?: boolean;
9
- indexUpdate?: boolean;
10
- update?: boolean;
11
- updateField?: string;
12
- updateTimestampOnCreation?: boolean;
13
- }
14
-
15
- export type WithCreated<Field extends string = 'created'> = { [key in Field]: Date };
16
-
17
- export type WithTimestamps<
18
- Created extends string = 'created',
19
- Updated extends string = 'updated',
20
- > = WithCreated<Created> & WithUpdated<Updated>;
21
-
22
- export type WithUpdated<Field extends string = 'updated'> = { [key in Field]?: Date };
23
-
24
- const timestampsMiddleware: PluginFunction<TimestampsMiddlewareOptions> = <
25
- Created extends string = 'created',
26
- Updated extends string = 'updated',
27
- DocType extends Document & WithTimestamps<Created, Updated> = Document &
28
- WithTimestamps<Created, Updated>,
29
- >(
30
- schema: Schema<DocType, Model<DocType>>,
31
- {
32
- creation = true,
33
- creationField = 'created',
34
- indexCreation = false,
35
- indexUpdate = false,
36
- update = true,
37
- updateField = 'updated',
38
- updateTimestampOnCreation = false,
39
- }: TimestampsMiddlewareOptions = {},
40
- ): void => {
41
- if (creation) {
42
- schema.add(
43
- new Schema<WithCreated<Created>>({
44
- [creationField]: Date,
45
- }),
46
- );
47
- }
48
-
49
- if (update) {
50
- schema.add(
51
- new Schema<WithUpdated<Updated>>({
52
- [updateField]: Date,
53
- }),
54
- );
55
- }
56
-
57
- schema.pre<DocType>('save', function schemaWithTimestampsPreSave(this: DocType) {
58
- const now = new Date();
59
-
60
- if (creation && this.isNew && !this.get(creationField)) {
61
- this.set(creationField, now);
62
- }
63
-
64
- if (update) {
65
- if (this.isNew) {
66
- if (updateTimestampOnCreation) {
67
- this.set(updateField, now);
68
- }
69
- // else: do nothing so it's undefined
70
- } else {
71
- this.set(updateField, now);
72
- }
73
- }
74
- });
75
-
76
- if (update) {
77
- schema.pre(['updateOne', 'findOneAndUpdate'], function schemaWithTimestampsPreUpdate() {
78
- // ensure $set exists and we don't stomp on other operators
79
- this.set({ $set: { [updateField]: new Date() } });
80
- });
81
- }
82
-
83
- if (creation && indexCreation !== false) {
84
- schema.path(creationField).index(indexCreation);
85
- }
86
-
87
- if (update && indexUpdate !== false) {
88
- schema.path(updateField).index(indexUpdate);
89
- }
90
- };
91
-
92
- export default timestampsMiddleware;