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