@feardread/fear 1.2.1 → 2.0.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.
@@ -1,11 +1,502 @@
1
- const mongoose = require("mongoose"); // Erase if already required
1
+ const mongoose = require("mongoose");
2
+ const slugify = require("slugify");
2
3
 
3
- // Declare the Schema of the Mongo model
4
- const categorySchema = new mongoose.Schema({
5
- title: { type: String, required: true, unique: true, index: true },
4
+ const categorySchema = new mongoose.Schema(
5
+ {
6
+ title: { type: String, trim: true, index: true,
7
+ required: [true, "Category title is required"],
8
+ maxlength: [100, "Category title cannot exceed 100 characters"],
9
+ },
10
+ slug: { type: String, unique: true, lowercase: true, index: true },
11
+ description: { type: String, trim: true,
12
+ maxlength: [1000, "Description cannot exceed 1000 characters"]
13
+ },
14
+ icon: { type: String, default: "fa-tag", trim: true },
15
+ color: { type: String, default: "#7934f3", trim: true,
16
+ match: [/^#[0-9A-F]{6}$/i, "Color must be a valid hex color"]
17
+ },
18
+ image: { public_id: { type: String, default: "" },
19
+ url: { type: String, default: "" },
20
+ secure_url: { type: String, default: "" },
21
+ },
22
+ parent: { type: mongoose.Schema.Types.ObjectId, ref: "Category", default: null, index: true },
23
+ ancestors: [{ type: mongoose.Schema.Types.ObjectId, ref: "Category" }],
24
+ children: [{ type: mongoose.Schema.Types.ObjectId, ref: "Category" }],
25
+ level: { type: Number, default: 0, min: 0, index: true },
26
+ isActive: { type: Boolean, default: true, index: true },
27
+ isFeatured: { type: Boolean, default: false, index: true },
28
+ status: { type: String, default: "active", index: true,
29
+ enum: ["active", "inactive", "archived", "draft"],
30
+ },
31
+ type: {
32
+ type: String, default: "general", index: true,
33
+ enum: ["product", "blog", "post", "page", "general", "custom"],
34
+ },
35
+ tags: [{ type: String, trim: true, lowercase: true }],
36
+ products: [{ type: mongoose.Schema.Types.ObjectId, ref: "Product" }],
37
+ posts: [{ type: mongoose.Schema.Types.ObjectId, ref: "Blog" }],
38
+ metadata: { type: Map, of: mongoose.Schema.Types.Mixed },
39
+ createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
40
+ updatedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
41
+ lastModifiedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
42
+ notes: [{
43
+ content: { type: String },
44
+ createdBy: {
45
+ type: mongoose.Schema.Types.ObjectId,
46
+ ref: "User"
47
+ },
48
+ createdAt: { type: Date, default: Date.now }
49
+ }],
6
50
  },
7
- { timestamps: true }
51
+ {
52
+ timestamps: true,
53
+ toJSON: { virtuals: true },
54
+ toObject: { virtuals: true }
55
+ }
8
56
  );
9
57
 
10
- //Export the model
11
- module.exports = mongoose.model("Category", categorySchema);
58
+ // Indexes for better query performance
59
+ categorySchema.index({ name: 1, type: 1 });
60
+ categorySchema.index({ slug: 1, type: 1 });
61
+ categorySchema.index({ parent: 1, isActive: 1 });
62
+ categorySchema.index({ featured: 1, isActive: 1 });
63
+ categorySchema.index({ type: 1, isActive: 1 });
64
+ categorySchema.index({ level: 1, displayOrder: 1 });
65
+ categorySchema.index({ "stats.itemCount": -1 });
66
+ categorySchema.index({ "stats.productCount": -1 });
67
+ categorySchema.index({ createdAt: -1 });
68
+ categorySchema.index({ path: 1 });
69
+
70
+ // Compound indexes
71
+ categorySchema.index({ type: 1, parent: 1, isActive: 1 });
72
+ categorySchema.index({ featured: 1, type: 1, displayOrder: 1 });
73
+
74
+ // Text index for search
75
+ categorySchema.index({
76
+ name: "text",
77
+ title: "text",
78
+ description: "text",
79
+ tags: "text"
80
+ });
81
+
82
+ // Virtuals
83
+ categorySchema.virtual("url").get(function() {
84
+ return `/category/${this.slug}`;
85
+ });
86
+
87
+ categorySchema.virtual("fullPath").get(function() {
88
+ return this.path || this.slug;
89
+ });
90
+
91
+ categorySchema.virtual("isParent").get(function() {
92
+ return this.children && this.children.length > 0;
93
+ });
94
+
95
+ categorySchema.virtual("hasParent").get(function() {
96
+ return this.parent !== null && this.parent !== undefined;
97
+ });
98
+
99
+ categorySchema.virtual("childCount").get(function() {
100
+ return this.children ? this.children.length : 0;
101
+ });
102
+
103
+ // Virtual populate for counting items
104
+ categorySchema.virtual("itemCount", {
105
+ ref: "Product",
106
+ localField: "_id",
107
+ foreignField: "category",
108
+ count: true
109
+ });
110
+
111
+ categorySchema.virtual("productCount", {
112
+ ref: "Product",
113
+ localField: "_id",
114
+ foreignField: "category",
115
+ count: true
116
+ });
117
+
118
+ categorySchema.virtual("postCount", {
119
+ ref: "Blog",
120
+ localField: "_id",
121
+ foreignField: "category",
122
+ count: true
123
+ });
124
+
125
+ // Pre-save middleware
126
+ categorySchema.pre("save", async function(next) {
127
+ // Generate slug
128
+ if (this.isModified("name") || this.isModified("title")) {
129
+ const nameToSlugify = this.title || this.name;
130
+ this.slug = slugify(nameToSlugify, {
131
+ lower: true,
132
+ strict: true,
133
+ remove: /[*+~.()'"!:@]/g
134
+ });
135
+
136
+ // Ensure unique slug
137
+ const slugRegEx = new RegExp(`^${this.slug}(-[0-9]*)?$`, "i");
138
+ const categoriesWithSlug = await this.constructor.find({
139
+ slug: slugRegEx,
140
+ _id: { $ne: this._id }
141
+ });
142
+
143
+ if (categoriesWithSlug.length > 0) {
144
+ this.slug = `${this.slug}-${categoriesWithSlug.length}`;
145
+ }
146
+ }
147
+
148
+ // Sync name and title if one is missing
149
+ if (!this.name && this.title) {
150
+ this.name = this.title;
151
+ }
152
+ if (!this.title && this.name) {
153
+ this.title = this.name;
154
+ }
155
+
156
+ // Sync isActive with active
157
+ if (this.isModified("isActive")) {
158
+ this.active = this.isActive;
159
+ }
160
+ if (this.isModified("active")) {
161
+ this.isActive = this.active;
162
+ }
163
+
164
+ // Calculate level and build ancestors
165
+ if (this.isModified("parent")) {
166
+ if (this.parent) {
167
+ const parent = await this.constructor.findById(this.parent);
168
+ if (parent) {
169
+ this.level = parent.level + 1;
170
+ this.ancestors = [...parent.ancestors, parent._id];
171
+
172
+ // Build path
173
+ const parentPath = parent.path || parent.slug;
174
+ this.path = `${parentPath}/${this.slug}`;
175
+
176
+ // Add to parent's children
177
+ if (!parent.children.includes(this._id)) {
178
+ parent.children.push(this._id);
179
+ await parent.save();
180
+ }
181
+ }
182
+ } else {
183
+ this.level = 0;
184
+ this.ancestors = [];
185
+ this.path = this.slug;
186
+ }
187
+ }
188
+
189
+ // Set SEO defaults
190
+ if (!this.seo.metaTitle) {
191
+ this.seo.metaTitle = (this.title || this.name).substring(0, 70);
192
+ }
193
+
194
+ if (!this.seo.metaDescription && this.description) {
195
+ this.seo.metaDescription = this.description.substring(0, 160);
196
+ }
197
+
198
+ next();
199
+ });
200
+
201
+ // Pre-update middleware
202
+ categorySchema.pre("findOneAndUpdate", async function(next) {
203
+ const update = this.getUpdate();
204
+
205
+ if (update.name || update.title) {
206
+ const nameToSlugify = update.title || update.name;
207
+ update.slug = slugify(nameToSlugify, {
208
+ lower: true,
209
+ strict: true,
210
+ remove: /[*+~.()'"!:@]/g
211
+ });
212
+ }
213
+
214
+ if (update.isActive !== undefined) {
215
+ update.active = update.isActive;
216
+ }
217
+
218
+ if (update.active !== undefined) {
219
+ update.isActive = update.active;
220
+ }
221
+
222
+ next();
223
+ });
224
+
225
+ // Post-remove middleware to clean up references
226
+ categorySchema.post("remove", async function(doc) {
227
+ // Remove from parent's children
228
+ if (doc.parent) {
229
+ await this.constructor.updateOne(
230
+ { _id: doc.parent },
231
+ { $pull: { children: doc._id } }
232
+ );
233
+ }
234
+
235
+ // Update children to remove parent reference
236
+ if (doc.children && doc.children.length > 0) {
237
+ await this.constructor.updateMany(
238
+ { _id: { $in: doc.children } },
239
+ { $set: { parent: null, level: 0 } }
240
+ );
241
+ }
242
+ });
243
+
244
+ // Instance methods
245
+ categorySchema.methods = {
246
+ // Get full category path
247
+ getFullPath: async function() {
248
+ if (this.ancestors.length === 0) {
249
+ return [this];
250
+ }
251
+
252
+ const ancestors = await this.constructor
253
+ .find({ _id: { $in: this.ancestors } })
254
+ .sort({ level: 1 });
255
+
256
+ return [...ancestors, this];
257
+ },
258
+
259
+ // Get all descendants
260
+ getDescendants: async function() {
261
+ return await this.constructor.find({
262
+ ancestors: this._id,
263
+ isDeleted: false
264
+ }).sort({ level: 1, displayOrder: 1 });
265
+ },
266
+
267
+ // Get immediate children
268
+ getChildren: async function() {
269
+ return await this.constructor
270
+ .find({
271
+ parent: this._id,
272
+ isDeleted: false
273
+ })
274
+ .sort({ displayOrder: 1 });
275
+ },
276
+
277
+ // Get siblings
278
+ getSiblings: async function() {
279
+ return await this.constructor
280
+ .find({
281
+ parent: this.parent,
282
+ _id: { $ne: this._id },
283
+ isDeleted: false
284
+ })
285
+ .sort({ displayOrder: 1 });
286
+ },
287
+
288
+ // Update item count
289
+ updateItemCount: async function() {
290
+ const Product = mongoose.model("Product");
291
+ const Blog = mongoose.model("Blog");
292
+
293
+ const productCount = await Product.countDocuments({
294
+ category: this._id,
295
+ isActive: true
296
+ });
297
+
298
+ const postCount = await Blog.countDocuments({
299
+ category: this._id,
300
+ published: true
301
+ });
302
+
303
+ this.stats.productCount = productCount;
304
+ this.stats.postCount = postCount;
305
+ this.stats.itemCount = productCount + postCount;
306
+
307
+ return this.save();
308
+ },
309
+
310
+ // Increment view count
311
+ incrementViews: function() {
312
+ this.stats.viewCount += 1;
313
+ return this.save();
314
+ },
315
+
316
+ // Increment click count
317
+ incrementClicks: function() {
318
+ this.stats.clickCount += 1;
319
+ return this.save();
320
+ },
321
+
322
+ // Move to different parent
323
+ moveTo: async function(newParentId) {
324
+ // Remove from old parent's children
325
+ if (this.parent) {
326
+ await this.constructor.updateOne(
327
+ { _id: this.parent },
328
+ { $pull: { children: this._id } }
329
+ );
330
+ }
331
+
332
+ // Update new parent
333
+ if (newParentId) {
334
+ const newParent = await this.constructor.findById(newParentId);
335
+ if (newParent) {
336
+ this.parent = newParentId;
337
+ this.level = newParent.level + 1;
338
+ this.ancestors = [...newParent.ancestors, newParent._id];
339
+
340
+ // Add to new parent's children
341
+ if (!newParent.children.includes(this._id)) {
342
+ newParent.children.push(this._id);
343
+ await newParent.save();
344
+ }
345
+ }
346
+ } else {
347
+ this.parent = null;
348
+ this.level = 0;
349
+ this.ancestors = [];
350
+ }
351
+
352
+ return this.save();
353
+ },
354
+ };
355
+
356
+ // Static methods
357
+ categorySchema.statics = {
358
+ // Get root categories (top level)
359
+ getRootCategories: function(type = null) {
360
+ const query = {
361
+ parent: null,
362
+ isActive: true,
363
+ isDeleted: false
364
+ };
365
+
366
+ if (type) {
367
+ query.type = type;
368
+ }
369
+
370
+ return this.find(query)
371
+ .sort({ displayOrder: 1, name: 1 })
372
+ .exec();
373
+ },
374
+
375
+ // Get category tree
376
+ getCategoryTree: async function(type = null) {
377
+ const query = {
378
+ isActive: true,
379
+ isDeleted: false
380
+ };
381
+
382
+ if (type) {
383
+ query.type = type;
384
+ }
385
+
386
+ const categories = await this.find(query)
387
+ .sort({ level: 1, displayOrder: 1 })
388
+ .exec();
389
+
390
+ // Build tree structure
391
+ const categoryMap = new Map();
392
+ const tree = [];
393
+
394
+ categories.forEach(cat => {
395
+ categoryMap.set(cat._id.toString(), { ...cat.toObject(), children: [] });
396
+ });
397
+
398
+ categories.forEach(cat => {
399
+ const node = categoryMap.get(cat._id.toString());
400
+ if (cat.parent) {
401
+ const parent = categoryMap.get(cat.parent.toString());
402
+ if (parent) {
403
+ parent.children.push(node);
404
+ }
405
+ } else {
406
+ tree.push(node);
407
+ }
408
+ });
409
+
410
+ return tree;
411
+ },
412
+
413
+ // Get featured categories
414
+ getFeatured: function(type = null, limit = 10) {
415
+ const query = {
416
+ featured: true,
417
+ isActive: true,
418
+ isDeleted: false
419
+ };
420
+
421
+ if (type) {
422
+ query.type = type;
423
+ }
424
+
425
+ return this.find(query)
426
+ .sort({ priority: -1, displayOrder: 1 })
427
+ .limit(limit)
428
+ .exec();
429
+ },
430
+
431
+ // Get popular categories
432
+ getPopular: function(type = null, limit = 10) {
433
+ const query = {
434
+ isActive: true,
435
+ isDeleted: false
436
+ };
437
+
438
+ if (type) {
439
+ query.type = type;
440
+ }
441
+
442
+ return this.find(query)
443
+ .sort({ "stats.itemCount": -1, "stats.viewCount": -1 })
444
+ .limit(limit)
445
+ .exec();
446
+ },
447
+
448
+ // Search categories
449
+ searchCategories: function(query, options = {}) {
450
+ const searchQuery = {
451
+ $text: { $search: query },
452
+ isActive: true,
453
+ isDeleted: false
454
+ };
455
+
456
+ if (options.type) {
457
+ searchQuery.type = options.type;
458
+ }
459
+
460
+ return this.find(searchQuery, { score: { $meta: "textScore" } })
461
+ .sort({ score: { $meta: "textScore" } })
462
+ .limit(options.limit || 20)
463
+ .exec();
464
+ },
465
+
466
+ // Get by type
467
+ getByType: function(type, options = {}) {
468
+ return this.find({
469
+ type,
470
+ isActive: true,
471
+ isDeleted: false
472
+ })
473
+ .sort(options.sort || { displayOrder: 1 })
474
+ .limit(options.limit || 0)
475
+ .exec();
476
+ }
477
+ };
478
+
479
+ // Query helpers
480
+ categorySchema.query = {
481
+ active: function() {
482
+ return this.where({ isActive: true, isDeleted: false });
483
+ },
484
+
485
+ featured: function() {
486
+ return this.where({ featured: true, isActive: true, isDeleted: false });
487
+ },
488
+
489
+ byType: function(type) {
490
+ return this.where({ type, isActive: true, isDeleted: false });
491
+ },
492
+
493
+ topLevel: function() {
494
+ return this.where({ parent: null, isActive: true, isDeleted: false });
495
+ },
496
+
497
+ hasChildren: function() {
498
+ return this.where({ "children.0": { $exists: true } });
499
+ }
500
+ };
501
+
502
+ module.exports = mongoose.model("Category", categorySchema);
package/models/events.js CHANGED
@@ -2,6 +2,7 @@ const mongoose = require("mongoose");
2
2
 
3
3
  const eventSchema = new mongoose.Schema({
4
4
  title: { type: String, required: true, unique: true, index: true },
5
+ description: { type: String, require: false },
5
6
  start: { type: Date, required: true, default: mongoose.now() },
6
7
  end: { type: Date, required: false, default: mongoose.now() },
7
8
  allDay: { type: Boolean, default: false },