@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.
- package/FEAR.js +76 -63
- package/FEARServer.js +290 -20
- package/controllers/address.js +9 -0
- package/controllers/auth/index.js +498 -92
- package/controllers/auth/password.js +237 -0
- package/controllers/order.js +0 -1
- package/controllers/payment.js +5 -142
- package/libs/db/index.js +5 -0
- package/libs/emailer/info.js +22 -34
- package/libs/emailer/smtp.js +511 -65
- package/libs/passport/index.js +137 -0
- package/libs/passport.js +22 -0
- package/libs/paypal/index.js +82 -0
- package/libs/stripe/index.js +306 -0
- package/libs/validator/index.js +2 -2
- package/models/address.js +37 -0
- package/models/blog.js +947 -17
- package/models/brand.js +205 -8
- package/models/category.js +498 -7
- package/models/events.js +1 -0
- package/models/order.js +29 -154
- package/models/payment.js +18 -79
- package/models/user.js +116 -49
- package/package.json +1 -1
- package/routes/address.js +16 -0
- package/routes/auth.js +9 -3
- package/routes/mail.js +10 -165
- package/routes/order.js +7 -4
- package/routes/password.js +17 -0
- package/routes/payment.js +4 -8
- package/routes/paypal.js +12 -0
- package/routes/stripe.js +27 -0
- package/libs/passport/passport.js +0 -109
- /package/routes/{events.js → event.js} +0 -0
package/models/category.js
CHANGED
|
@@ -1,11 +1,502 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
const slugify = require("slugify");
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
title: { type: String,
|
|
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
|
-
{
|
|
51
|
+
{
|
|
52
|
+
timestamps: true,
|
|
53
|
+
toJSON: { virtuals: true },
|
|
54
|
+
toObject: { virtuals: true }
|
|
55
|
+
}
|
|
8
56
|
);
|
|
9
57
|
|
|
10
|
-
//
|
|
11
|
-
|
|
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 },
|