diff --git a/apps/catalog/src/catalog.module.ts b/apps/catalog/src/catalog.module.ts index 32b8554..d53756f 100644 --- a/apps/catalog/src/catalog.module.ts +++ b/apps/catalog/src/catalog.module.ts @@ -1,10 +1,24 @@ -import { Module } from '@nestjs/common'; +import { Module, OnModuleInit } from '@nestjs/common'; import { CatalogController } from './catalog.controller'; import { CatalogService } from './catalog.service'; import { ClientsModule } from '@nestjs/microservices'; import { makeKnex, makeTypeOrm, natsClient } from '@freeblox/shared'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; +import knex from 'knex'; +import { ContentEntity } from './database/entities/content.entity'; +import { ContentRevisionEntity } from './database/entities/content-revision.entity'; +import { + ContentModerationEntity, + ContentModerationBanEntity, +} from './database/entities/content-moderation.entity'; +import { ContentAssetEntity } from './database/entities/content-asset.entity'; +import { ContentPriceEntity } from './database/entities/content-price.entity'; +import { ContentOwnershipEntity } from './database/entities/content-ownership.entity'; +import { ContentTradeEntity } from './database/entities/content-trade.entity'; +import { ContentFavoriteEntity } from './database/entities/content-favorite.entity'; +import { ContentVoteEntity } from './database/entities/content-vote.entity'; +import { ContentReportEntity } from './database/entities/content-report.entity'; @Module({ imports: [ @@ -17,6 +31,19 @@ import { TypeOrmModule } from '@nestjs/typeorm'; inject: [ConfigService], useFactory: (config: ConfigService) => config.get('typeorm'), }), + TypeOrmModule.forFeature([ + ContentEntity, + ContentRevisionEntity, + ContentModerationEntity, + ContentModerationBanEntity, + ContentAssetEntity, + ContentPriceEntity, + ContentOwnershipEntity, + ContentTradeEntity, + ContentFavoriteEntity, + ContentVoteEntity, + ContentReportEntity, + ]), ClientsModule.register([ natsClient('catalog'), natsClient('auth'), @@ -26,4 +53,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; controllers: [CatalogController], providers: [CatalogService], }) -export class CatalogModule {} +export class CatalogModule implements OnModuleInit { + constructor(private readonly config: ConfigService) {} + + async onModuleInit() { + const knexInstance = knex(this.config.get('knex')); + await knexInstance.migrate.latest(); + // await knexInstance.seed.run(); + } +} diff --git a/apps/catalog/src/database/entities/content-asset.entity.ts b/apps/catalog/src/database/entities/content-asset.entity.ts new file mode 100644 index 0000000..4f58009 --- /dev/null +++ b/apps/catalog/src/database/entities/content-asset.entity.ts @@ -0,0 +1,44 @@ +import { Expose } from 'class-transformer'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ContentAssetType } from '../../enums/content-asset-type.enum'; +import { ContentRevisionEntity } from './content-revision.entity'; +import { ContentEntity } from './content.entity'; + +@Entity('content_asset') +@Expose() +export class ContentAssetEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'content_id' }) + contentId: number; + + @Column({ name: 'revision_id' }) + revisionId: number; + + @Column({ name: 'asset_id', type: 'uuid' }) + assetId: string; + + @Column({ type: 'string', enum: ContentAssetType }) + type: ContentAssetType; + + @Column({ name: 'type_name', nullable: true }) + typeName?: string; + + @Column() + index: number; + + @ManyToOne(() => ContentEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'content_id' }) + content: ContentEntity; + + @ManyToOne(() => ContentRevisionEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'revision_id' }) + revision: ContentRevisionEntity; +} diff --git a/apps/catalog/src/database/entities/content-favorite.entity.ts b/apps/catalog/src/database/entities/content-favorite.entity.ts new file mode 100644 index 0000000..4d5a93f --- /dev/null +++ b/apps/catalog/src/database/entities/content-favorite.entity.ts @@ -0,0 +1,31 @@ +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ContentEntity } from './content.entity'; +import { MetaEntity } from '@freeblox/shared'; + +@Entity('content_favorite') +@Expose() +export class ContentFavoriteEntity extends MetaEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'content_id' }) + @Exclude() + contentId: number; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + @Index() + @Exclude() + userId: string; + + @ManyToOne(() => ContentEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'content_id' }) + content: ContentEntity; +} diff --git a/apps/catalog/src/database/entities/content-moderation.entity.ts b/apps/catalog/src/database/entities/content-moderation.entity.ts new file mode 100644 index 0000000..16404bc --- /dev/null +++ b/apps/catalog/src/database/entities/content-moderation.entity.ts @@ -0,0 +1,107 @@ +import { MetaEntity } from '@freeblox/shared'; +import { Exclude, Expose, Type } from 'class-transformer'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ContentEntity } from './content.entity'; +import { ContentRevisionEntity } from './content-revision.entity'; +import { ModeratorAction } from '../../enums/moderation-action.enum'; +import { RejectionReason } from '../../enums/rejection-reason.enum'; +import { + IsBoolean, + IsEnum, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; +import { BanEntity } from 'apps/auth/src/database/entities/ban.entity'; + +@Entity('content_moderation') +@Expose() +export class ContentModerationEntity extends MetaEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'content_id' }) + @IsNumber() + contentId: number; + + @Column({ name: 'revision_id' }) + @IsNumber() + revisionId: number; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + @Exclude() + userId: string; + + @Column({ + type: 'enum', + enum: ModeratorAction, + default: ModeratorAction.PENDING, + }) + @IsEnum(ModeratorAction) + action: ModeratorAction; + + @Column({ + type: 'enum', + enum: RejectionReason, + nullable: true, + name: 'rejection_reason', + }) + @IsEnum(RejectionReason) + rejectionReason?: RejectionReason; + + @Column({ nullable: true }) + @IsString() + @IsOptional() + description?: string; + + @Column({ default: false }) + @IsBoolean() + @IsOptional() + penalty: boolean; + + @Column({ default: false, name: 'asset_delete' }) + @IsBoolean() + @IsOptional() + assetDelete: boolean; + + @CreateDateColumn({ name: 'created_at' }) + @Expose() + createdAt: Date; + + @CreateDateColumn({ name: 'decided_at' }) + decidedAt: Date; + + @ManyToOne(() => ContentEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'content_id' }) + content: ContentEntity; + + @ManyToOne(() => ContentRevisionEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'revision_id' }) + revision: ContentRevisionEntity; + + @OneToMany(() => ContentModerationBanEntity, (ban) => ban.moderation) + @Type(() => ContentModerationBanEntity) + bans?: ContentModerationBanEntity[]; +} + +@Entity('content_moderation_ban') +export class ContentModerationBanEntity { + @Column({ nullable: true, name: 'ban_id' }) + banId: number; + + @ManyToOne(() => ContentModerationEntity, (mod) => mod.bans) + @JoinColumn({ name: 'content_moderation_id' }) + @Expose() + moderation: ContentModerationEntity; + + @Expose() + ban?: BanEntity; +} diff --git a/apps/catalog/src/database/entities/content-ownership.entity.ts b/apps/catalog/src/database/entities/content-ownership.entity.ts new file mode 100644 index 0000000..c67818b --- /dev/null +++ b/apps/catalog/src/database/entities/content-ownership.entity.ts @@ -0,0 +1,82 @@ +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ContentEntity } from './content.entity'; +import { Currency } from '../../enums/currency.enum'; +import { UserMetaEntity } from '@freeblox/shared'; + +@Entity('content_ownership') +@Expose() +export class ContentOwnershipEntity extends UserMetaEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'content_id' }) + contentId: number; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + @Exclude() + userId: string; + + @Column({ name: 'previous_ownership_id', nullable: true }) + @Exclude() + previousOwnershipId: string; + + @Column({ name: 'purchase_price', nullable: true }) + @Index() + purchasePrice: number; + + @Column({ + type: 'enum', + enum: Currency, + name: 'purchase_currency', + nullable: true, + }) + @Index() + purchaseCurrency: Currency; + + @Column({ nullable: true }) + @Index() + serial: number; + + @ManyToOne(() => ContentEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'content_id' }) + @Exclude() + content: ContentEntity; + + @ManyToOne(() => ContentOwnershipEntity, { onDelete: 'SET NULL' }) + @JoinColumn({ name: 'previous_ownership_id' }) + @Exclude() + previous?: ContentOwnershipEntity; + + @CreateDateColumn({ name: 'created_at' }) + @Expose() + createdAt: Date; + + @Column({ type: Date, name: 'ended_at', nullable: true }) + endedAt: Date; + + @Column({ type: Date, name: 'expires_at', nullable: true }) + expiresAt: Date; + + /** + * is ownership expired + */ + get expired() { + return this.expiresAt.getTime() < Date.now(); + } + + /** + * is ownership invalid (Expired or ended) + */ + get invalid() { + return !!this.endedAt || this.expired; + } +} diff --git a/apps/catalog/src/database/entities/content-price.entity.ts b/apps/catalog/src/database/entities/content-price.entity.ts new file mode 100644 index 0000000..624b5a7 --- /dev/null +++ b/apps/catalog/src/database/entities/content-price.entity.ts @@ -0,0 +1,35 @@ +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ContentEntity } from './content.entity'; +import { Currency } from '../../enums/currency.enum'; +import { UserMetaEntity } from '@freeblox/shared'; + +@Entity('content_price') +@Expose() +export class ContentPriceEntity extends UserMetaEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'content_id' }) + contentId: number; + + @Column() + @Index() + price: number; + + @Column({ type: 'enum', enum: Currency }) + @Index() + currency: Currency; + + @ManyToOne(() => ContentEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'content_id' }) + @Exclude() + content: ContentEntity; +} diff --git a/apps/catalog/src/database/entities/content-report.entity.ts b/apps/catalog/src/database/entities/content-report.entity.ts new file mode 100644 index 0000000..4601a69 --- /dev/null +++ b/apps/catalog/src/database/entities/content-report.entity.ts @@ -0,0 +1,82 @@ +import { MetaEntity } from '@freeblox/shared'; +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ContentEntity } from './content.entity'; +import { ContentRevisionEntity } from './content-revision.entity'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { ReportStatus } from '../../enums/report-status.enum'; +import { ContentModerationEntity } from './content-moderation.entity'; + +@Entity('content_report') +@Expose() +export class ContentReportEntity extends MetaEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'content_id' }) + @IsNumber() + contentId: number; + + @Column({ name: 'revision_id' }) + revisionId: number; + + @Column({ name: 'user_id', type: 'uuid' }) + @Exclude() + userId: string; + + @Column({ name: 'moderator_id', type: 'uuid', nullable: true }) + @Exclude() + moderatorId?: string; + + @Column() + @IsString() + reason: string; + + @Column() + @IsString() + description: string; + + @Column({ nullable: true }) + @IsString() + @IsOptional() + notes?: string; + + @Column({ type: 'enum', enum: ReportStatus }) + status: ReportStatus; + + @CreateDateColumn({ name: 'created_at' }) + @Expose() + createdAt: Date; + + @CreateDateColumn({ name: 'resolved_at' }) + resolvedAt: Date; + + @ManyToOne(() => ContentEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'content_id' }) + content: ContentEntity; + + @ManyToOne(() => ContentRevisionEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'revision_id' }) + revision?: ContentRevisionEntity; + + @ManyToMany(() => ContentModerationEntity) + @JoinTable({ + name: 'content_report_action', + joinColumn: { + name: 'content_report_id', + }, + inverseJoinColumn: { + name: 'content_moderation_id', + }, + }) + actions?: ContentModerationEntity[]; +} diff --git a/apps/catalog/src/database/entities/content-revision.entity.ts b/apps/catalog/src/database/entities/content-revision.entity.ts new file mode 100644 index 0000000..edc0176 --- /dev/null +++ b/apps/catalog/src/database/entities/content-revision.entity.ts @@ -0,0 +1,35 @@ +import { UserMetaEntity } from '@freeblox/shared'; +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ContentEntity } from './content.entity'; + +@Entity('content_revision') +@Exclude() +export class ContentRevisionEntity extends UserMetaEntity { + @PrimaryGeneratedColumn() + @Expose() + id: number; + + @Column({ name: 'content_id' }) + contentId: number; + + @CreateDateColumn({ name: 'created_at' }) + @Expose() + createdAt: Date; + + @DeleteDateColumn({ name: 'deleted_at' }) + @Expose() + deletedAt?: Date; + + @ManyToOne(() => ContentEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'content_id' }) + content: ContentEntity; +} diff --git a/apps/catalog/src/database/entities/content-trade.entity.ts b/apps/catalog/src/database/entities/content-trade.entity.ts new file mode 100644 index 0000000..63baa9f --- /dev/null +++ b/apps/catalog/src/database/entities/content-trade.entity.ts @@ -0,0 +1,58 @@ +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + Entity, + Index, + JoinTable, + ManyToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { MetaEntity } from '@freeblox/shared'; +import { TradeStatus } from '../../enums/trade-status.enum'; +import { ContentOwnershipEntity } from './content-ownership.entity'; + +@Entity('content_trade') +@Expose() +export class ContentTradeEntity extends MetaEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'user_id', type: 'uuid' }) + @Exclude() + userId: string; + + @Column({ name: 'recipient_id', type: 'uuid' }) + @Exclude() + recipientId: string; + + @Column({ type: 'enum', enum: TradeStatus }) + @Index() + status: TradeStatus; + + @Column() + description: string; + + @ManyToMany(() => ContentOwnershipEntity) + @JoinTable({ + name: 'content_trade_user_content', + joinColumn: { + name: 'content_trade_id', + }, + inverseJoinColumn: { + name: 'content_ownership_id', + }, + }) + userItems: ContentOwnershipEntity[]; + + @ManyToMany(() => ContentOwnershipEntity) + @JoinTable({ + name: 'content_trade_recipient_content', + joinColumn: { + name: 'content_trade_id', + }, + inverseJoinColumn: { + name: 'content_ownership_id', + }, + }) + recipientItems: ContentOwnershipEntity[]; +} diff --git a/apps/catalog/src/database/entities/content-vote.entity.ts b/apps/catalog/src/database/entities/content-vote.entity.ts new file mode 100644 index 0000000..2e15e80 --- /dev/null +++ b/apps/catalog/src/database/entities/content-vote.entity.ts @@ -0,0 +1,36 @@ +import { Exclude, Expose } from 'class-transformer'; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ContentEntity } from './content.entity'; +import { MetaEntity } from '@freeblox/shared'; +import { Vote } from '../../enums/vote.enum'; + +@Entity('content_vote') +@Expose() +export class ContentVoteEntity extends MetaEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'content_id' }) + @Exclude() + contentId: number; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + @Index() + @Exclude() + userId: string; + + @Column({ type: 'enum', enum: Vote }) + vote: Vote; + + @ManyToOne(() => ContentEntity, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'content_id' }) + @Exclude() + content: ContentEntity; +} diff --git a/apps/catalog/src/database/entities/content.entity.ts b/apps/catalog/src/database/entities/content.entity.ts new file mode 100644 index 0000000..df94dcd --- /dev/null +++ b/apps/catalog/src/database/entities/content.entity.ts @@ -0,0 +1,109 @@ +import { UserMetaEntity } from '@freeblox/shared'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Privacy } from '../../enums/privacy.enum'; +import { ContentType } from '../../enums/content-type.enum'; +import { Exclude, Expose } from 'class-transformer'; +import { UserEntity } from 'apps/auth/src/database/entities/user.entity'; +import { + IsBoolean, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + MaxLength, +} from 'class-validator'; + +@Entity('content') +@Expose() +export class ContentEntity extends UserMetaEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 255 }) + @Index() + @IsString() + @IsNotEmpty() + @MaxLength(255) + name: string; + + @Column() + @IsString() + @IsNotEmpty() + @MaxLength(5000) + description: string; + + @Column({ type: 'uuid', name: 'user_id' }) + userId: string; + + @Column({ name: 'parent_id', nullable: true }) + parentId: number; + + @Column({ default: false }) + @IsBoolean() + @IsOptional() + restricted: boolean; + + @Column({ default: false }) + @IsBoolean() + @IsOptional() + onsale: boolean; + + @Column({ default: false }) + @IsBoolean() + @IsOptional() + published: boolean; + + @Column({ default: true, name: 'comments_enabled' }) + @IsBoolean() + @IsOptional() + commentsEnabled: boolean; + + @Column({ default: false, name: 'open_source' }) + @IsBoolean() + @IsOptional() + openSource: boolean; + + @Column({ type: 'enum', enum: Privacy, default: Privacy.PUBLIC }) + @IsEnum(Privacy) + privacy: Privacy; + + @Column({ type: 'string', enum: ContentType }) + @IsEnum(ContentType) + @Index() + type: ContentType; + + @Column({ unsigned: true, nullable: true }) + @IsNumber() + @IsOptional() + stock: number; + + @Column({ nullable: true }) + @IsString() + @IsOptional() + license: string; + + @ManyToOne(() => ContentEntity) + @JoinColumn({ name: 'parent_id' }) + @Exclude() + parent?: ContentEntity; + + @CreateDateColumn({ name: 'created_at' }) + @Expose() + createdAt: Date; + + @DeleteDateColumn({ name: 'deleted_at' }) + @Exclude() + deletedAt?: Date; + + user?: UserEntity; +} diff --git a/apps/catalog/src/database/migrations/20230722090425_content.ts b/apps/catalog/src/database/migrations/20230722090425_content.ts new file mode 100644 index 0000000..c2d9c99 --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722090425_content.ts @@ -0,0 +1,41 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('content', (table) => { + table.increments('id').primary(); + + table.string('name', 255).notNullable().index(); + table.text('description').notNullable(); + + table.uuid('user_id').nullable(); + table.integer('parent_id').unsigned().nullable(); + + table.boolean('restricted').defaultTo(false); + table.boolean('onsale').defaultTo(false).index(); + table.boolean('published').defaultTo(false); + table.boolean('comments_enabled').defaultTo(true); + table.boolean('open_source').defaultTo(false); + + table + .enum('privacy', ['public', 'friends', 'unlisted', 'private']) + .notNullable() + .defaultTo('public'); + + table.string('type').notNullable().index().defaultTo('content'); + + table.integer('stock').unsigned().nullable(); + table.text('license').nullable(); + + table.uuid('created_by').nullable(); + table.uuid('updated_by').nullable(); + + table.timestamps(true, true); + table.timestamp('deleted_at'); + + table.foreign('parent_id').references('content.id').onDelete('CASCADE'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('content'); +} diff --git a/apps/catalog/src/database/migrations/20230722091016_content-revision.ts b/apps/catalog/src/database/migrations/20230722091016_content-revision.ts new file mode 100644 index 0000000..cc85cf6 --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722091016_content-revision.ts @@ -0,0 +1,21 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('content_revision', (table) => { + table.increments('id').primary(); + + table.integer('content_id').unsigned().notNullable(); + + table.uuid('created_by').nullable(); + table.uuid('updated_by').nullable(); + + table.timestamps(true, true); + table.timestamp('deleted_at'); + + table.foreign('content_id').references('content.id').onDelete('CASCADE'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('content_revision'); +} diff --git a/apps/catalog/src/database/migrations/20230722091246_content-moderator-action.ts b/apps/catalog/src/database/migrations/20230722091246_content-moderator-action.ts new file mode 100644 index 0000000..eeba263 --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722091246_content-moderator-action.ts @@ -0,0 +1,54 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await Promise.all([ + knex.schema.createTable('content_moderation', (table) => { + table.increments('id').primary(); + + table.integer('content_id').unsigned().notNullable(); + table.integer('revision_id').unsigned().notNullable(); + + table.uuid('user_id').nullable(); + + table + .enum('action', ['pending', 'approve', 'reject', 'forward']) + .index() + .notNullable() + .defaultTo('pending'); + + table + .enum('rejection_reason', ['tos', 'illegal', 'dmca', 'other']) + .index() + .nullable(); + + table.text('description').nullable(); + + table.boolean('penalty').notNullable().defaultTo(false); + table.boolean('asset_delete').notNullable().defaultTo(false); + + table.timestamps(true, true); + table.timestamp('decided_at'); + + table.foreign('content_id').references('content.id').onDelete('CASCADE'); + table + .foreign('revision_id') + .references('content_revision.id') + .onDelete('CASCADE'); + }), + knex.schema.createTable('content_moderation_ban', (table) => { + table.integer('content_moderation_id').unsigned().notNullable(); + table.integer('ban_id').unsigned().nullable(); + table + .foreign('content_moderation_id') + .references('content_moderation.id') + .onDelete('CASCADE'); + }), + ]); +} + +export async function down(knex: Knex): Promise { + await Promise.all([ + knex.schema.dropTable('content_moderation'), + knex.schema.dropTable('content_moderation_ban'), + ]); +} diff --git a/apps/catalog/src/database/migrations/20230722091907_content-asset.ts b/apps/catalog/src/database/migrations/20230722091907_content-asset.ts new file mode 100644 index 0000000..ef16cf3 --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722091907_content-asset.ts @@ -0,0 +1,25 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('content_asset', (table) => { + table.increments('id').primary(); + + table.integer('content_id').unsigned().notNullable(); + table.integer('revision_id').unsigned().notNullable(); + table.uuid('asset_id').notNullable().index(); + + table.string('type').notNullable().index(); + table.string('type_name').nullable(); + table.integer('index').notNullable().defaultTo(0); + + table.foreign('content_id').references('content.id').onDelete('CASCADE'); + table + .foreign('revision_id') + .references('content_revision.id') + .onDelete('CASCADE'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('content_asset'); +} diff --git a/apps/catalog/src/database/migrations/20230722092320_content-price.ts b/apps/catalog/src/database/migrations/20230722092320_content-price.ts new file mode 100644 index 0000000..2c684ca --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722092320_content-price.ts @@ -0,0 +1,23 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('content_price', (table) => { + table.increments('id').primary(); + + table.integer('content_id').unsigned().notNullable(); + + table.integer('price').unsigned().notNullable().defaultTo(0).index(); + table.enum('currency', ['whole', 'denom']).notNullable().index(); + + table.uuid('created_by').nullable(); + table.uuid('updated_by').nullable(); + + table.timestamps(true, true); + + table.foreign('content_id').references('content.id').onDelete('CASCADE'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('content_price'); +} diff --git a/apps/catalog/src/database/migrations/20230722092916_content-ownership.ts b/apps/catalog/src/database/migrations/20230722092916_content-ownership.ts new file mode 100644 index 0000000..dca08f6 --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722092916_content-ownership.ts @@ -0,0 +1,40 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('content_ownership', (table) => { + table.increments('id').primary(); + + table.integer('content_id').unsigned().notNullable(); + table.uuid('user_id').notNullable(); + + table + .enum('source', ['author', 'purchase', 'trade', 'gift']) + .notNullable() + .defaultTo('author') + .index(); + + table.integer('previous_ownership_id').unsigned().nullable(); + + table.integer('purchase_price').nullable(); + table.enum('purchase_currency', ['whole', 'denom']).nullable(); + + table.integer('serial').unsigned().nullable(); + + table.uuid('created_by').nullable(); + table.uuid('updated_by').nullable(); + + table.timestamps(true, true); + table.timestamp('ended_at').nullable(); + table.timestamp('expires_at').nullable(); + + table.foreign('content_id').references('content.id').onDelete('CASCADE'); + table + .foreign('previous_ownership_id') + .references('content_ownership.id') + .onDelete('CASCADE'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('content_ownership'); +} diff --git a/apps/catalog/src/database/migrations/20230722093320_content-trade.ts b/apps/catalog/src/database/migrations/20230722093320_content-trade.ts new file mode 100644 index 0000000..4fc5dae --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722093320_content-trade.ts @@ -0,0 +1,61 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await Promise.all([ + knex.schema.createTable('content_trade', (table) => { + table.increments('id').primary(); + + table.uuid('user_id').notNullable(); + table.uuid('recipient_id').notNullable(); + + table + .enum('status', [ + 'sent', + 'opened', + 'rejected', + 'return_to_sender', + 'confirmed', + 'cancelled', + ]) + .notNullable() + .defaultTo('sent') + .index(); + + table.text('description').nullable(); + + table.timestamps(true, true); + }), + knex.schema.createTable('content_trade_user_content', (table) => { + table.integer('content_trade_id').unsigned().notNullable(); + table.integer('content_ownership_id').unsigned().notNullable(); + table + .foreign('content_trade_id') + .references('content_trade.id') + .onDelete('CASCADE'); + table + .foreign('content_ownership_id') + .references('content_ownership.id') + .onDelete('CASCADE'); + }), + knex.schema.createTable('content_trade_recipient_content', (table) => { + table.integer('content_trade_id').unsigned().notNullable(); + table.integer('content_ownership_id').unsigned().notNullable(); + table + .foreign('content_trade_id') + .references('content_trade.id') + .onDelete('CASCADE'); + table + .foreign('content_ownership_id') + .references('content_ownership.id') + .onDelete('CASCADE'); + }), + ]); +} + +export async function down(knex: Knex): Promise { + await Promise.all([ + knex.schema.dropTable('content_trade'), + knex.schema.dropTable('content_trade_user_content'), + knex.schema.dropTable('content_trade_recipient_content'), + ]); +} diff --git a/apps/catalog/src/database/migrations/20230722093744_content-favorite.ts b/apps/catalog/src/database/migrations/20230722093744_content-favorite.ts new file mode 100644 index 0000000..f06621e --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722093744_content-favorite.ts @@ -0,0 +1,18 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('content_favorite', (table) => { + table.increments('id').primary(); + + table.integer('content_id').unsigned().notNullable(); + table.uuid('user_id').index().notNullable(); + + table.timestamps(true, true); + + table.foreign('content_id').references('content.id').onDelete('CASCADE'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('content_favorite'); +} diff --git a/apps/catalog/src/database/migrations/20230722093836_content-vote.ts b/apps/catalog/src/database/migrations/20230722093836_content-vote.ts new file mode 100644 index 0000000..afe2606 --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722093836_content-vote.ts @@ -0,0 +1,20 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('content_vote', (table) => { + table.increments('id').primary(); + + table.integer('content_id').index().unsigned().notNullable(); + table.uuid('user_id').notNullable(); + + table.enum('vote', ['up', 'down']).index().notNullable(); + + table.timestamps(true, true); + + table.foreign('content_id').references('content.id').onDelete('CASCADE'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('content_vote'); +} diff --git a/apps/catalog/src/database/migrations/20230722093939_content-report.ts b/apps/catalog/src/database/migrations/20230722093939_content-report.ts new file mode 100644 index 0000000..d0b348c --- /dev/null +++ b/apps/catalog/src/database/migrations/20230722093939_content-report.ts @@ -0,0 +1,47 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await Promise.all([ + knex.schema.createTable('content_report', (table) => { + table.increments('id').primary(); + + table.integer('content_id').index().unsigned().notNullable(); + table.uuid('user_id').notNullable(); + table.uuid('moderator_id').nullable(); + + table.text('reason').notNullable(); + table.text('description').notNullable(); + table.text('notes').nullable(); + + table + .enum('status', ['open', 'closed', 'invalid', 'resolved']) + .index() + .notNullable() + .defaultTo('open'); + + table.timestamps(true, true); + table.timestamp('resolved_at'); + + table.foreign('content_id').references('content.id').onDelete('CASCADE'); + }), + knex.schema.createTable('content_report_action', (table) => { + table.integer('content_report_id').index().unsigned().notNullable(); + table.integer('content_moderation_id').index().unsigned().notNullable(); + table + .foreign('content_report_id') + .references('content_report.id') + .onDelete('CASCADE'); + table + .foreign('content_moderation_id') + .references('content_moderation.id') + .onDelete('CASCADE'); + }), + ]); +} + +export async function down(knex: Knex): Promise { + await Promise.all([ + knex.schema.dropTable('content_report'), + knex.schema.dropTable('content_report_action'), + ]); +} diff --git a/apps/catalog/src/enums/content-asset-type.enum.ts b/apps/catalog/src/enums/content-asset-type.enum.ts new file mode 100644 index 0000000..cf1ac30 --- /dev/null +++ b/apps/catalog/src/enums/content-asset-type.enum.ts @@ -0,0 +1,11 @@ +export enum ContentAssetType { + IMAGE = 'image', + TEXTURE = 'texture', + TEXTURE3D = 'texture3d', + MESH = 'mesh', + ANIMATION = 'animation', + GAMEOBJECT = 'gameobject', + WORLD = 'world', + CHARACTER = 'character', + SOUND = 'sound', +} diff --git a/apps/catalog/src/enums/content-type.enum.ts b/apps/catalog/src/enums/content-type.enum.ts new file mode 100644 index 0000000..9160ce9 --- /dev/null +++ b/apps/catalog/src/enums/content-type.enum.ts @@ -0,0 +1,18 @@ +export enum ContentType { + CONTENT = 'content', + CHARACTER = 'character', + HAT = 'hat', + ACCESSORY = 'accessory', + FRONT = 'front', + BACK = 'back', + TOOL = 'tool', + MESH = 'mesh', + TEXTURE = 'texture', + GAMEOBJECT = 'gameobject', + SOUND = 'sound', + ANIMATION = 'animation', + COMMENT = 'comment', + STATUS = 'status', + GAME = 'game', + WORLD = 'world', +} diff --git a/apps/catalog/src/enums/currency.enum.ts b/apps/catalog/src/enums/currency.enum.ts new file mode 100644 index 0000000..20b8947 --- /dev/null +++ b/apps/catalog/src/enums/currency.enum.ts @@ -0,0 +1,4 @@ +export enum Currency { + WHOLE = 'whole', + DENOMINATION = 'denom', +} diff --git a/apps/catalog/src/enums/moderation-action.enum.ts b/apps/catalog/src/enums/moderation-action.enum.ts new file mode 100644 index 0000000..791c8e2 --- /dev/null +++ b/apps/catalog/src/enums/moderation-action.enum.ts @@ -0,0 +1,6 @@ +export enum ModeratorAction { + PENDING = 'pending', + APPROVE = 'approve', + REJECT = 'reject', + FORWARD = 'forward', +} diff --git a/apps/catalog/src/enums/privacy.enum.ts b/apps/catalog/src/enums/privacy.enum.ts new file mode 100644 index 0000000..13f5dc3 --- /dev/null +++ b/apps/catalog/src/enums/privacy.enum.ts @@ -0,0 +1,6 @@ +export enum Privacy { + PUBLIC = 'public', + FRIENDS = 'friends', + UNLISTED = 'unlisted', + PRIVATE = 'private', +} diff --git a/apps/catalog/src/enums/rejection-reason.enum.ts b/apps/catalog/src/enums/rejection-reason.enum.ts new file mode 100644 index 0000000..02e9503 --- /dev/null +++ b/apps/catalog/src/enums/rejection-reason.enum.ts @@ -0,0 +1,6 @@ +export enum RejectionReason { + TERMS_OF_SERVICE = 'tos', + ILLEGAL_CONTENT = 'illegal', + DMCA = 'dmca', + OTHER = 'other', +} diff --git a/apps/catalog/src/enums/report-status.enum.ts b/apps/catalog/src/enums/report-status.enum.ts new file mode 100644 index 0000000..f977497 --- /dev/null +++ b/apps/catalog/src/enums/report-status.enum.ts @@ -0,0 +1,6 @@ +export enum ReportStatus { + OPEN = 'open', + CLOSED = 'closed', + INVALID = 'invalid', + RESOLVED = 'resolved', +} diff --git a/apps/catalog/src/enums/trade-status.enum.ts b/apps/catalog/src/enums/trade-status.enum.ts new file mode 100644 index 0000000..3e524ab --- /dev/null +++ b/apps/catalog/src/enums/trade-status.enum.ts @@ -0,0 +1,8 @@ +export enum TradeStatus { + SENT = 'sent', + OPENED = 'opened', + REJECTED = 'rejected', + RETURN_TO_SENDER = 'return_to_sender', + CONFIRMED = 'confirmed', + CANCELLED = 'cancelled', +} diff --git a/apps/catalog/src/enums/vote.enum.ts b/apps/catalog/src/enums/vote.enum.ts new file mode 100644 index 0000000..3766f8f --- /dev/null +++ b/apps/catalog/src/enums/vote.enum.ts @@ -0,0 +1,4 @@ +export enum Vote { + UPVOTE = 'up', + DOWNVOTE = 'down', +} diff --git a/apps/catalog/src/knexfile.ts b/apps/catalog/src/knexfile.ts new file mode 100644 index 0000000..b432566 --- /dev/null +++ b/apps/catalog/src/knexfile.ts @@ -0,0 +1,5 @@ +import { getKnex } from '../../../libs/shared/src/'; + +module.exports = { + development: getKnex('catalog', __dirname, ['.ts', '.js']), +};