diff --git a/apps/api/src/characters/entities/character.entity.ts b/apps/api/src/characters/entities/character.entity.ts index 5318267..97d74ae 100644 --- a/apps/api/src/characters/entities/character.entity.ts +++ b/apps/api/src/characters/entities/character.entity.ts @@ -17,11 +17,8 @@ export class Character { @Column({ name: 'name', type: 'varchar', length: 150 }) name!: string; - @Column({ name: 'level', type: 'integer' }) - level!: number; - - @Column({ name: 'experience', type: 'integer' }) - experience!: number; + @Column({ name: 'renown', type: 'integer' }) + renown!: number; @Column({ name: 'silver', type: 'integer' }) silver!: number; diff --git a/apps/api/src/database/migrations/renown-and-reputation-entities.metadata.spec.ts b/apps/api/src/database/migrations/renown-and-reputation-entities.metadata.spec.ts new file mode 100644 index 0000000..bf67ad0 --- /dev/null +++ b/apps/api/src/database/migrations/renown-and-reputation-entities.metadata.spec.ts @@ -0,0 +1,71 @@ +import 'reflect-metadata'; +import { getMetadataArgsStorage } from 'typeorm'; +import { Character } from '../../characters/entities/character.entity'; +import { ItemDefinition } from '../../items/entities/item-definition.entity'; +import { ItemType } from '../../items/item-type.enum'; +import { CombatReward } from '../../rewards/entities/combat-reward.entity'; +import { ReputationFaction } from '../../reputation/entities/reputation-faction.entity'; +import { CharacterReputation } from '../../reputation/entities/character-reputation.entity'; +import { RenownMilestoneDefinition } from '../../renown/entities/renown-milestone-definition.entity'; +import { CharacterRenownMilestone } from '../../renown/entities/character-renown-milestone.entity'; +import { TurnInDefinition } from '../../turn-in/entities/turn-in-definition.entity'; + +function columnNames(target: unknown): string[] { + return getMetadataArgsStorage() + .columns.filter((column) => column.target === target) + .map((column) => column.propertyName); +} + +function uniqueIndexFor(target: unknown, columns: string[]): boolean { + const index = getMetadataArgsStorage().indices.find( + (candidate) => + candidate.target === target && columns.every((column) => candidate.columns?.includes(column)), + ); + const meta = index as typeof index & { options?: { unique?: boolean }; unique?: boolean }; + return (meta?.options?.unique ?? meta?.unique) === true; +} + +describe('Slice 0.6.5 entity metadata', () => { + it('Character exposes renown and no longer exposes level/experience', () => { + const names = columnNames(Character); + expect(names).toContain('renown'); + expect(names).not.toContain('level'); + expect(names).not.toContain('experience'); + }); + + it('ItemDefinition no longer exposes requiredLevel', () => { + expect(columnNames(ItemDefinition)).not.toContain('requiredLevel'); + }); + + it('ItemType has exactly the five Slice 0.6.5 values', () => { + expect(Object.values(ItemType).sort()).toEqual( + ['CONSUMABLE', 'EQUIPMENT', 'QUEST_ITEM', 'TRADE_GOOD', 'TROPHY'].sort(), + ); + }); + + it('CombatReward no longer exposes experienceGranted', () => { + expect(columnNames(CombatReward)).not.toContain('experienceGranted'); + }); + + it('ReputationFaction has a unique key', () => { + expect(uniqueIndexFor(ReputationFaction, ['key'])).toBe(true); + }); + + it('CharacterReputation enforces one row per character per faction', () => { + expect(uniqueIndexFor(CharacterReputation, ['characterId', 'factionId'])).toBe(true); + }); + + it('RenownMilestoneDefinition has a unique key', () => { + expect(uniqueIndexFor(RenownMilestoneDefinition, ['key'])).toBe(true); + }); + + it('CharacterRenownMilestone enforces one row per character per milestone', () => { + expect( + uniqueIndexFor(CharacterRenownMilestone, ['characterId', 'milestoneId']), + ).toBe(true); + }); + + it('TurnInDefinition has a unique key', () => { + expect(uniqueIndexFor(TurnInDefinition, ['key'])).toBe(true); + }); +}); diff --git a/apps/api/src/items/entities/item-definition.entity.ts b/apps/api/src/items/entities/item-definition.entity.ts index c89adc8..69281a1 100644 --- a/apps/api/src/items/entities/item-definition.entity.ts +++ b/apps/api/src/items/entities/item-definition.entity.ts @@ -43,9 +43,6 @@ export class ItemDefinition { @Column({ name: 'tier', type: 'integer' }) tier!: number; - @Column({ name: 'required_level', type: 'integer' }) - requiredLevel!: number; - @Column({ name: 'weapon_damage', type: 'integer' }) weaponDamage!: number; diff --git a/apps/api/src/items/item-type.enum.ts b/apps/api/src/items/item-type.enum.ts index 9050b87..269a91c 100644 --- a/apps/api/src/items/item-type.enum.ts +++ b/apps/api/src/items/item-type.enum.ts @@ -1,6 +1,7 @@ export enum ItemType { - WEAPON = 'WEAPON', - ARMOR = 'ARMOR', - MATERIAL = 'MATERIAL', + EQUIPMENT = 'EQUIPMENT', + TRADE_GOOD = 'TRADE_GOOD', + TROPHY = 'TROPHY', + QUEST_ITEM = 'QUEST_ITEM', CONSUMABLE = 'CONSUMABLE', } diff --git a/apps/api/src/renown/entities/character-renown-milestone.entity.ts b/apps/api/src/renown/entities/character-renown-milestone.entity.ts new file mode 100644 index 0000000..4bd0ae0 --- /dev/null +++ b/apps/api/src/renown/entities/character-renown-milestone.entity.ts @@ -0,0 +1,39 @@ +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Character } from '../../characters/entities/character.entity'; +import { RenownMilestoneDefinition } from './renown-milestone-definition.entity'; + +@Entity({ name: 'character_renown_milestones' }) +@Index('IDX_character_renown_milestones_character_milestone', ['characterId', 'milestoneId'], { + unique: true, +}) +export class CharacterRenownMilestone { + @PrimaryGeneratedColumn('uuid', { name: 'id' }) + id!: string; + + @Column({ name: 'character_id', type: 'uuid' }) + characterId!: string; + + @Column({ name: 'milestone_id', type: 'uuid' }) + milestoneId!: string; + + @Column({ name: 'completed_at', type: 'timestamptz' }) + completedAt!: Date; + + @Column({ name: 'times_completed', type: 'integer' }) + timesCompleted!: number; + + @ManyToOne(() => Character, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'character_id' }) + character!: Character; + + @ManyToOne(() => RenownMilestoneDefinition, { onDelete: 'RESTRICT' }) + @JoinColumn({ name: 'milestone_id' }) + milestone!: RenownMilestoneDefinition; +} diff --git a/apps/api/src/renown/entities/renown-milestone-definition.entity.ts b/apps/api/src/renown/entities/renown-milestone-definition.entity.ts new file mode 100644 index 0000000..a51d19c --- /dev/null +++ b/apps/api/src/renown/entities/renown-milestone-definition.entity.ts @@ -0,0 +1,39 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity({ name: 'renown_milestone_definitions' }) +@Index('IDX_renown_milestone_definitions_key', ['key'], { unique: true }) +export class RenownMilestoneDefinition { + @PrimaryGeneratedColumn('uuid', { name: 'id' }) + id!: string; + + @Column({ name: 'key', type: 'varchar', length: 100 }) + key!: string; + + @Column({ name: 'name', type: 'varchar', length: 150 }) + name!: string; + + @Column({ name: 'description', type: 'text' }) + description!: string; + + @Column({ name: 'renown_reward', type: 'integer' }) + renownReward!: number; + + @Column({ name: 'repeatable', type: 'boolean' }) + repeatable!: boolean; + + @Column({ name: 'enabled', type: 'boolean' }) + enabled!: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt!: Date; +} diff --git a/apps/api/src/reputation/entities/character-reputation.entity.ts b/apps/api/src/reputation/entities/character-reputation.entity.ts new file mode 100644 index 0000000..6f3cb5f --- /dev/null +++ b/apps/api/src/reputation/entities/character-reputation.entity.ts @@ -0,0 +1,44 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Character } from '../../characters/entities/character.entity'; +import { ReputationFaction } from './reputation-faction.entity'; + +@Entity({ name: 'character_reputation' }) +@Index('IDX_character_reputation_character_faction', ['characterId', 'factionId'], { + unique: true, +}) +export class CharacterReputation { + @PrimaryGeneratedColumn('uuid', { name: 'id' }) + id!: string; + + @Column({ name: 'character_id', type: 'uuid' }) + characterId!: string; + + @Column({ name: 'faction_id', type: 'uuid' }) + factionId!: string; + + @Column({ name: 'reputation', type: 'integer' }) + reputation!: number; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt!: Date; + + @ManyToOne(() => Character, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'character_id' }) + character!: Character; + + @ManyToOne(() => ReputationFaction, { onDelete: 'RESTRICT' }) + @JoinColumn({ name: 'faction_id' }) + faction!: ReputationFaction; +} diff --git a/apps/api/src/reputation/entities/reputation-faction.entity.ts b/apps/api/src/reputation/entities/reputation-faction.entity.ts new file mode 100644 index 0000000..dfae297 --- /dev/null +++ b/apps/api/src/reputation/entities/reputation-faction.entity.ts @@ -0,0 +1,36 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity({ name: 'reputation_factions' }) +@Index('IDX_reputation_factions_key', ['key'], { unique: true }) +export class ReputationFaction { + @PrimaryGeneratedColumn('uuid', { name: 'id' }) + id!: string; + + @Column({ name: 'key', type: 'varchar', length: 100 }) + key!: string; + + @Column({ name: 'name', type: 'varchar', length: 150 }) + name!: string; + + @Column({ name: 'description', type: 'text' }) + description!: string; + + @Column({ name: 'region_key', type: 'varchar', length: 100 }) + regionKey!: string; + + @Column({ name: 'enabled', type: 'boolean' }) + enabled!: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt!: Date; +} diff --git a/apps/api/src/rewards/entities/combat-reward.entity.ts b/apps/api/src/rewards/entities/combat-reward.entity.ts index ff467c0..ce242d2 100644 --- a/apps/api/src/rewards/entities/combat-reward.entity.ts +++ b/apps/api/src/rewards/entities/combat-reward.entity.ts @@ -29,9 +29,6 @@ export class CombatReward { @Column({ name: 'character_id', type: 'uuid' }) characterId!: string; - @Column({ name: 'experience_granted', type: 'integer' }) - experienceGranted!: number; - @Column({ name: 'silver_granted', type: 'integer' }) silverGranted!: number; diff --git a/apps/api/src/turn-in/entities/turn-in-definition.entity.ts b/apps/api/src/turn-in/entities/turn-in-definition.entity.ts new file mode 100644 index 0000000..fc02277 --- /dev/null +++ b/apps/api/src/turn-in/entities/turn-in-definition.entity.ts @@ -0,0 +1,54 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { ItemDefinition } from '../../items/entities/item-definition.entity'; +import { ReputationFaction } from '../../reputation/entities/reputation-faction.entity'; + +@Entity({ name: 'turn_in_definitions' }) +@Index('IDX_turn_in_definitions_key', ['key'], { unique: true }) +export class TurnInDefinition { + @PrimaryGeneratedColumn('uuid', { name: 'id' }) + id!: string; + + @Column({ name: 'key', type: 'varchar', length: 100 }) + key!: string; + + @Column({ name: 'item_definition_id', type: 'uuid' }) + itemDefinitionId!: string; + + @Column({ name: 'faction_id', type: 'uuid' }) + factionId!: string; + + @Column({ name: 'silver_reward_per_item', type: 'integer' }) + silverRewardPerItem!: number; + + @Column({ name: 'reputation_reward_per_item', type: 'integer' }) + reputationRewardPerItem!: number; + + @Column({ name: 'repeatable', type: 'boolean' }) + repeatable!: boolean; + + @Column({ name: 'enabled', type: 'boolean' }) + enabled!: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt!: Date; + + @ManyToOne(() => ItemDefinition, { onDelete: 'RESTRICT' }) + @JoinColumn({ name: 'item_definition_id' }) + itemDefinition!: ItemDefinition; + + @ManyToOne(() => ReputationFaction, { onDelete: 'RESTRICT' }) + @JoinColumn({ name: 'faction_id' }) + faction!: ReputationFaction; +}