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

100% Statements 19/19
100% Branches 0/0
100% Functions 10/10
100% Lines 19/19

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 126 127 128 129 130 131 132 133                      61x     18006x     61x         2078x   9x     2078x     61x                       4489x             61x         23628x         61x           2078x               2078x           2699x             61x                                                 4489x                     61x         61x       61x  
// 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 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 { ProgramType } from "shared/types/models/programItem";
import { UserGroup } from "shared/types/models/user";
 
const LotterySignupSchemaDb = z.object({
  programItemId: z.string(),
  priority: z.number(),
  signedToStartTime: z.date().transform((date) => 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) => date.toISOString()),
  lastProgramItemEndTime: z.optional(
    z.date().transform((date) => date.toISOString()),
  ),
  programType: z.optional(z.enum(ProgramType)),
  createdAt: z.date().transform((date) => 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) => 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,
    },
    // No getter: absent on every event log item but a batched lottery's rejection, and a getter
    // would turn that absence into an Invalid Date on the toObject/toJSON path
    lastProgramItemEndTime: { type: Date },
    programType: { type: String },
    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);