diff --git a/src/core/accounts/dtos/account.dto.ts b/src/core/accounts/dtos/account.dto.ts new file mode 100644 index 0000000..360e320 --- /dev/null +++ b/src/core/accounts/dtos/account.dto.ts @@ -0,0 +1,49 @@ +import { Expose, Type } from 'class-transformer'; +import { IsDefined } from 'class-validator'; +import { GroupDto } from 'src/core/groups/dtos'; +import { AccountBasicDto, SystemStatus } from 'src/libs/shared'; +import { RoleDto } from 'src/core/role/dtos/role.dto'; + +export class UserAccountDto extends AccountBasicDto { + @Expose() + firstName?: string; + + @Expose() + lastName?: string; + + @Expose() + email!: string; + + @Expose() + @Type(() => RoleDto) + role!: RoleDto[]; + + @Expose() + @Type(() => GroupDto) + groups!: GroupDto[]; + + @Expose() + @IsDefined() + status!: SystemStatus; + + get fullName(): string { + const fullName = [this.firstName, this.lastName].filter(Boolean).join(' '); + if (fullName) { + return fullName; + } else { + return this.email; + } + } + + get initials(): string { + if (this.firstName && this.lastName) { + return `${this.firstName[0].toUpperCase()}${this.lastName[0].toUpperCase()}`; + } else if (this.firstName) { + return `${this.firstName[0].toUpperCase()}`; + } else if (this.lastName) { + return `${this.lastName[0].toUpperCase()}`; + } else { + return ''; + } + } +} diff --git a/src/core/accounts/dtos/index.ts b/src/core/accounts/dtos/index.ts new file mode 100644 index 0000000..c8ff8ee --- /dev/null +++ b/src/core/accounts/dtos/index.ts @@ -0,0 +1 @@ +export * from './account.dto'; diff --git a/src/core/groups/dtos/create-group.dto.ts b/src/core/groups/dtos/create-group.dto.ts new file mode 100644 index 0000000..b6ac2e3 --- /dev/null +++ b/src/core/groups/dtos/create-group.dto.ts @@ -0,0 +1,29 @@ +import { Optional } from '@nestjs/common'; +import { Expose } from 'class-transformer'; +import { IsNotEmpty, IsString, MaxLength } from 'class-validator'; +import { FieldConstraints } from 'src/libs/shared'; + +export class CreateGroupDto { + @Expose() + @IsString() + @IsNotEmpty() + @MaxLength(FieldConstraints.FIRST_NAME.MAX_LENGTH) + name!: string; + + @Expose() + @Optional() + @IsString() + @MaxLength(FieldConstraints.COURSE_NAME.MAX_LENGTH) + courseName?: string; + + @Expose() + @Optional() + @IsString() + @MaxLength(FieldConstraints.DESCRIPTION.MAX_LENGTH) + description?: string; + + @Expose() + @IsString() + @IsNotEmpty() + avatar!: string; +} diff --git a/src/core/groups/dtos/group.dto.ts b/src/core/groups/dtos/group.dto.ts new file mode 100644 index 0000000..a00f042 --- /dev/null +++ b/src/core/groups/dtos/group.dto.ts @@ -0,0 +1,24 @@ +import { Expose } from 'class-transformer'; +import { IsDefined } from 'class-validator'; +import { SystemStatus } from 'src/libs/shared'; + +export class GroupDto { + @Expose() + name!: string; + + @Expose() + courseName?: string; + + @Expose() + description?: string; + + @Expose() + avatar!: string; + + @Expose() + code!: string; + + @IsDefined() + @Expose() + status!: SystemStatus; +} diff --git a/src/core/groups/dtos/index.ts b/src/core/groups/dtos/index.ts new file mode 100644 index 0000000..7a00ef4 --- /dev/null +++ b/src/core/groups/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './create-group.dto'; +export * from './join-group.dto'; +export * from './group.dto'; diff --git a/src/core/groups/dtos/join-group.dto.ts b/src/core/groups/dtos/join-group.dto.ts new file mode 100644 index 0000000..7522847 --- /dev/null +++ b/src/core/groups/dtos/join-group.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsString, Matches, MaxLength } from 'class-validator'; + +export class JoinGroupDto { + @IsString() + @IsNotEmpty() + userId!: string; + //TODO: change match & length decorator to use appfield contraints + @IsString() + @IsNotEmpty() + @MaxLength(20) + @Matches(/^[aA-z0-9-]+$/) + code!: string; +} diff --git a/src/core/role/dtos/index.ts b/src/core/role/dtos/index.ts new file mode 100644 index 0000000..985a22d --- /dev/null +++ b/src/core/role/dtos/index.ts @@ -0,0 +1 @@ +export * from './role.dto'; diff --git a/src/core/role/dtos/role.dto.ts b/src/core/role/dtos/role.dto.ts new file mode 100644 index 0000000..9bbd7b7 --- /dev/null +++ b/src/core/role/dtos/role.dto.ts @@ -0,0 +1,27 @@ +import { Expose, Type } from 'class-transformer'; +import { IsDefined } from 'class-validator'; +import { Action, BaseDto, Subject, SystemStatus } from 'src/libs/shared'; + +export class PermissionRule { + @Expose() + action!: Action; + + @Expose() + subject!: Subject; +} + +export class RoleDto extends BaseDto { + @Expose() + name!: string; + + @Expose() + code!: string; + + @Expose() + @Type(() => PermissionRule) + permissions!: PermissionRule[]; + + @Expose() + @IsDefined() + status!: SystemStatus; +} diff --git a/src/libs/internal/index.ts b/src/libs/internal/index.ts deleted file mode 100644 index 93f8032..0000000 --- a/src/libs/internal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './filters'; -export * from './helpers'; diff --git a/src/libs/shared/consts/fields-contraints.const.ts b/src/libs/shared/consts/fields-contraints.const.ts index a784d76..926a9e7 100644 --- a/src/libs/shared/consts/fields-contraints.const.ts +++ b/src/libs/shared/consts/fields-contraints.const.ts @@ -7,7 +7,6 @@ export class FieldConstraints { }; static readonly USERNAME = { MAX_LENGTH: 50, - PATTERN: /^[a-zA-Z0-9-_]+$/, }; static readonly CODE = { MAX_LENGTH: 50, @@ -22,10 +21,12 @@ export class FieldConstraints { static readonly SUBJECT = { MAX_LENGTH: 30, }; - static readonly EMAIL = { - MAX_LENGTH: 120, + + static readonly COURSE_NAME = { + MAX_LENGTH: 50, }; - static readonly CONTENT = { - PATTERN: /^[a-zA-Z0-9]{6}$/, + + static readonly DESCRIPTION = { + MAX_LENGTH: 200, }; } diff --git a/src/libs/shared/dtos/account-basic.dto.ts b/src/libs/shared/dtos/account-basic.dto.ts new file mode 100644 index 0000000..3ee9ec3 --- /dev/null +++ b/src/libs/shared/dtos/account-basic.dto.ts @@ -0,0 +1,14 @@ +import { Expose } from 'class-transformer'; +import { BasicDto } from './basic.dto'; + +export abstract class AccountBasicDto extends BasicDto { + @Expose() + username!: string; + + @Expose() + avatarUrl?: string; + + abstract fullName: string; + + abstract initials: string; +} diff --git a/src/libs/shared/dtos/base.dto.ts b/src/libs/shared/dtos/base.dto.ts new file mode 100644 index 0000000..1e02d4a --- /dev/null +++ b/src/libs/shared/dtos/base.dto.ts @@ -0,0 +1,31 @@ +import { Exclude, Expose, Transform, Type } from 'class-transformer'; +import { BasicDto } from './basic.dto'; +import { AccountBasicDto } from './account-basic.dto'; +@Exclude() +export abstract class BaseDto extends BasicDto { + @Expose() + @Transform( + ({ value, obj }) => { + if (obj.createdBy) { + return value; + } else { + return undefined; + } + }, + { toClassOnly: true }, + ) + @Type(() => Date) + createdAt?: Date; + + @Expose() + @Type(() => Date) + updatedAt!: Date; + + @Expose() + @Type(() => AccountBasicDto) + createdBy?: AccountBasicDto; + + @Expose() + @Type(() => AccountBasicDto) + updatedBy!: AccountBasicDto; +} diff --git a/src/libs/shared/dtos/basic.dto.ts b/src/libs/shared/dtos/basic.dto.ts new file mode 100644 index 0000000..b395487 --- /dev/null +++ b/src/libs/shared/dtos/basic.dto.ts @@ -0,0 +1,18 @@ +import { Exclude, Expose, Transform } from 'class-transformer'; +@Exclude() +export abstract class BasicDto { + @Expose() + @Transform( + ({ value, obj }) => { + if (value && typeof value === 'string') { + return value; + } else if (obj._id) { + return typeof obj._id === 'string' ? obj._id : obj._id.toString(); + } else { + return undefined; + } + }, + { toClassOnly: true }, + ) + id!: string; +} diff --git a/src/libs/shared/dtos/index.ts b/src/libs/shared/dtos/index.ts new file mode 100644 index 0000000..c350536 --- /dev/null +++ b/src/libs/shared/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './base.dto'; +export * from './basic.dto'; +export * from './account-basic.dto'; diff --git a/src/libs/shared/enums/event-type.enum.ts b/src/libs/shared/enums/event-type.enum.ts index 10a5db9..484bbb3 100644 --- a/src/libs/shared/enums/event-type.enum.ts +++ b/src/libs/shared/enums/event-type.enum.ts @@ -1,9 +1,9 @@ export enum EventType { - ZAJĘCIA = 'Zajęcia', - SPOTKANIE = 'Spotkanie', - EGZAMIN = 'Egzamin', - KOLOKWIUM = 'Kolokwium', - ODWOLANIE_ZAJEC = 'Odwołanie zajęć', - POPRAWKA = 'Poprawka', - INNE = 'Inne', + CLASSES = 'Zajęcia', + MEETING = 'Spotkanie', + EXAM = 'Egzamin', + QUIZ = 'Kolokwium', + CLASS_CANCELLATION = 'Odwołanie zajęć', + RETAKE = 'Poprawka', + OTHER = 'Inne', } diff --git a/src/libs/shared/index.ts b/src/libs/shared/index.ts index 951ed57..2ca5949 100644 --- a/src/libs/shared/index.ts +++ b/src/libs/shared/index.ts @@ -1,2 +1,3 @@ export * from './consts'; export * from './enums'; +export * from './dtos'; diff --git a/src/models/announcement.model.ts b/src/models/announcement.model.ts index 314b78a..ab60413 100644 --- a/src/models/announcement.model.ts +++ b/src/models/announcement.model.ts @@ -1,25 +1,8 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import { BaseClass } from './base.model'; +import { BaseClass } from './base-class.model'; import { AnnouncementTargetType } from 'src/libs/shared'; -export type AnnouncementTargetDocument = AnnouncementTarget & Document; -export class AnnouncementTarget { - @Prop({ - required: true, - lowercase: true, - trim: true, - enum: AnnouncementTargetType, - }) - type!: AnnouncementTargetType; - - @Prop({ default: [], required: true, type: [String], select: false }) - value!: string[]; -} - -export const AnnouncementTargetSchema = - SchemaFactory.createForClass(AnnouncementTarget); - export type AnnouncementDocument = Announcement & Document; @Schema() @@ -36,8 +19,23 @@ export class Announcement extends BaseClass { @Prop({ required: true }) endDate!: Date; - @Prop({ required: true }) - target: AnnouncementTarget; + @Prop({ required: true, enum: AnnouncementTargetType }) + targetType!: AnnouncementTargetType; + + @Prop({ required: false, type: [String], select: false }) // Optional target value (array) + targetValue?: string[]; + + @Prop({ + required: true, + type: String, + ref: 'User', + readonly: true, + select: false, + }) + createdBy!: string; + + @Prop({ required: true, type: String, ref: 'User' }) + updatedBy!: string; @Prop({ required: false }) expiresAt?: Date; diff --git a/src/models/app-permissions.model.ts b/src/models/app-permissions.model.ts new file mode 100644 index 0000000..f7e964d --- /dev/null +++ b/src/models/app-permissions.model.ts @@ -0,0 +1,29 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; +import { Action, Subject, FieldConstraints } from 'src/libs/shared'; + +export type AppPermissionsDocument = AppPermissions & Document; + +@Schema() +export class AppPermissions { + @Prop({ + required: true, + lowercase: true, + trim: true, + enum: Action, + maxlength: FieldConstraints.ACTION.MAX_LENGTH, + }) + action!: Action; + + @Prop({ + required: true, + lowercase: true, + trim: true, + enum: Subject, + maxlength: FieldConstraints.SUBJECT.MAX_LENGTH, + }) + subject!: Subject; +} + +export const AppPermissionsSchema = + SchemaFactory.createForClass(AppPermissions); diff --git a/src/models/base-class.model.ts b/src/models/base-class.model.ts new file mode 100644 index 0000000..0be56f0 --- /dev/null +++ b/src/models/base-class.model.ts @@ -0,0 +1,21 @@ +import { Prop } from '@nestjs/mongoose'; +import { SystemStatus } from 'src/libs/shared'; +import { v4 as uuid } from 'uuid'; + +export abstract class BaseClass { + @Prop({ default: uuid, readonly: true, select: false }) + _id!: string; + + get id(): string { + return this._id; + } + + @Prop({ required: true, default: SystemStatus.ACTIVE, enum: SystemStatus }) + status!: SystemStatus; + + @Prop({ required: true, default: Date.now, readonly: true }) + createdAt!: Date; + + @Prop({ required: true, default: Date.now }) + updatedAt!: Date; +} diff --git a/src/models/event.model.ts b/src/models/event.model.ts index 3b2c96a..654da64 100644 --- a/src/models/event.model.ts +++ b/src/models/event.model.ts @@ -1,7 +1,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; import { EventType } from 'src/libs/shared'; -import { BaseClass } from './base.model'; +import { BaseClass } from './base-class.model'; export type EventDocument = Event & Document; @@ -19,17 +19,29 @@ export class Event extends BaseClass { @Prop({ required: true }) endDate!: Date; + @Prop({ required: true }) + startTime!: string; + + @Prop({ required: true }) + endTime!: string; + + @Prop({ required: true, type: [String], ref: 'Group', select: false }) + groups!: string[]; + + @Prop({ required: true, enum: EventType }) + type!: EventType; + @Prop({ - default: [], required: true, - type: [String], - ref: 'Group', + type: String, + ref: 'User', + readonly: true, select: false, }) - groups!: string[]; + createdBy!: string; - @Prop({ required: true, enum: EventType }) - type!: EventType; + @Prop({ required: true, type: String, ref: 'User' }) + updatedBy!: string; @Prop({ required: true, type: String, ref: 'Role' }) role!: string; diff --git a/src/models/group.model.ts b/src/models/group.model.ts index c64a965..a9b2390 100644 --- a/src/models/group.model.ts +++ b/src/models/group.model.ts @@ -1,7 +1,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import { BaseClass } from './base.model'; +import { BaseClass } from './base-class.model'; import { FieldConstraints, VerificationStatus } from 'src/libs/shared'; export type GroupDocument = Group & Document; @@ -25,10 +25,10 @@ export class Group extends BaseClass { @Prop({ required: true }) avatarUrl!: string; - @Prop({ required: false }) + @Prop({ required: false }) // Optional course name courseName?: string; - @Prop({ required: false }) + @Prop({ required: false }) // Optional description description?: string; @Prop({ @@ -38,29 +38,35 @@ export class Group extends BaseClass { }) verificationStatus!: VerificationStatus; - @Prop({ required: false }) + @Prop({ required: false }) // Optional rejection reason rejectionReason?: string; - @Prop({ required: true, type: String, ref: 'UserAccount' }) + @Prop({ required: true, type: String, ref: 'User' }) president!: string; + @Prop({ required: true, type: [String], select: false }) + codes!: string[]; + @Prop({ - default: [], required: true, + default: [], type: [String], + ref: 'User', select: false, - ref: 'JoinCode', }) - codes!: string[]; + members!: string[]; @Prop({ required: true, - default: [], - type: [String], - ref: 'UserAccount', + type: String, + ref: 'User', + readonly: true, select: false, }) - members!: string[]; + createdBy!: string; + + @Prop({ required: true, type: String, ref: 'User' }) + updatedBy: string; } export const GroupSchema = SchemaFactory.createForClass(Group); diff --git a/src/models/index.ts b/src/models/index.ts index efd6274..bfd40bc 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,12 +1,14 @@ -import { Announcement, AnnouncementTarget } from './announcement.model'; +import { Announcement } from './announcement.model'; +import { AppPermissions } from './app-permissions.model'; import { Event } from './event.model'; import { Group } from './group.model'; import { JoinCode } from './join-code.model'; -import { Role, AppPermissions } from './role.model'; +import { Role } from './role.model'; import { UserAccount } from './user-account.model'; export * from './announcement.model'; -export * from './basic.model'; +export * from './app-permissions.model'; +export * from './base-class.model'; export * from './event.model'; export * from './group.model'; export * from './join-code.model'; @@ -19,14 +21,10 @@ export const MongooseModels = [ schema: Announcement, collection: 'announcements', }, - { - name: AnnouncementTarget.name, - schema: AnnouncementTarget, - collection: 'announcementTargets', - }, { name: AppPermissions.name, schema: AppPermissions, + collection: 'appPermissions', }, { name: Event.name, diff --git a/src/models/join-code.model.ts b/src/models/join-code.model.ts index 7072ba3..00f30be 100644 --- a/src/models/join-code.model.ts +++ b/src/models/join-code.model.ts @@ -1,19 +1,22 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import { BaseClass } from './base.model'; -import { FieldConstraints } from 'src/libs/shared'; +import { BaseClass } from './base-class.model'; export type JoinCodeDocument = JoinCode & Document; -@Schema({ _id: false }) +@Schema() export class JoinCode extends BaseClass { @Prop({ required: true, type: String, - unique: true, - match: FieldConstraints.CONTENT.PATTERN, + ref: 'User', + readonly: true, + select: false, }) - content!: string; + createdBy!: string; + + @Prop({ required: true, type: String, ref: 'User' }) + updatedBy!: string; @Prop({ required: true, type: String, ref: 'Role' }) role!: string; @@ -32,7 +35,3 @@ export class JoinCode extends BaseClass { } export const JoinCodeSchema = SchemaFactory.createForClass(JoinCode); - -JoinCodeSchema.virtual('_id').get(function () { - return this.content; -}); diff --git a/src/models/role.model.ts b/src/models/role.model.ts index 62d6637..e6ee819 100644 --- a/src/models/role.model.ts +++ b/src/models/role.model.ts @@ -1,32 +1,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import { BaseClass } from './base.model'; -import { Action, FieldConstraints, Subject } from 'src/libs/shared'; - -export type AppPermissionsDocument = AppPermissions & Document; -@Schema() -export class AppPermissions { - @Prop({ - required: true, - lowercase: true, - trim: true, - enum: Action, - maxlength: FieldConstraints.ACTION.MAX_LENGTH, - }) - action!: Action; - - @Prop({ - required: true, - lowercase: true, - trim: true, - enum: Subject, - maxlength: FieldConstraints.SUBJECT.MAX_LENGTH, - }) - subject!: Subject; -} - -export const AppPermissionsSchema = - SchemaFactory.createForClass(AppPermissions); +import { BaseClass } from './base-class.model'; +import { FieldConstraints } from 'src/libs/shared'; export type RoleDocument = Role & Document; @@ -47,13 +22,26 @@ export class Role extends BaseClass { }) name!: string; + @Prop({ + required: true, + type: String, + ref: 'User', + readonly: true, + select: false, + }) + createdBy!: string; + + @Prop({ required: true, type: String, ref: 'User' }) + updatedBy!: string; + @Prop({ required: true, default: [], - type: [AppPermissions], + type: String, + ref: 'AppPermissions', select: false, }) - permissions!: AppPermissions[]; + permissions!: string[]; } export const RoleSchema = SchemaFactory.createForClass(Role); diff --git a/src/models/user-account.model.ts b/src/models/user-account.model.ts index 96580bc..3888bea 100644 --- a/src/models/user-account.model.ts +++ b/src/models/user-account.model.ts @@ -1,19 +1,18 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; import { FieldConstraints } from 'src/libs/shared'; -import { BasicAccount } from './basic-account.model'; +import { BaseClass } from './base-class.model'; export type UserDocument = UserAccount & Document; @Schema() -export class UserAccount extends BasicAccount { +export class UserAccount extends BaseClass { @Prop({ required: true, unique: true, trim: true, sparse: true, maxlength: FieldConstraints.USERNAME.MAX_LENGTH, - match: FieldConstraints.USERNAME.PATTERN, }) username!: string; @@ -29,15 +28,29 @@ export class UserAccount extends BasicAccount { }) lastName?: string; - @Prop({ - required: true, - unique: true, - maxlength: FieldConstraints.EMAIL.MAX_LENGTH, - }) + @Prop({ required: false }) + avatarUrl?: string; + + @Prop({ required: true, unique: true }) email!: string; + @Prop({ required: true, default: [], type: [String], ref: 'Role' }) + role!: string[]; + @Prop({ required: true, default: [], type: [String], ref: 'Group' }) groups!: string[]; + + @Prop({ + required: true, + type: String, + ref: 'User', + readonly: true, + select: false, + }) + createdBy!: string; + + @Prop({ required: true, type: String, ref: 'User' }) + updatedBy!: string; } export const UserAccountSchema = SchemaFactory.createForClass(UserAccount); diff --git a/src/roles/dtos/role.dto.ts b/src/roles/dtos/role.dto.ts new file mode 100644 index 0000000..9bbd7b7 --- /dev/null +++ b/src/roles/dtos/role.dto.ts @@ -0,0 +1,27 @@ +import { Expose, Type } from 'class-transformer'; +import { IsDefined } from 'class-validator'; +import { Action, BaseDto, Subject, SystemStatus } from 'src/libs/shared'; + +export class PermissionRule { + @Expose() + action!: Action; + + @Expose() + subject!: Subject; +} + +export class RoleDto extends BaseDto { + @Expose() + name!: string; + + @Expose() + code!: string; + + @Expose() + @Type(() => PermissionRule) + permissions!: PermissionRule[]; + + @Expose() + @IsDefined() + status!: SystemStatus; +}