import { Prop, Schema, SchemaFactory, raw } from '@nestjs/mongoose'; import { HydratedDocument, Types } from 'mongoose'; import { ModerationStatus } from '../../../common/enums/moderation-status.enum'; import { PostType } from '../../../common/enums/post-type.enum'; import { PostVisibility } from '../../../common/enums/post-visibility.enum'; import { ProcessingStatus } from '../../../common/enums/processing-status.enum'; import { buildPostMediaResponse } from '../../../common/utils/post-media-response.util'; import { resolveManagedFileUrl, resolveManagedFileUrlRecord, resolveManagedFileUrlRecords, resolveManagedFileUrls, } from '../../../common/utils/public-url.util'; import { User } from '../../users/schemas/user.schema'; export type PostDocument = HydratedDocument; export type PostMediaVariantSet = { originalUrl: string; lowUrl: string; mediumUrl: string; highUrl: string; }; export type PostImageItem = { url: string; caption: string; altText: string; order: number; }; const mediaVariantSetSchema = raw({ originalUrl: { type: String, default: '' }, lowUrl: { type: String, default: '' }, mediumUrl: { type: String, default: '' }, highUrl: { type: String, default: '' }, }); const imageItemSchema = raw({ url: { type: String, default: '' }, caption: { type: String, default: '' }, altText: { type: String, default: '' }, order: { type: Number, default: 0 }, }); @Schema({ timestamps: true, versionKey: false }) export class Post { @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) authorId!: Types.ObjectId; @Prop({ type: Types.ObjectId, ref: 'Post', default: null, index: true }) repostOfPostId?: Types.ObjectId | null; @Prop({ type: Types.ObjectId, ref: 'Post', default: null, index: true }) quoteOfPostId?: Types.ObjectId | null; @Prop({ default: '', trim: true, maxlength: 2200, required: true }) content!: string; @Prop({ default: '' }) videoUrl!: string; @Prop({ default: '' }) hlsUrl!: string; @Prop({ default: '' }) audioUrl!: string; @Prop({ type: Number, min: 1, max: 7200, default: null }) durationSeconds!: number | null; @Prop({ default: '' }) thumbnailUrl!: string; @Prop({ type: mediaVariantSetSchema, default: null }) thumbnailVariants!: PostMediaVariantSet | null; @Prop({ default: '', trim: true, maxlength: 80 }) style!: string; @Prop({ default: '', trim: true, maxlength: 80 }) maqam!: string; @Prop({ default: '', trim: true, maxlength: 40 }) rhythmSignature!: string; @Prop({ type: [Number], default: [] }) waveformPeaks!: number[]; @Prop({ type: [String], default: [] }) imageUrls!: string[]; @Prop({ type: [imageItemSchema], default: [] }) imageItems!: PostImageItem[]; @Prop({ type: [mediaVariantSetSchema], default: [] }) imageVariants!: PostMediaVariantSet[]; @Prop({ type: [Types.ObjectId], ref: User.name, default: [], index: true }) taggedUserIds!: Types.ObjectId[]; @Prop({ type: [String], default: [] }) mentionUsernames!: string[]; @Prop({ type: [Types.ObjectId], ref: User.name, default: [], index: true }) collaboratorIds!: Types.ObjectId[]; @Prop({ default: '' }) location!: string; @Prop({ type: Number, min: -90, max: 90, default: null }) latitude!: number | null; @Prop({ type: Number, min: -180, max: 180, default: null }) longitude!: number | null; @Prop({ enum: PostType, default: PostType.TEXT, index: true }) postType!: PostType; @Prop({ type: String, enum: Object.values(ProcessingStatus), default: ProcessingStatus.READY, index: true, }) processingStatus!: ProcessingStatus; @Prop({ enum: PostVisibility, default: PostVisibility.PUBLIC, index: true }) visibility!: PostVisibility; @Prop({ default: 0, min: 0 }) likesCount!: number; @Prop({ default: 0, min: 0 }) commentsCount!: number; @Prop({ default: 0, min: 0 }) savesCount!: number; @Prop({ default: 0, min: 0 }) shareCount!: number; @Prop({ default: 0, min: 0 }) viewCount!: number; @Prop({ default: 0, min: 0 }) playCount!: number; @Prop({ default: false, index: true }) commentsDisabled!: boolean; @Prop({ default: false }) commentsFollowersOnly!: boolean; @Prop({ type: [String], default: [] }) commentFilterKeywords!: string[]; @Prop({ default: false, index: true }) pinnedToProfile!: boolean; @Prop({ default: false, index: true }) isArchived!: boolean; @Prop({ type: [String], default: [], index: true }) hashtags!: string[]; @Prop({ type: String, enum: Object.values(ModerationStatus), default: ModerationStatus.ACTIVE, index: true, }) moderationStatus!: ModerationStatus; @Prop({ default: '', maxlength: 300 }) moderationReason!: string; @Prop({ default: false, index: true }) isDeleted!: boolean; @Prop({ type: Date, default: null }) deletedAt?: Date | null; @Prop({ type: Types.ObjectId, ref: User.name, default: null }) deletedBy?: Types.ObjectId | null; } export const PostSchema = SchemaFactory.createForClass(Post); PostSchema.index({ authorId: 1, createdAt: -1 }); PostSchema.index({ repostOfPostId: 1, createdAt: -1 }); PostSchema.index({ quoteOfPostId: 1, createdAt: -1 }); PostSchema.index({ visibility: 1, createdAt: -1 }); PostSchema.index({ postType: 1, createdAt: -1 }); PostSchema.index({ processingStatus: 1, createdAt: -1 }); PostSchema.index({ hashtags: 1, createdAt: -1 }); PostSchema.index({ taggedUserIds: 1, createdAt: -1 }); PostSchema.index({ collaboratorIds: 1, createdAt: -1 }); PostSchema.index({ authorId: 1, pinnedToProfile: -1, createdAt: -1 }); PostSchema.index({ authorId: 1, isArchived: 1, createdAt: -1 }); PostSchema.index({ moderationStatus: 1, createdAt: -1 }); PostSchema.index({ authorId: 1, isDeleted: 1, createdAt: -1 }); PostSchema.index({ visibility: 1, isDeleted: 1, createdAt: -1 }); PostSchema.index({ visibility: 1, isDeleted: 1, likesCount: -1, commentsCount: -1, savesCount: -1, shareCount: -1, viewCount: -1, playCount: -1, createdAt: -1, }); const transformManagedPostFiles = (_doc: unknown, ret: any) => { ret.imageUrls = resolveManagedFileUrls(ret.imageUrls); ret.imageItems = Array.isArray(ret.imageItems) ? ret.imageItems.map((item: PostImageItem) => ({ ...item, url: resolveManagedFileUrl(item.url), })) : []; ret.imageVariants = resolveManagedFileUrlRecords(ret.imageVariants); ret.videoUrl = resolveManagedFileUrl(ret.videoUrl); ret.hlsUrl = resolveManagedFileUrl(ret.hlsUrl); ret.audioUrl = resolveManagedFileUrl(ret.audioUrl); ret.thumbnailUrl = resolveManagedFileUrl(ret.thumbnailUrl); ret.thumbnailVariants = resolveManagedFileUrlRecord(ret.thumbnailVariants); ret.processingStatus = ret.processingStatus ?? ProcessingStatus.READY; ret.media = buildPostMediaResponse(ret); return ret; }; PostSchema.set('toJSON', { transform: transformManagedPostFiles }); PostSchema.set('toObject', { transform: transformManagedPostFiles });