File

src/modules/article/article.model.ts

Index

Properties

Properties

comments
Type : number
Decorators :
@IsInt()
@prop({default: 0})
likes
Type : number
Decorators :
@IsInt()
@prop({default: 0})
views
Type : number
Decorators :
@IsInt()
@prop({default: 0})
import { AutoIncrementID } from '@typegoose/auto-increment'
import { prop, index, plugin, Ref, modelOptions } from '@typegoose/typegoose'
import {
  IsString,
  IsBoolean,
  IsNotEmpty,
  IsArray,
  IsOptional,
  IsDefined,
  IsIn,
  IsInt,
  MaxLength,
  Matches,
  ArrayNotEmpty,
  ArrayUnique
} from 'class-validator'
import { Language, SortType, PublishState, PublicState, OriginState } from '@app/constants/biz.constant'
import { GENERAL_AUTO_INCREMENT_ID_CONFIG } from '@app/constants/increment.constant'
import { getProviderByTypegooseClass } from '@app/transformers/model.transformer'
import { mongoosePaginate } from '@app/utils/paginate'
import { Category } from '@app/modules/category/category.model'
import { KeyValueModel } from '@app/models/key-value.model'
import { Tag } from '@app/modules/tag/tag.model'

export const ARTICLE_LANGUAGES = [Language.English, Language.Chinese, Language.Mixed] as const
export const ARTICLE_PUBLISH_STATES = [PublishState.Draft, PublishState.Published, PublishState.Recycle] as const
export const ARTICLE_PUBLIC_STATES = [PublicState.Public, PublicState.Secret, PublicState.Reserve] as const
export const ARTICLE_ORIGIN_STATES = [OriginState.Original, OriginState.Reprint, OriginState.Hybrid] as const

export const ARTICLE_FULL_QUERY_REF_POPULATE = ['categories', 'tags']
export const ARTICLE_LIST_QUERY_PROJECTION = { content: false }
export const ARTICLE_LIST_QUERY_GUEST_FILTER = Object.freeze({
  state: PublishState.Published,
  public: PublicState.Public
})

export const ARTICLE_HOTTEST_SORT_PARAMS = Object.freeze({
  'meta.comments': SortType.Desc,
  'meta.likes': SortType.Desc
})

const ARTICLE_DEFAULT_META: ArticleMeta = Object.freeze({
  likes: 0,
  views: 0,
  comments: 0
})

export class ArticleMeta {
  @IsInt()
  @prop({ default: 0 })
  likes: number

  @IsInt()
  @prop({ default: 0 })
  views: number

  // MARK: keep comments field manual
  // 1. `.sort()` can't by other model schema
  // https://stackoverflow.com/questions/66174791/how-to-access-a-different-schema-in-a-virtual-method
  // 2. `virtual` can't support publicOnly params and can't access other schema
  @IsInt()
  @prop({ default: 0 })
  comments: number
}

@plugin(mongoosePaginate)
@plugin(AutoIncrementID, GENERAL_AUTO_INCREMENT_ID_CONFIG)
@modelOptions({
  schemaOptions: {
    versionKey: false,
    toObject: { getters: true },
    timestamps: {
      createdAt: 'created_at',
      updatedAt: 'updated_at'
    }
  }
})
@index(
  { title: 'text', content: 'text', description: 'text' },
  {
    name: 'SearchIndex',
    weights: {
      title: 10,
      description: 18,
      content: 3
    }
  }
)
export class Article {
  @prop({ unique: true })
  id: number

  @Matches(/^[a-zA-Z0-9-_]+$/)
  @MaxLength(50)
  @IsString()
  @IsOptional()
  @prop({ default: null, validate: /^[a-zA-Z0-9-_]+$/, index: true })
  slug: string

  @IsString()
  @IsNotEmpty({ message: 'title?' })
  @prop({ required: true, validate: /\S+/, text: true })
  title: string

  @IsString()
  @IsNotEmpty({ message: 'content?' })
  @prop({ required: true, validate: /\S+/, text: true })
  content: string

  @IsString()
  @prop({ default: '', text: true })
  description: string

  @ArrayUnique()
  @IsArray()
  @IsDefined()
  @prop({ default: [], type: () => [String] })
  keywords: string[]

  @IsString()
  @IsOptional()
  @prop({ type: String, default: null })
  thumbnail: string | null

  // publish state
  @IsIn(ARTICLE_PUBLISH_STATES)
  @IsInt()
  @IsDefined()
  @prop({ enum: PublishState, default: PublishState.Published, index: true })
  state: PublishState

  // public state
  @IsIn(ARTICLE_PUBLIC_STATES)
  @IsInt()
  @IsDefined()
  @prop({ enum: PublicState, default: PublicState.Public, index: true })
  public: PublicState

  // origin state
  @IsIn(ARTICLE_ORIGIN_STATES)
  @IsInt()
  @IsDefined()
  @prop({ enum: OriginState, default: OriginState.Original, index: true })
  origin: OriginState

  // category
  @ArrayUnique()
  @ArrayNotEmpty()
  @IsArray()
  @prop({ ref: () => Category, required: true, index: true })
  categories: Ref<Category>[]

  // tag
  // https://typegoose.github.io/typegoose/docs/api/virtuals#virtual-populate
  @prop({ ref: () => Tag, index: true })
  tags: Ref<Tag>[]

  // language
  // MARK: can't use 'language' field
  // https://docs.mongodb.com/manual/tutorial/specify-language-for-text-index/
  // https://docs.mongodb.com/manual/reference/text-search-languages/#std-label-text-search-languages
  @IsIn(ARTICLE_LANGUAGES)
  @IsString()
  @IsDefined()
  @prop({ default: Language.Chinese, index: true })
  lang: Language

  // featured
  @IsBoolean()
  @prop({ default: false, index: true })
  featured: boolean

  // disabled comments
  @IsBoolean()
  @prop({ default: false })
  disabled_comments: boolean

  @prop({ _id: false, default: { ...ARTICLE_DEFAULT_META } })
  meta: ArticleMeta

  @prop({ default: Date.now, index: true, immutable: true })
  created_at?: Date

  @prop({ default: Date.now })
  updated_at?: Date

  @ArrayUnique()
  @IsArray()
  @prop({ _id: false, default: [], type: () => [KeyValueModel] })
  extends: KeyValueModel[]
}

export const ArticleProvider = getProviderByTypegooseClass(Article)

results matching ""

    No results matching ""