diff --git a/test/api/unit/libs/baseModel.test.js b/test/api/unit/libs/baseModel.test.js index a9c826bfa5..28e85aea50 100644 --- a/test/api/unit/libs/baseModel.test.js +++ b/test/api/unit/libs/baseModel.test.js @@ -5,10 +5,17 @@ describe('Base model plugin', () => { let schema; beforeEach(() => { - schema = new mongoose.Schema(); + schema = new mongoose.Schema({}, { + typeKey: '$type', + }); sandbox.stub(schema, 'add'); }); + it('throws if "typeKey" is not set to $type', () => { + const schemaWithoutTypeKey = new mongoose.Schema(); + expect(() => schemaWithoutTypeKey.plugin(baseModel)).to.throw; + }); + it('adds a _id field to the schema', () => { schema.plugin(baseModel); diff --git a/website/server/libs/baseModel.js b/website/server/libs/baseModel.js index 4ed6d0aad7..3a6e7b16a1 100644 --- a/website/server/libs/baseModel.js +++ b/website/server/libs/baseModel.js @@ -3,10 +3,14 @@ import validator from 'validator'; import _ from 'lodash'; module.exports = function baseModel (schema, options = {}) { + if (schema.options.typeKey !== '$type') { + throw new Error('Every schema must use $type as the typeKey, see https://mongoosejs.com/docs/guide.html#typeKey'); + } + if (options._id !== false) { schema.add({ _id: { - type: String, + $type: String, default: uuid, validate: [v => validator.isUUID(v), 'Invalid uuid.'], }, @@ -16,11 +20,11 @@ module.exports = function baseModel (schema, options = {}) { if (options.timestamps) { schema.add({ createdAt: { - type: Date, + $type: Date, default: Date.now, }, updatedAt: { - type: Date, + $type: Date, default: Date.now, }, }); diff --git a/website/server/models/challenge.js b/website/server/models/challenge.js index d0f02c41c4..2bb773f925 100644 --- a/website/server/models/challenge.js +++ b/website/server/models/challenge.js @@ -20,28 +20,29 @@ const MIN_SHORTNAME_SIZE_FOR_CHALLENGES = shared.constants.MIN_SHORTNAME_SIZE_FO const MAX_SUMMARY_SIZE_FOR_CHALLENGES = shared.constants.MAX_SUMMARY_SIZE_FOR_CHALLENGES; let schema = new Schema({ - name: {type: String, required: true}, - shortName: {type: String, required: true, minlength: MIN_SHORTNAME_SIZE_FOR_CHALLENGES}, - summary: {type: String, maxlength: MAX_SUMMARY_SIZE_FOR_CHALLENGES}, + name: {$type: String, required: true}, + shortName: {$type: String, required: true, minlength: MIN_SHORTNAME_SIZE_FOR_CHALLENGES}, + summary: {$type: String, maxlength: MAX_SUMMARY_SIZE_FOR_CHALLENGES}, description: String, - official: {type: Boolean, default: false}, + official: {$type: Boolean, default: false}, tasksOrder: { - habits: [{type: String, ref: 'Task'}], - dailys: [{type: String, ref: 'Task'}], - todos: [{type: String, ref: 'Task'}], - rewards: [{type: String, ref: 'Task'}], + habits: [{$type: String, ref: 'Task'}], + dailys: [{$type: String, ref: 'Task'}], + todos: [{$type: String, ref: 'Task'}], + rewards: [{$type: String, ref: 'Task'}], }, - leader: {type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.'], required: true}, - group: {type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid.'], required: true}, - memberCount: {type: Number, default: 0}, - prize: {type: Number, default: 0, min: 0}, + leader: {$type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.'], required: true}, + group: {$type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid.'], required: true}, + memberCount: {$type: Number, default: 0}, + prize: {$type: Number, default: 0, min: 0}, categories: [{ - slug: {type: String}, - name: {type: String}, + slug: {$type: String}, + name: {$type: String}, }], }, { strict: true, minimize: false, // So empty objects are returned + typeKey: '$type', // So that we can use fields named `type` }); schema.plugin(baseModel, { diff --git a/website/server/models/coupon.js b/website/server/models/coupon.js index c973ffb76c..2d99143b68 100644 --- a/website/server/models/coupon.js +++ b/website/server/models/coupon.js @@ -11,12 +11,13 @@ import { } from '../libs/errors'; export let schema = new mongoose.Schema({ - _id: {type: String, default: couponCode.generate, required: true}, - event: {type: String, enum: ['wondercon', 'google_6mo']}, - user: {type: String, ref: 'User'}, + _id: {$type: String, default: couponCode.generate, required: true}, + event: {$type: String, enum: ['wondercon', 'google_6mo']}, + user: {$type: String, ref: 'User'}, }, { strict: true, minimize: false, // So empty objects are returned + typeKey: '$type', // So that we can use fields named `type` }); schema.plugin(baseModel, { diff --git a/website/server/models/emailUnsubscription.js b/website/server/models/emailUnsubscription.js index 4df2ab530b..da21d264cb 100644 --- a/website/server/models/emailUnsubscription.js +++ b/website/server/models/emailUnsubscription.js @@ -5,7 +5,7 @@ import baseModel from '../libs/baseModel'; // A collection used to store mailing list unsubscription for non registered email addresses export let schema = new mongoose.Schema({ email: { - type: String, + $type: String, required: true, trim: true, lowercase: true, @@ -14,6 +14,7 @@ export let schema = new mongoose.Schema({ }, { strict: true, minimize: false, // So empty objects are returned + typeKey: '$type', // So that we can use fields named `type` }); schema.plugin(baseModel, { diff --git a/website/server/models/group.js b/website/server/models/group.js index ee47849cee..1064213db0 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -69,31 +69,31 @@ export const MAX_CHAT_COUNT = 200; export const MAX_SUBBED_GROUP_CHAT_COUNT = 400; export let schema = new Schema({ - name: {type: String, required: true}, - summary: {type: String, maxlength: MAX_SUMMARY_SIZE_FOR_GUILDS}, + name: {$type: String, required: true}, + summary: {$type: String, maxlength: MAX_SUMMARY_SIZE_FOR_GUILDS}, description: String, - leader: {type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.'], required: true}, - type: {type: String, enum: ['guild', 'party'], required: true}, - privacy: {type: String, enum: ['private', 'public'], default: 'private', required: true}, + leader: {$type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.'], required: true}, + type: {$type: String, enum: ['guild', 'party'], required: true}, + privacy: {$type: String, enum: ['private', 'public'], default: 'private', required: true}, chat: Array, // Used for backward compatibility, but messages aren't stored here leaderOnly: { // restrict group actions to leader (members can't do them) - challenges: {type: Boolean, default: false, required: true}, - // invites: {type: Boolean, default: false, required: true}, + challenges: {$type: Boolean, default: false, required: true}, + // invites: {$type: Boolean, default: false, required: true}, // Some group plans prevent members from getting gems - getGems: {type: Boolean, default: false}, + getGems: {$type: Boolean, default: false}, }, - memberCount: {type: Number, default: 1}, - challengeCount: {type: Number, default: 0}, - balance: {type: Number, default: 0}, + memberCount: {$type: Number, default: 1}, + challengeCount: {$type: Number, default: 0}, + balance: {$type: Number, default: 0}, logo: String, leaderMessage: String, quest: { key: String, - active: {type: Boolean, default: false}, - leader: {type: String, ref: 'User'}, + active: {$type: Boolean, default: false}, + leader: {$type: String, ref: 'User'}, progress: { hp: Number, - collect: {type: Schema.Types.Mixed, default: () => { + collect: {$type: Schema.Types.Mixed, default: () => { return {}; }}, // {feather: 5, ingot: 3} rage: Number, // limit break / "energy stored in shell", for explosion-attacks @@ -102,34 +102,35 @@ export let schema = new Schema({ // Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click // 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them. // TODO when booting user, remove from .joined and check again if we can now start the quest - members: {type: Schema.Types.Mixed, default: () => { + members: {$type: Schema.Types.Mixed, default: () => { return {}; }}, - extra: {type: Schema.Types.Mixed, default: () => { + extra: {$type: Schema.Types.Mixed, default: () => { return {}; }}, }, tasksOrder: { - habits: [{type: String, ref: 'Task'}], - dailys: [{type: String, ref: 'Task'}], - todos: [{type: String, ref: 'Task'}], - rewards: [{type: String, ref: 'Task'}], + habits: [{$type: String, ref: 'Task'}], + dailys: [{$type: String, ref: 'Task'}], + todos: [{$type: String, ref: 'Task'}], + rewards: [{$type: String, ref: 'Task'}], }, purchased: { - plan: {type: SubscriptionPlanSchema, default: () => { + plan: {$type: SubscriptionPlanSchema, default: () => { return {}; }}, }, - managers: {type: Schema.Types.Mixed, default: () => { + managers: {$type: Schema.Types.Mixed, default: () => { return {}; }}, categories: [{ - slug: {type: String}, - name: {type: String}, + slug: {$type: String}, + name: {$type: String}, }], }, { strict: true, minimize: false, // So empty objects are returned + typeKey: '$type', // So that we can use fields named `type` }); schema.plugin(baseModel, { diff --git a/website/server/models/iapPurchaseReceipt.js b/website/server/models/iapPurchaseReceipt.js index a03fa69a5e..6c2e339e47 100644 --- a/website/server/models/iapPurchaseReceipt.js +++ b/website/server/models/iapPurchaseReceipt.js @@ -5,12 +5,13 @@ import validator from 'validator'; const Schema = mongoose.Schema; export let schema = new Schema({ - _id: {type: String, required: true}, // Use a custom string as _id - consumed: {type: Boolean, default: false, required: true}, - userId: {type: String, ref: 'User', required: true, validate: [v => validator.isUUID(v), 'Invalid uuid.']}, + _id: {$type: String, required: true}, // Use a custom string as _id + consumed: {$type: Boolean, default: false, required: true}, + userId: {$type: String, ref: 'User', required: true, validate: [v => validator.isUUID(v), 'Invalid uuid.']}, }, { strict: true, minimize: false, // So empty objects are returned + typeKey: '$type', // So that we can use fields named `type` }); schema.plugin(baseModel, { diff --git a/website/server/models/message.js b/website/server/models/message.js index 5e7089677c..eeecaef74e 100644 --- a/website/server/models/message.js +++ b/website/server/models/message.js @@ -10,22 +10,23 @@ const defaultSchema = () => ({ // sender properties user: String, // profile name - contributor: {type: mongoose.Schema.Types.Mixed}, - backer: {type: mongoose.Schema.Types.Mixed}, + contributor: {$type: mongoose.Schema.Types.Mixed}, + backer: {$type: mongoose.Schema.Types.Mixed}, uuid: String, // sender uuid - userStyles: {type: mongoose.Schema.Types.Mixed}, + userStyles: {$type: mongoose.Schema.Types.Mixed}, - flags: {type: mongoose.Schema.Types.Mixed, default: {}}, - flagCount: {type: Number, default: 0}, - likes: {type: mongoose.Schema.Types.Mixed}, - _meta: {type: mongoose.Schema.Types.Mixed}, + flags: {$type: mongoose.Schema.Types.Mixed, default: {}}, + flagCount: {$type: Number, default: 0}, + likes: {$type: mongoose.Schema.Types.Mixed}, + _meta: {$type: mongoose.Schema.Types.Mixed}, }); const chatSchema = new mongoose.Schema({ ...defaultSchema(), - groupId: {type: String, ref: 'Group'}, + groupId: {$type: String, ref: 'Group'}, }, { minimize: false, // Allow for empty flags to be saved + typeKey: '$type', // So that we can use fields named `type` }); chatSchema.plugin(baseModel, { @@ -33,14 +34,15 @@ chatSchema.plugin(baseModel, { }); const inboxSchema = new mongoose.Schema({ - sent: {type: Boolean, default: false}, // if the owner sent this message + sent: {$type: Boolean, default: false}, // if the owner sent this message // the uuid of the user where the message is stored, // we store two copies of each inbox messages: // one for the sender and one for the receiver - ownerId: {type: String, ref: 'User'}, + ownerId: {$type: String, ref: 'User'}, ...defaultSchema(), }, { minimize: false, // Allow for empty flags to be saved + typeKey: '$type', // So that we can use fields named `type` }); inboxSchema.plugin(baseModel, { diff --git a/website/server/models/pushDevice.js b/website/server/models/pushDevice.js index 2467f6e963..d9e7d29a55 100644 --- a/website/server/models/pushDevice.js +++ b/website/server/models/pushDevice.js @@ -4,12 +4,13 @@ import baseModel from '../libs/baseModel'; const Schema = mongoose.Schema; export let schema = new Schema({ - regId: {type: String, required: true}, - type: {type: String, required: true, enum: ['ios', 'android']}, + regId: {$type: String, required: true}, + type: {$type: String, required: true, enum: ['ios', 'android']}, }, { strict: true, minimize: false, // So empty objects are returned _id: false, + typeKey: '$type', // So that we can use a field named `type` }); schema.plugin(baseModel, { diff --git a/website/server/models/subscriptionPlan.js b/website/server/models/subscriptionPlan.js index 86609cfa44..c26f03bd6a 100644 --- a/website/server/models/subscriptionPlan.js +++ b/website/server/models/subscriptionPlan.js @@ -5,30 +5,31 @@ import validator from 'validator'; export let schema = new mongoose.Schema({ planId: String, subscriptionId: String, - owner: {type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, - quantity: {type: Number, default: 1}, + owner: {$type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, + quantity: {$type: Number, default: 1}, paymentMethod: String, // enum: ['Paypal', 'Stripe', 'Gift', 'Amazon Payments', 'Google', '']} customerId: String, // Billing Agreement Id in case of Amazon Payments dateCreated: Date, dateTerminated: Date, dateUpdated: Date, - extraMonths: {type: Number, default: 0}, - gemsBought: {type: Number, default: 0}, - mysteryItems: {type: Array, default: () => []}, + extraMonths: {$type: Number, default: 0}, + gemsBought: {$type: Number, default: 0}, + mysteryItems: {$type: Array, default: () => []}, lastBillingDate: Date, // Used only for Amazon Payments to keep track of billing date additionalData: mongoose.Schema.Types.Mixed, // Example for Google: {'receipt': 'serialized receipt json', 'signature': 'signature string'} nextPaymentProcessing: Date, // indicates when the queue server should process this subscription again. nextBillingDate: Date, // Next time google will bill this user. consecutive: { - count: {type: Number, default: 0}, - offset: {type: Number, default: 0}, // when gifted subs, offset++ for each month. offset-- each new-month (cron). count doesn't ++ until offset==0 - gemCapExtra: {type: Number, default: 0}, - trinkets: {type: Number, default: 0}, + count: {$type: Number, default: 0}, + offset: {$type: Number, default: 0}, // when gifted subs, offset++ for each month. offset-- each new-month (cron). count doesn't ++ until offset==0 + gemCapExtra: {$type: Number, default: 0}, + trinkets: {$type: Number, default: 0}, }, }, { strict: true, minimize: false, // So empty objects are returned _id: false, + typeKey: '$type', // So that we can use fields named `type` }); schema.plugin(baseModel, { diff --git a/website/server/models/tag.js b/website/server/models/tag.js index 659b50f1d7..5e96037656 100644 --- a/website/server/models/tag.js +++ b/website/server/models/tag.js @@ -7,18 +7,19 @@ const Schema = mongoose.Schema; export let schema = new Schema({ id: { - type: String, + $type: String, default: uuid, validate: [v => validator.isUUID(v), 'Invalid uuid.'], required: true, }, - name: {type: String, required: true}, - challenge: {type: String}, - group: {type: String}, + name: {$type: String, required: true}, + challenge: {$type: String}, + group: {$type: String}, }, { strict: true, minimize: false, // So empty objects are returned _id: false, // use id instead of _id + typeKey: '$type', // So that we can use fields named `type` }); schema.plugin(baseModel, { diff --git a/website/server/models/task.js b/website/server/models/task.js index 55dcf72499..d1fbe17970 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -16,6 +16,7 @@ let discriminatorOptions = { let subDiscriminatorOptions = _.defaults(_.cloneDeep(discriminatorOptions), { _id: false, minimize: false, // So empty objects are returned + typeKey: '$type', // So that we can use fields named `type` }); export let tasksTypes = ['habit', 'daily', 'todo', 'reward']; @@ -39,11 +40,11 @@ export const taskIsGroupOrChallengeQuery = { // Important // When something changes here remember to update the client side model at common/script/libs/taskDefaults export let TaskSchema = new Schema({ - type: {type: String, enum: tasksTypes, required: true, default: tasksTypes[0]}, - text: {type: String, required: true}, - notes: {type: String, default: ''}, + type: {$type: String, enum: tasksTypes, required: true, default: tasksTypes[0]}, + text: {$type: String, required: true}, + notes: {$type: String, default: ''}, alias: { - type: String, + $type: String, match: [/^[a-zA-Z0-9-_]+$/, 'Task short names can only contain alphanumeric characters, underscores and dashes.'], validate: [{ validator () { @@ -75,12 +76,12 @@ export let TaskSchema = new Schema({ }], }, tags: [{ - type: String, + $type: String, validate: [v => validator.isUUID(v), 'Invalid uuid.'], }], - value: {type: Number, default: 0, required: true}, // redness or cost for rewards Required because it must be settable (for rewards) + value: {$type: Number, default: 0, required: true}, // redness or cost for rewards Required because it must be settable (for rewards) priority: { - type: Number, + $type: Number, default: 1, required: true, validate: [ @@ -88,42 +89,43 @@ export let TaskSchema = new Schema({ 'Valid priority values are 0.1, 1, 1.5, 2.', ], }, - attribute: {type: String, default: 'str', enum: ['str', 'con', 'int', 'per']}, - userId: {type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, // When not set it belongs to a challenge + attribute: {$type: String, default: 'str', enum: ['str', 'con', 'int', 'per']}, + userId: {$type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, // When not set it belongs to a challenge challenge: { - shortName: {type: String}, - id: {type: String, ref: 'Challenge', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, // When set (and userId not set) it's the original task - taskId: {type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, // When not set but challenge.id defined it's the original task - broken: {type: String, enum: ['CHALLENGE_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED', 'CHALLENGE_CLOSED', 'CHALLENGE_TASK_NOT_FOUND']}, // CHALLENGE_TASK_NOT_FOUND comes from v3 migration + shortName: {$type: String}, + id: {$type: String, ref: 'Challenge', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, // When set (and userId not set) it's the original task + taskId: {$type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, // When not set but challenge.id defined it's the original task + broken: {$type: String, enum: ['CHALLENGE_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED', 'CHALLENGE_CLOSED', 'CHALLENGE_TASK_NOT_FOUND']}, // CHALLENGE_TASK_NOT_FOUND comes from v3 migration winner: String, // user.profile.name of the winner }, group: { - id: {type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, - broken: {type: String, enum: ['GROUP_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED']}, - assignedUsers: [{type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}], - taskId: {type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, + id: {$type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, + broken: {$type: String, enum: ['GROUP_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED']}, + assignedUsers: [{$type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}], + taskId: {$type: String, ref: 'Task', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, approval: { - required: {type: Boolean, default: false}, - approved: {type: Boolean, default: false}, - dateApproved: {type: Date}, - approvingUser: {type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, - requested: {type: Boolean, default: false}, - requestedDate: {type: Date}, + required: {$type: Boolean, default: false}, + approved: {$type: Boolean, default: false}, + dateApproved: {$type: Date}, + approvingUser: {$type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid.']}, + requested: {$type: Boolean, default: false}, + requestedDate: {$type: Date}, }, - sharedCompletion: {type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.default}, + sharedCompletion: {$type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.default}, }, reminders: [{ _id: false, - id: {type: String, validate: [v => validator.isUUID(v), 'Invalid uuid.'], default: shared.uuid, required: true}, - startDate: {type: Date}, - time: {type: Date, required: true}, + id: {$type: String, validate: [v => validator.isUUID(v), 'Invalid uuid.'], default: shared.uuid, required: true}, + startDate: {$type: Date}, + time: {$type: Date, required: true}, }], }, _.defaults({ minimize: false, // So empty objects are returned strict: true, + typeKey: '$type', // So that we can use fields named `type` }, discriminatorOptions)); TaskSchema.plugin(baseModel, { @@ -258,32 +260,32 @@ let habitDailySchema = () => { // dailys and todos shared fields let dailyTodoSchema = () => { return { - completed: {type: Boolean, default: false}, + completed: {$type: Boolean, default: false}, // Checklist fields (dailies and todos) - collapseChecklist: {type: Boolean, default: false}, + collapseChecklist: {$type: Boolean, default: false}, checklist: [{ - completed: {type: Boolean, default: false}, - text: {type: String, required: false, default: ''}, // required:false because it can be empty on creation + completed: {$type: Boolean, default: false}, + text: {$type: String, required: false, default: ''}, // required:false because it can be empty on creation _id: false, - id: {type: String, default: shared.uuid, required: true, validate: [v => validator.isUUID(v), 'Invalid uuid.']}, - linkId: {type: String}, + id: {$type: String, default: shared.uuid, required: true, validate: [v => validator.isUUID(v), 'Invalid uuid.']}, + linkId: {$type: String}, }], }; }; export let HabitSchema = new Schema(_.defaults({ - up: {type: Boolean, default: true}, - down: {type: Boolean, default: true}, - counterUp: {type: Number, default: 0}, - counterDown: {type: Number, default: 0}, - frequency: {type: String, default: 'daily', enum: ['daily', 'weekly', 'monthly']}, + up: {$type: Boolean, default: true}, + down: {$type: Boolean, default: true}, + counterUp: {$type: Number, default: 0}, + counterDown: {$type: Number, default: 0}, + frequency: {$type: String, default: 'daily', enum: ['daily', 'weekly', 'monthly']}, }, habitDailySchema()), subDiscriminatorOptions); export let habit = Task.discriminator('habit', HabitSchema); export let DailySchema = new Schema(_.defaults({ - frequency: {type: String, default: 'weekly', enum: ['daily', 'weekly', 'monthly', 'yearly']}, + frequency: {$type: String, default: 'weekly', enum: ['daily', 'weekly', 'monthly', 'yearly']}, everyX: { - type: Number, + $type: Number, default: 1, validate: [ (val) => val % 1 === 0 && val >= 0 && val <= 9999, @@ -291,33 +293,33 @@ export let DailySchema = new Schema(_.defaults({ ], }, startDate: { - type: Date, + $type: Date, default () { return moment().startOf('day').toDate(); }, required: true, }, repeat: { // used only for 'weekly' frequency, - m: {type: Boolean, default: true}, - t: {type: Boolean, default: true}, - w: {type: Boolean, default: true}, - th: {type: Boolean, default: true}, - f: {type: Boolean, default: true}, - s: {type: Boolean, default: true}, - su: {type: Boolean, default: true}, + m: {$type: Boolean, default: true}, + t: {$type: Boolean, default: true}, + w: {$type: Boolean, default: true}, + th: {$type: Boolean, default: true}, + f: {$type: Boolean, default: true}, + s: {$type: Boolean, default: true}, + su: {$type: Boolean, default: true}, }, - streak: {type: Number, default: 0}, - daysOfMonth: {type: [Number], default: []}, // Days of the month that the daily should repeat on - weeksOfMonth: {type: [Number], default: []}, // Weeks of the month that the daily should repeat on - isDue: {type: Boolean}, - nextDue: [{type: String}], - yesterDaily: {type: Boolean, default: true, required: true}, + streak: {$type: Number, default: 0}, + daysOfMonth: {$type: [Number], default: []}, // Days of the month that the daily should repeat on + weeksOfMonth: {$type: [Number], default: []}, // Weeks of the month that the daily should repeat on + isDue: {$type: Boolean}, + nextDue: [{$type: String}], + yesterDaily: {$type: Boolean, default: true, required: true}, }, habitDailySchema(), dailyTodoSchema()), subDiscriminatorOptions); export let daily = Task.discriminator('daily', DailySchema); export let TodoSchema = new Schema(_.defaults({ dateCompleted: Date, - // TODO we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to type: Date see http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript + // TODO we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to $type: Date see http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript date: String, // due date for todos }, dailyTodoSchema()), subDiscriminatorOptions); export let todo = Task.discriminator('todo', TodoSchema); diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index 4ce701ecbe..3909c0dd53 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -19,21 +19,21 @@ const RESTRICTED_EMAIL_DOMAINS = Object.freeze(['habitica.com', 'habitrpg.com']) // User schema definition let schema = new Schema({ apiToken: { - type: String, + $type: String, default: shared.uuid, }, auth: { blocked: Boolean, - facebook: {type: Schema.Types.Mixed, default: () => { + facebook: {$type: Schema.Types.Mixed, default: () => { return {}; }}, - google: {type: Schema.Types.Mixed, default: () => { + google: {$type: Schema.Types.Mixed, default: () => { return {}; }}, local: { email: { - type: String, + $type: String, validate: [{ validator: (v) => validator.isEmail(v), message: shared.i18n.t('invalidEmail'), @@ -49,14 +49,14 @@ let schema = new Schema({ }], }, username: { - type: String, + $type: String, }, // Store a lowercase version of username to check for duplicates lowerCaseUsername: String, hashed_password: String, // eslint-disable-line camelcase // Legacy password are hashed with SHA1, new ones with bcrypt passwordHashMethod: { - type: String, + $type: String, enum: ['bcrypt', 'sha1'], }, salt: String, // Salt for SHA1 encrypted passwords, not stored for bcrypt, @@ -64,22 +64,22 @@ let schema = new Schema({ passwordResetCode: String, }, timestamps: { - created: {type: Date, default: Date.now}, - loggedin: {type: Date, default: Date.now}, + created: {$type: Date, default: Date.now}, + loggedin: {$type: Date, default: Date.now}, }, }, // We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which // have been updated (http://goo.gl/gQLz41), but we want *every* update - _v: { type: Number, default: 0 }, + _v: { $type: Number, default: 0 }, migration: String, achievements: { originalUser: Boolean, habitSurveys: Number, ultimateGearSets: { - healer: {type: Boolean, default: false}, - wizard: {type: Boolean, default: false}, - rogue: {type: Boolean, default: false}, - warrior: {type: Boolean, default: false}, + healer: {$type: Boolean, default: false}, + wizard: {$type: Boolean, default: false}, + rogue: {$type: Boolean, default: false}, + warrior: {$type: Boolean, default: false}, }, beastMaster: Boolean, beastMasterCount: Number, @@ -94,12 +94,12 @@ let schema = new Schema({ seafoam: Number, streak: Number, challenges: Array, - quests: {type: Schema.Types.Mixed, default: () => { + quests: {$type: Schema.Types.Mixed, default: () => { return {}; }}, rebirths: Number, rebirthLevel: Number, - perfect: {type: Number, default: 0}, + perfect: {$type: Number, default: 0}, habitBirthdays: Number, valentine: Number, nye: Number, @@ -129,7 +129,7 @@ let schema = new Schema({ contributor: { // 1-9, see https://trello.com/c/wkFzONhE/277-contributor-gear https://github.com/HabitRPG/habitica/issues/3801 level: { - type: Number, + $type: Number, min: 0, max: 9, }, @@ -142,106 +142,106 @@ let schema = new Schema({ critical: String, }, - balance: {type: Number, default: 0}, + balance: {$type: Number, default: 0}, purchased: { - ads: {type: Boolean, default: false}, + ads: {$type: Boolean, default: false}, // eg, {skeleton: true, pumpkin: true, eb052b: true} - skin: {type: Schema.Types.Mixed, default: () => { + skin: {$type: Schema.Types.Mixed, default: () => { return {}; }}, - hair: {type: Schema.Types.Mixed, default: () => { + hair: {$type: Schema.Types.Mixed, default: () => { return {}; }}, - shirt: {type: Schema.Types.Mixed, default: () => { + shirt: {$type: Schema.Types.Mixed, default: () => { return {}; }}, - background: {type: Schema.Types.Mixed, default: () => { + background: {$type: Schema.Types.Mixed, default: () => { return {}; }}, - txnCount: {type: Number, default: 0}, + txnCount: {$type: Number, default: 0}, mobileChat: Boolean, - plan: {type: SubscriptionPlanSchema, default: () => { + plan: {$type: SubscriptionPlanSchema, default: () => { return {}; }}, }, flags: { - customizationsNotification: {type: Boolean, default: false}, - showTour: {type: Boolean, default: true}, + customizationsNotification: {$type: Boolean, default: false}, + showTour: {$type: Boolean, default: true}, tour: { // -1 indicates "uninitiated", -2 means "complete", any other number is the current tour step (0-index) - intro: {type: Number, default: -1}, - classes: {type: Number, default: -1}, - stats: {type: Number, default: -1}, - tavern: {type: Number, default: -1}, - party: {type: Number, default: -1}, - guilds: {type: Number, default: -1}, - challenges: {type: Number, default: -1}, - market: {type: Number, default: -1}, - pets: {type: Number, default: -1}, - mounts: {type: Number, default: -1}, - hall: {type: Number, default: -1}, - equipment: {type: Number, default: -1}, + intro: {$type: Number, default: -1}, + classes: {$type: Number, default: -1}, + stats: {$type: Number, default: -1}, + tavern: {$type: Number, default: -1}, + party: {$type: Number, default: -1}, + guilds: {$type: Number, default: -1}, + challenges: {$type: Number, default: -1}, + market: {$type: Number, default: -1}, + pets: {$type: Number, default: -1}, + mounts: {$type: Number, default: -1}, + hall: {$type: Number, default: -1}, + equipment: {$type: Number, default: -1}, }, tutorial: { common: { - habits: {type: Boolean, default: false}, - dailies: {type: Boolean, default: false}, - todos: {type: Boolean, default: false}, - rewards: {type: Boolean, default: false}, - party: {type: Boolean, default: false}, - pets: {type: Boolean, default: false}, - gems: {type: Boolean, default: false}, - skills: {type: Boolean, default: false}, - classes: {type: Boolean, default: false}, - tavern: {type: Boolean, default: false}, - equipment: {type: Boolean, default: false}, - items: {type: Boolean, default: false}, - mounts: {type: Boolean, default: false}, - inbox: {type: Boolean, default: false}, - stats: {type: Boolean, default: false}, + habits: {$type: Boolean, default: false}, + dailies: {$type: Boolean, default: false}, + todos: {$type: Boolean, default: false}, + rewards: {$type: Boolean, default: false}, + party: {$type: Boolean, default: false}, + pets: {$type: Boolean, default: false}, + gems: {$type: Boolean, default: false}, + skills: {$type: Boolean, default: false}, + classes: {$type: Boolean, default: false}, + tavern: {$type: Boolean, default: false}, + equipment: {$type: Boolean, default: false}, + items: {$type: Boolean, default: false}, + mounts: {$type: Boolean, default: false}, + inbox: {$type: Boolean, default: false}, + stats: {$type: Boolean, default: false}, }, ios: { - addTask: {type: Boolean, default: false}, - editTask: {type: Boolean, default: false}, - deleteTask: {type: Boolean, default: false}, - filterTask: {type: Boolean, default: false}, - groupPets: {type: Boolean, default: false}, - inviteParty: {type: Boolean, default: false}, - reorderTask: {type: Boolean, default: false}, + addTask: {$type: Boolean, default: false}, + editTask: {$type: Boolean, default: false}, + deleteTask: {$type: Boolean, default: false}, + filterTask: {$type: Boolean, default: false}, + groupPets: {$type: Boolean, default: false}, + inviteParty: {$type: Boolean, default: false}, + reorderTask: {$type: Boolean, default: false}, }, }, - dropsEnabled: {type: Boolean, default: false}, - itemsEnabled: {type: Boolean, default: false}, - newStuff: {type: Boolean, default: false}, - rewrite: {type: Boolean, default: true}, - classSelected: {type: Boolean, default: false}, + dropsEnabled: {$type: Boolean, default: false}, + itemsEnabled: {$type: Boolean, default: false}, + newStuff: {$type: Boolean, default: false}, + rewrite: {$type: Boolean, default: true}, + classSelected: {$type: Boolean, default: false}, mathUpdates: Boolean, - rebirthEnabled: {type: Boolean, default: false}, - levelDrops: {type: Schema.Types.Mixed, default: () => { + rebirthEnabled: {$type: Boolean, default: false}, + levelDrops: {$type: Schema.Types.Mixed, default: () => { return {}; }}, chatRevoked: Boolean, // Used to track the status of recapture emails sent to each user, // can be 0 - no email sent - 1, 2, 3 or 4 - 4 means no more email will be sent to the user - recaptureEmailsPhase: {type: Number, default: 0}, + recaptureEmailsPhase: {$type: Number, default: 0}, // Needed to track the tip to send inside the email - weeklyRecapEmailsPhase: {type: Number, default: 0}, + weeklyRecapEmailsPhase: {$type: Number, default: 0}, // Used to track when the next weekly recap should be sent - lastWeeklyRecap: {type: Date, default: Date.now}, + lastWeeklyRecap: {$type: Date, default: Date.now}, // Used to enable weekly recap emails as users login lastWeeklyRecapDiscriminator: Boolean, onboardingEmailsPhase: String, // Keep track of the latest onboarding email sent - communityGuidelinesAccepted: {type: Boolean, default: false}, - cronCount: {type: Number, default: 0}, - welcomed: {type: Boolean, default: false}, - armoireEnabled: {type: Boolean, default: true}, - armoireOpened: {type: Boolean, default: false}, - armoireEmpty: {type: Boolean, default: false}, - cardReceived: {type: Boolean, default: false}, - warnedLowHealth: {type: Boolean, default: false}, - verifiedUsername: {type: Boolean, default: false}, + communityGuidelinesAccepted: {$type: Boolean, default: false}, + cronCount: {$type: Number, default: 0}, + welcomed: {$type: Boolean, default: false}, + armoireEnabled: {$type: Boolean, default: true}, + armoireOpened: {$type: Boolean, default: false}, + armoireEmpty: {$type: Boolean, default: false}, + cardReceived: {$type: Boolean, default: false}, + warnedLowHealth: {$type: Boolean, default: false}, + verifiedUsername: {$type: Boolean, default: false}, }, history: { @@ -252,7 +252,7 @@ let schema = new Schema({ items: { gear: { owned: _.transform(shared.content.gear.flat, (m, v) => { - m[v.key] = {type: Boolean}; + m[v.key] = {$type: Boolean}; if (v.key.match(/(armor|head|shield)_warrior_0/) || v.gearSet === 'glasses' || v.gearSet === 'headband') { m[v.key].default = true; } @@ -260,9 +260,9 @@ let schema = new Schema({ equipped: { weapon: String, - armor: {type: String, default: 'armor_base_0'}, - head: {type: String, default: 'head_base_0'}, - shield: {type: String, default: 'shield_base_0'}, + armor: {$type: String, default: 'armor_base_0'}, + head: {$type: String, default: 'head_base_0'}, + shield: {$type: String, default: 'shield_base_0'}, back: String, headAccessory: String, eyewear: String, @@ -270,9 +270,9 @@ let schema = new Schema({ }, costume: { weapon: String, - armor: {type: String, default: 'armor_base_0'}, - head: {type: String, default: 'head_base_0'}, - shield: {type: String, default: 'shield_base_0'}, + armor: {$type: String, default: 'armor_base_0'}, + head: {$type: String, default: 'head_base_0'}, + shield: {$type: String, default: 'shield_base_0'}, back: String, headAccessory: String, eyewear: String, @@ -281,25 +281,25 @@ let schema = new Schema({ }, special: { - snowball: {type: Number, default: 0}, - spookySparkles: {type: Number, default: 0}, - shinySeed: {type: Number, default: 0}, - seafoam: {type: Number, default: 0}, - valentine: {type: Number, default: 0}, + snowball: {$type: Number, default: 0}, + spookySparkles: {$type: Number, default: 0}, + shinySeed: {$type: Number, default: 0}, + seafoam: {$type: Number, default: 0}, + valentine: {$type: Number, default: 0}, valentineReceived: Array, // array of strings, by sender name - nye: {type: Number, default: 0}, + nye: {$type: Number, default: 0}, nyeReceived: Array, - greeting: {type: Number, default: 0}, + greeting: {$type: Number, default: 0}, greetingReceived: Array, - thankyou: {type: Number, default: 0}, + thankyou: {$type: Number, default: 0}, thankyouReceived: Array, - birthday: {type: Number, default: 0}, + birthday: {$type: Number, default: 0}, birthdayReceived: Array, - congrats: {type: Number, default: 0}, + congrats: {$type: Number, default: 0}, congratsReceived: Array, - getwell: {type: Number, default: 0}, + getwell: {$type: Number, default: 0}, getwellReceived: Array, - goodluck: {type: Number, default: 0}, + goodluck: {$type: Number, default: 0}, goodluckReceived: Array, }, @@ -361,44 +361,44 @@ let schema = new Schema({ quests: _.transform(shared.content.quests, (m, v, k) => m[k] = Number), lastDrop: { - date: {type: Date, default: Date.now}, - count: {type: Number, default: 0}, + date: {$type: Date, default: Date.now}, + count: {$type: Number, default: 0}, }, }, - lastCron: {type: Date, default: Date.now}, - _cronSignature: {type: String, default: 'NOT_RUNNING'}, // Private property used to avoid double cron + lastCron: {$type: Date, default: Date.now}, + _cronSignature: {$type: String, default: 'NOT_RUNNING'}, // Private property used to avoid double cron // {GROUP_ID: Boolean}, represents whether they have unseen chat messages - newMessages: {type: Schema.Types.Mixed, default: () => { + newMessages: {$type: Schema.Types.Mixed, default: () => { return {}; }}, - challenges: [{type: String, ref: 'Challenge', validate: [v => validator.isUUID(v), 'Invalid uuid.']}], + challenges: [{$type: String, ref: 'Challenge', validate: [v => validator.isUUID(v), 'Invalid uuid.']}], invitations: { // Using an array without validation because otherwise mongoose treat this as a subdocument and applies _id by default // Schema is (id, name, inviter, publicGuild) // TODO one way to fix is http://mongoosejs.com/docs/guide.html#_id - guilds: {type: Array, default: () => []}, + guilds: {$type: Array, default: () => []}, // Using a Mixed type because otherwise user.invitations.party = {} // to reset invitation, causes validation to fail TODO // schema is the same as for guild invitations (id, name, inviter) - party: {type: Schema.Types.Mixed, default: () => { + party: {$type: Schema.Types.Mixed, default: () => { return {}; }}, parties: [{ id: { - type: String, + $type: String, ref: 'Group', required: true, validate: [v => validator.isUUID(v), 'Invalid uuid.'], }, name: { - type: String, + $type: String, required: true, }, inviter: { - type: String, + $type: String, ref: 'User', required: true, validate: [v => validator.isUUID(v), 'Invalid uuid.'], @@ -406,105 +406,105 @@ let schema = new Schema({ }], }, - guilds: [{type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid.']}], + guilds: [{$type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid.']}], party: { - _id: {type: String, validate: [v => validator.isUUID(v), 'Invalid uuid.'], ref: 'Group'}, - order: {type: String, default: 'level'}, - orderAscending: {type: String, default: 'ascending'}, + _id: {$type: String, validate: [v => validator.isUUID(v), 'Invalid uuid.'], ref: 'Group'}, + order: {$type: String, default: 'level'}, + orderAscending: {$type: String, default: 'ascending'}, quest: { key: String, progress: { - up: {type: Number, default: 0}, - down: {type: Number, default: 0}, - collect: {type: Schema.Types.Mixed, default: () => { + up: {$type: Number, default: 0}, + down: {$type: Number, default: 0}, + collect: {$type: Schema.Types.Mixed, default: () => { return {}; }}, - collectedItems: {type: Number, default: 0}, + collectedItems: {$type: Number, default: 0}, }, completed: String, // When quest is done, we move it from key => completed, and it's a one-time flag (for modal) that they unset by clicking "ok" in browser - RSVPNeeded: {type: Boolean, default: false}, // Set to true when invite is pending, set to false when quest invite is accepted or rejected, quest starts, or quest is cancelled + RSVPNeeded: {$type: Boolean, default: false}, // Set to true when invite is pending, set to false when quest invite is accepted or rejected, quest starts, or quest is cancelled }, }, preferences: { - dayStart: {type: Number, default: 0, min: 0, max: 23}, - size: {type: String, enum: ['broad', 'slim'], default: 'slim'}, + dayStart: {$type: Number, default: 0, min: 0, max: 23}, + size: {$type: String, enum: ['broad', 'slim'], default: 'slim'}, hair: { - color: {type: String, default: 'red'}, - base: {type: Number, default: 3}, - bangs: {type: Number, default: 1}, - beard: {type: Number, default: 0}, - mustache: {type: Number, default: 0}, - flower: {type: Number, default: 1}, + color: {$type: String, default: 'red'}, + base: {$type: Number, default: 3}, + bangs: {$type: Number, default: 1}, + beard: {$type: Number, default: 0}, + mustache: {$type: Number, default: 0}, + flower: {$type: Number, default: 1}, }, - hideHeader: {type: Boolean, default: false}, - skin: {type: String, default: '915533'}, - shirt: {type: String, default: 'blue'}, - timezoneOffset: {type: Number, default: 0}, - sound: {type: String, default: 'rosstavoTheme', enum: ['off', ...shared.content.audioThemes]}, - chair: {type: String, default: 'none'}, + hideHeader: {$type: Boolean, default: false}, + skin: {$type: String, default: '915533'}, + shirt: {$type: String, default: 'blue'}, + timezoneOffset: {$type: Number, default: 0}, + sound: {$type: String, default: 'rosstavoTheme', enum: ['off', ...shared.content.audioThemes]}, + chair: {$type: String, default: 'none'}, timezoneOffsetAtLastCron: Number, language: String, automaticAllocation: Boolean, - allocationMode: {type: String, enum: ['flat', 'classbased', 'taskbased'], default: 'flat'}, - autoEquip: {type: Boolean, default: true}, + allocationMode: {$type: String, enum: ['flat', 'classbased', 'taskbased'], default: 'flat'}, + autoEquip: {$type: Boolean, default: true}, costume: Boolean, - dateFormat: {type: String, enum: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], default: 'MM/dd/yyyy'}, - sleep: {type: Boolean, default: false}, - stickyHeader: {type: Boolean, default: true}, - disableClasses: {type: Boolean, default: false}, - newTaskEdit: {type: Boolean, default: false}, - dailyDueDefaultView: {type: Boolean, default: false}, - advancedCollapsed: {type: Boolean, default: false}, - toolbarCollapsed: {type: Boolean, default: false}, - reverseChatOrder: {type: Boolean, default: false}, + dateFormat: {$type: String, enum: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], default: 'MM/dd/yyyy'}, + sleep: {$type: Boolean, default: false}, + stickyHeader: {$type: Boolean, default: true}, + disableClasses: {$type: Boolean, default: false}, + newTaskEdit: {$type: Boolean, default: false}, + dailyDueDefaultView: {$type: Boolean, default: false}, + advancedCollapsed: {$type: Boolean, default: false}, + toolbarCollapsed: {$type: Boolean, default: false}, + reverseChatOrder: {$type: Boolean, default: false}, background: String, - displayInviteToPartyWhenPartyIs1: {type: Boolean, default: true}, - webhooks: {type: Schema.Types.Mixed, default: () => { + displayInviteToPartyWhenPartyIs1: {$type: Boolean, default: true}, + webhooks: {$type: Schema.Types.Mixed, default: () => { return {}; }}, // For the following fields make sure to use strict comparison when searching for falsey values (=== false) // As users who didn't login after these were introduced may have them undefined/null emailNotifications: { - unsubscribeFromAll: {type: Boolean, default: false}, - newPM: {type: Boolean, default: true}, - kickedGroup: {type: Boolean, default: true}, - wonChallenge: {type: Boolean, default: true}, - giftedGems: {type: Boolean, default: true}, - giftedSubscription: {type: Boolean, default: true}, - invitedParty: {type: Boolean, default: true}, - invitedGuild: {type: Boolean, default: true}, - questStarted: {type: Boolean, default: true}, - invitedQuest: {type: Boolean, default: true}, - // remindersToLogin: {type: Boolean, default: true}, + unsubscribeFromAll: {$type: Boolean, default: false}, + newPM: {$type: Boolean, default: true}, + kickedGroup: {$type: Boolean, default: true}, + wonChallenge: {$type: Boolean, default: true}, + giftedGems: {$type: Boolean, default: true}, + giftedSubscription: {$type: Boolean, default: true}, + invitedParty: {$type: Boolean, default: true}, + invitedGuild: {$type: Boolean, default: true}, + questStarted: {$type: Boolean, default: true}, + invitedQuest: {$type: Boolean, default: true}, + // remindersToLogin: {$type: Boolean, default: true}, // importantAnnouncements are in fact the recapture emails - importantAnnouncements: {type: Boolean, default: true}, - weeklyRecaps: {type: Boolean, default: true}, - onboarding: {type: Boolean, default: true}, + importantAnnouncements: {$type: Boolean, default: true}, + weeklyRecaps: {$type: Boolean, default: true}, + onboarding: {$type: Boolean, default: true}, }, pushNotifications: { - unsubscribeFromAll: {type: Boolean, default: false}, - newPM: {type: Boolean, default: true}, - wonChallenge: {type: Boolean, default: true}, - giftedGems: {type: Boolean, default: true}, - giftedSubscription: {type: Boolean, default: true}, - invitedParty: {type: Boolean, default: true}, - invitedGuild: {type: Boolean, default: true}, - questStarted: {type: Boolean, default: true}, - invitedQuest: {type: Boolean, default: true}, + unsubscribeFromAll: {$type: Boolean, default: false}, + newPM: {$type: Boolean, default: true}, + wonChallenge: {$type: Boolean, default: true}, + giftedGems: {$type: Boolean, default: true}, + giftedSubscription: {$type: Boolean, default: true}, + invitedParty: {$type: Boolean, default: true}, + invitedGuild: {$type: Boolean, default: true}, + questStarted: {$type: Boolean, default: true}, + invitedQuest: {$type: Boolean, default: true}, }, suppressModals: { - levelUp: {type: Boolean, default: false}, - hatchPet: {type: Boolean, default: false}, - raisePet: {type: Boolean, default: false}, - streak: {type: Boolean, default: false}, + levelUp: {$type: Boolean, default: false}, + hatchPet: {$type: Boolean, default: false}, + raisePet: {$type: Boolean, default: false}, + streak: {$type: Boolean, default: false}, }, tasks: { - groupByChallenge: {type: Boolean, default: false}, // @TODO remove? not used - confirmScoreNotes: {type: Boolean, default: false}, // @TODO remove? not used + groupByChallenge: {$type: Boolean, default: false}, // @TODO remove? not used + confirmScoreNotes: {$type: Boolean, default: false}, // @TODO remove? not used }, improvementCategories: { - type: Array, + $type: Array, validate: (categories) => { const validCategories = ['work', 'exercise', 'healthWellness', 'school', 'teams', 'chores', 'creativity']; let isValidCategory = categories.every(category => validCategories.indexOf(category) !== -1); @@ -516,42 +516,42 @@ let schema = new Schema({ blurb: String, imageUrl: String, name: { - type: String, + $type: String, required: true, trim: true, }, }, stats: { - hp: {type: Number, default: shared.maxHealth}, - mp: {type: Number, default: 10}, - exp: {type: Number, default: 0}, - gp: {type: Number, default: 0}, - lvl: {type: Number, default: 1, min: 1}, + hp: {$type: Number, default: shared.maxHealth}, + mp: {$type: Number, default: 10}, + exp: {$type: Number, default: 0}, + gp: {$type: Number, default: 0}, + lvl: {$type: Number, default: 1, min: 1}, // Class System - class: {type: String, enum: ['warrior', 'rogue', 'wizard', 'healer'], default: 'warrior', required: true}, - points: {type: Number, default: 0}, - str: {type: Number, default: 0}, - con: {type: Number, default: 0}, - int: {type: Number, default: 0}, - per: {type: Number, default: 0}, + class: {$type: String, enum: ['warrior', 'rogue', 'wizard', 'healer'], default: 'warrior', required: true}, + points: {$type: Number, default: 0}, + str: {$type: Number, default: 0}, + con: {$type: Number, default: 0}, + int: {$type: Number, default: 0}, + per: {$type: Number, default: 0}, buffs: { - str: {type: Number, default: 0}, - int: {type: Number, default: 0}, - per: {type: Number, default: 0}, - con: {type: Number, default: 0}, - stealth: {type: Number, default: 0}, - streaks: {type: Boolean, default: false}, - snowball: {type: Boolean, default: false}, - spookySparkles: {type: Boolean, default: false}, - shinySeed: {type: Boolean, default: false}, - seafoam: {type: Boolean, default: false}, + str: {$type: Number, default: 0}, + int: {$type: Number, default: 0}, + per: {$type: Number, default: 0}, + con: {$type: Number, default: 0}, + stealth: {$type: Number, default: 0}, + streaks: {$type: Boolean, default: false}, + snowball: {$type: Boolean, default: false}, + spookySparkles: {$type: Boolean, default: false}, + shinySeed: {$type: Boolean, default: false}, + seafoam: {$type: Boolean, default: false}, }, training: { - int: {type: Number, default: 0}, - per: {type: Number, default: 0}, - str: {type: Number, default: 0}, - con: {type: Number, default: 0}, + int: {$type: Number, default: 0}, + per: {$type: Number, default: 0}, + str: {$type: Number, default: 0}, + con: {$type: Number, default: 0}, }, }, @@ -560,45 +560,46 @@ let schema = new Schema({ inbox: { // messages are stored in the Inbox collection - newMessages: {type: Number, default: 0}, - blocks: {type: Array, default: () => []}, - optOut: {type: Boolean, default: false}, + newMessages: {$type: Number, default: 0}, + blocks: {$type: Array, default: () => []}, + optOut: {$type: Boolean, default: false}, }, tasksOrder: { - habits: [{type: String, ref: 'Task'}], - dailys: [{type: String, ref: 'Task'}], - todos: [{type: String, ref: 'Task'}], - rewards: [{type: String, ref: 'Task'}], + habits: [{$type: String, ref: 'Task'}], + dailys: [{$type: String, ref: 'Task'}], + todos: [{$type: String, ref: 'Task'}], + rewards: [{$type: String, ref: 'Task'}], }, - extra: {type: Schema.Types.Mixed, default: () => { + extra: {$type: Schema.Types.Mixed, default: () => { return {}; }}, pushDevices: [PushDeviceSchema], - _ABtests: {type: Schema.Types.Mixed, default: () => { + _ABtests: {$type: Schema.Types.Mixed, default: () => { return {}; }}, webhooks: [WebhookSchema], - loginIncentives: {type: Number, default: 0}, - invitesSent: {type: Number, default: 0}, + loginIncentives: {$type: Number, default: 0}, + invitesSent: {$type: Number, default: 0}, // Items manually pinned by the user pinnedItems: [{ _id: false, - path: {type: String}, - type: {type: String}, + path: {$type: String}, + type: {$type: String}, }], // Ordered array of shown pinned items, necessary for sorting because seasonal items are not stored in pinnedItems - pinnedItemsOrder: [{type: String}], + pinnedItemsOrder: [{$type: String}], // Items the user manually unpinned from the ones suggested by Habitica unpinnedItems: [{ _id: false, - path: {type: String}, - type: {type: String}, + path: {$type: String}, + type: {$type: String}, }], }, { skipVersioning: { notifications: true }, strict: true, minimize: false, // So empty objects are returned + typeKey: '$type', // So that we can use fields named `type` }); module.exports = schema; diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js index 6ca205b03c..acb280b16a 100644 --- a/website/server/models/userNotification.js +++ b/website/server/models/userNotification.js @@ -37,7 +37,7 @@ const Schema = mongoose.Schema; export let schema = new Schema({ id: { - type: String, + $type: String, default: uuid, validate: [v => validator.isUUID(v), 'Invalid uuid.'], // @TODO: Add these back once we figure out the issue with notifications @@ -45,25 +45,26 @@ export let schema = new Schema({ // required: true, }, type: { - type: String, + $type: String, // @TODO: Add these back once we figure out the issue with notifications // See Fix for https://github.com/HabitRPG/habitica/issues/9923 // required: true, enum: NOTIFICATION_TYPES, }, - data: {type: Schema.Types.Mixed, default: () => { + data: {$type: Schema.Types.Mixed, default: () => { return {}; }}, // A field to mark the notification as seen without deleting it, optional use seen: { - type: Boolean, + $type: Boolean, // required: true, default: () => false, }, }, { strict: true, minimize: false, // So empty objects are returned - _id: false, // use id instead of _id + _id: false, // use id instead of _id, + typeKey: '$type', // So that we can use fields named `type` }); /** diff --git a/website/server/models/webhook.js b/website/server/models/webhook.js index 3761166619..f4075faf38 100644 --- a/website/server/models/webhook.js +++ b/website/server/models/webhook.js @@ -32,13 +32,13 @@ const QUEST_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({ export let schema = new Schema({ id: { - type: String, + $type: String, required: true, validate: [v => validator.isUUID(v), shared.i18n.t('invalidWebhookId')], default: uuid, }, type: { - type: String, + $type: String, required: true, enum: [ 'globalActivity', // global webhooks send a request for every type of event @@ -48,12 +48,12 @@ export let schema = new Schema({ default: 'taskActivity', }, label: { - type: String, + $type: String, required: false, default: '', }, url: { - type: String, + $type: String, required: true, validate: [(v) => { return validator.isURL(v, { @@ -61,9 +61,9 @@ export let schema = new Schema({ }); }, shared.i18n.t('invalidUrl')], }, - enabled: { type: Boolean, required: true, default: true }, + enabled: { $type: Boolean, required: true, default: true }, options: { - type: Schema.Types.Mixed, + $type: Schema.Types.Mixed, required: true, default () { return {}; @@ -73,6 +73,7 @@ export let schema = new Schema({ strict: true, minimize: false, // So empty objects are returned _id: false, + typeKey: '$type', // So that we can use fields named `type` }); schema.plugin(baseModel, {