All files / server/src/features/user userSchema.ts

100% Statements 18/18
100% Branches 0/0
100% Functions 9/9
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125                      59x     14918x     59x         1531x 1531x     59x                       3266x             59x         19553x         59x           1531x       1531x           2043x             59x                                                 3266x                     59x         59x       59x  
// Registers the global mongoose plugins, which are applied when a model is
// compiled below, so it has to stay above the other imports
import "server/db/mongoosePlugins";
import dayjs from "dayjs";
import mongoose, { ObjectId } from "mongoose";
import mongooseLeanVirtuals from "mongoose-lean-virtuals";
import { z } from "zod";
import { StoredEmailSchema } from "shared/constants/validation";
import { EventLogAction } from "shared/types/models/eventLog";
import { UserGroup } from "shared/types/models/user";
 
const LotterySignupSchemaDb = z.object({
  programItemId: z.string(),
  priority: z.number(),
  signedToStartTime: z.date().transform((date) => dayjs(date).toISOString()),
});
 
const EventLogItemSchemaDb = z.object({
  eventLogItemId: z.custom<ObjectId>().transform(String),
  action: z.enum(EventLogAction),
  isSeen: z.boolean(),
  programItemId: z.string(),
  programItemStartTime: z.date().transform((date) => dayjs(date).toISOString()),
  createdAt: z.date().transform((date) => dayjs(date).toISOString()),
});
 
export const UserSchemaDb = z
  .object({
    kompassiId: z.string(),
    kompassiUsernameAccepted: z.boolean(),
    username: z.string(),
    password: z.string(),
    userGroup: z.enum(UserGroup),
    serial: z.string(),
    groupCode: z.string(),
    isGroupCreator: z.boolean(),
    favoriteProgramItemIds: z.array(z.string()),
    lotterySignups: z.array(LotterySignupSchemaDb),
    createdAt: z.date().transform((date) => dayjs(date).toISOString()),
    eventLogItems: z.array(EventLogItemSchemaDb),
    email: StoredEmailSchema,
    emailNotificationPermitAsked: z.boolean(),
  })
  .strip();
 
const lotterySignupSchema = new mongoose.Schema({
  programItemId: { type: String, required: true },
  priority: { type: Number, required: true },
  signedToStartTime: {
    type: Date,
    get: (value: Date) => new Date(value),
    required: true,
  },
});
 
const eventLogItemSchema = new mongoose.Schema(
  {
    action: { type: String, required: true },
    programItemId: { type: String, required: true },
    programItemStartTime: {
      type: Date,
      get: (value: Date) => new Date(value),
      required: true,
    },
    isSeen: { type: Boolean, required: true },
    createdAt: { type: Date, get: (value: Date) => new Date(value) },
  },
  {
    virtuals: {
      eventLogItemId: {
        get(this: { _id: ObjectId }) {
          return this._id;
        },
      },
    },
  },
);
 
const userSchema = new mongoose.Schema(
  {
    // No `required`: "" is the meaningful value for a local account, and the
    // global validator override that lets empty strings satisfy `required`
    // only loads with the Express app - tests connecting straight to the DB
    // would reject every local account. The default covers the undefined case
    kompassiId: { type: String, default: "" },
    kompassiUsernameAccepted: { type: Boolean, required: true },
    // Usernames identify a user everywhere: sign-ups, event log writes and
    // assignment results all match on this field, and a username-filtered update
    // only ever reaches one document. A second document with the same name would
    // silently collect writes meant for the other
    username: { type: String, required: true, unique: true },
    password: { type: String, required: true },
    userGroup: { type: String, required: true },
    serial: { type: String, required: true },
    isGroupCreator: { type: Boolean, required: true },
    groupCode: { type: String, required: true },
    favoriteProgramItemIds: { type: [String], required: true },
    lotterySignups: { type: [lotterySignupSchema], required: true },
    eventLogItems: { type: [eventLogItemSchema], required: true },
    email: { type: String, required: false },
    emailNotificationPermitAsked: { type: Boolean, required: true },
    createdAt: {
      type: Date,
      get: (value: Date) => new Date(value),
    },
  },
  {
    timestamps: true,
  },
);
 
// One Konsti account per Kompassi identity, and an index for the lookup every
// Kompassi login does. Partial so it ignores the "" local accounts share,
// which would otherwise all collide with each other
userSchema.index(
  { kompassiId: 1 },
  { unique: true, partialFilterExpression: { kompassiId: { $gt: "" } } },
);
 
userSchema.plugin(mongooseLeanVirtuals, {
  enabledByDefault: true,
});
 
export const UserModel = mongoose.model("user", userSchema);