@ideal-photography/shared
Version:
Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.
160 lines (136 loc) • 4.4 kB
JavaScript
import mongoose from 'mongoose';
const userPushSubscriptionSchema = new mongoose.Schema({
// Reference to User
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: [true, 'User reference is required'],
index: true
},
// Push subscription data from browser
subscription: {
endpoint: {
type: String,
required: [true, 'Subscription endpoint is required'],
trim: true
},
keys: {
p256dh: {
type: String,
required: [true, 'P256DH key is required'],
trim: true
},
auth: {
type: String,
required: [true, 'Auth key is required'],
trim: true
}
}
},
// Subscription status and management
isActive: {
type: Boolean,
default: true,
index: true
},
// Tracking
lastSentAt: {
type: Date,
default: null
},
// Device/browser information
userAgent: {
type: String,
trim: true,
maxlength: [500, 'User agent too long']
},
// Subscription metadata
metadata: {
type: mongoose.Schema.Types.Mixed,
default: {}
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Indexes for performance
userPushSubscriptionSchema.index({ user: 1, isActive: 1 });
userPushSubscriptionSchema.index({ 'subscription.endpoint': 1 });
userPushSubscriptionSchema.index({ lastSentAt: 1 });
userPushSubscriptionSchema.index({ createdAt: -1 });
// Compound index for efficient queries
userPushSubscriptionSchema.index({
user: 1,
isActive: 1,
lastSentAt: 1
});
// Virtual for subscription age
userPushSubscriptionSchema.virtual('ageInDays').get(function () {
return Math.floor((Date.now() - this.createdAt) / (1000 * 60 * 60 * 24));
});
// Virtual for subscription status
userPushSubscriptionSchema.virtual('isStale').get(function () {
// Consider subscription stale if no activity for 30 days
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
return this.lastSentAt && this.lastSentAt < thirtyDaysAgo;
});
// Methods
userPushSubscriptionSchema.methods.markAsInactive = function () {
this.isActive = false;
return this.save();
};
userPushSubscriptionSchema.methods.updateLastSent = function () {
this.lastSentAt = new Date();
return this.save();
};
userPushSubscriptionSchema.methods.isValid = function () {
return this.isActive &&
this.subscription?.endpoint &&
this.subscription?.keys?.p256dh &&
this.subscription?.keys?.auth;
};
// Static methods
userPushSubscriptionSchema.statics.findActiveByUser = function (userId) {
return this.find({
user: userId,
isActive: true
}).sort({ lastSentAt: -1 });
};
userPushSubscriptionSchema.statics.findStaleSubscriptions = function (daysOld = 30) {
const cutoffDate = new Date(Date.now() - daysOld * 24 * 60 * 60 * 1000);
return this.find({
isActive: true,
lastSentAt: { $lt: cutoffDate }
});
};
userPushSubscriptionSchema.statics.cleanupInactive = function () {
return this.updateMany(
{ isActive: false },
{ $set: { isActive: false } }
);
};
userPushSubscriptionSchema.statics.getUserSubscriptionCount = function (userId) {
return this.countDocuments({
user: userId,
isActive: true
});
};
// Pre-save middleware
userPushSubscriptionSchema.pre('save', function (next) {
// Ensure subscription data is valid
if (!this.subscription?.endpoint || !this.subscription?.keys?.p256dh || !this.subscription?.keys?.auth) {
return next(new Error('Invalid subscription data'));
}
// Set user agent if not provided
if (!this.userAgent && this.isNew) {
this.userAgent = 'Unknown';
}
next();
});
// Pre-remove middleware
userPushSubscriptionSchema.pre('remove', function (next) {
console.log(`Removing push subscription for user ${this.user}`);
next();
});
export default mongoose.model('UserPushSubscription', userPushSubscriptionSchema);